feat(skills): always_inject belongs beside the skill, not in one database
Migration 0083 added the column for a measured failure — under the `index` arm, `workspace-repo-commit-protocol` scored Trigger=FAIL while its boundary check passed, because a rule that applies to everyone who writes reads to each agent as nobody's in particular. The column shipped and was never set: prod ran 0 of 53 skills flagged, and the post-v0.8.5 validation mission made 76 tool calls with ZERO ReadMcpResourceTool among them. Not plumbing — the door answered 200 from inside that container, and the agents used ToolSearch four times to reach for other tools they did not have. Setting it by hand fixes one database. A rebuilt one comes up un-flagged, with nothing in the repo recording that the skill was ever meant to be injected — the same shape as every silent-success defect in this project. So the frontmatter carries it, the loader parses it, and the upsert writes it. The file wins on conflict: builtins are code-managed, and a setting that exists only in one database is a setting nobody can find. Guarded both ways. `always_inject` defaults FALSE, because defaulting true would quietly abolish the index arm rather than fix it; and a test asserts the shipped skill still carries the flag, verified by flipping it to false and watching the test fail. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
This commit is contained in:
co-authored by
Claude Opus 5
parent
bda6bef4db
commit
42c24de6a9
@@ -7,6 +7,13 @@
|
|||||||
//! description: <one-line, shown to the LLM in resources/list>
|
//! description: <one-line, shown to the LLM in resources/list>
|
||||||
//! when_to_use: <trigger sentence, appended to description>
|
//! when_to_use: <trigger sentence, appended to description>
|
||||||
//! tags: [foundation, rust, ...]
|
//! tags: [foundation, rust, ...]
|
||||||
|
//! always_inject: true # optional, default false
|
||||||
|
//!
|
||||||
|
//! `always_inject` makes the body reach the agent in full even under the
|
||||||
|
//! `index` (progressive-disclosure) arm. It is for a CROSS-CUTTING procedure —
|
||||||
|
//! one that applies to everyone who writes, and so reads to each agent as
|
||||||
|
//! nobody's in particular, which is how `workspace-repo-commit-protocol`
|
||||||
|
//! scored Trigger=FAIL beside a passing boundary check.
|
||||||
//!
|
//!
|
||||||
//! The body is the rest of the file. Both are upserted idempotently:
|
//! The body is the rest of the file. Both are upserted idempotently:
|
||||||
//! `skills_catalog::upsert_builtin` bumps the version + appends to
|
//! `skills_catalog::upsert_builtin` bumps the version + appends to
|
||||||
@@ -27,6 +34,8 @@ struct Frontmatter {
|
|||||||
when_to_use: Option<String>,
|
when_to_use: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
tags: Vec<String>,
|
tags: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
always_inject: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn skills_dir() -> PathBuf {
|
fn skills_dir() -> PathBuf {
|
||||||
@@ -120,6 +129,7 @@ async fn load_one(pool: &PgPool, path: &std::path::Path) -> Result<String, Strin
|
|||||||
when_to_use: fm.when_to_use.as_deref(),
|
when_to_use: fm.when_to_use.as_deref(),
|
||||||
tags: fm.tags.clone(),
|
tags: fm.tags.clone(),
|
||||||
body,
|
body,
|
||||||
|
always_inject: fm.always_inject,
|
||||||
};
|
};
|
||||||
upsert_builtin(pool, skill)
|
upsert_builtin(pool, skill)
|
||||||
.await
|
.await
|
||||||
@@ -158,6 +168,36 @@ mod tests {
|
|||||||
assert!(split_frontmatter("# plain md\n").is_none());
|
assert!(split_frontmatter("# plain md\n").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn always_inject_is_opt_in_and_parses() {
|
||||||
|
let off: Frontmatter = serde_yaml::from_str("name: a\ndescription: b\n").unwrap();
|
||||||
|
assert!(
|
||||||
|
!off.always_inject,
|
||||||
|
"full delivery must be opted INTO — defaulting true would abolish the index arm"
|
||||||
|
);
|
||||||
|
let on: Frontmatter =
|
||||||
|
serde_yaml::from_str("name: a\ndescription: b\nalways_inject: true\n").unwrap();
|
||||||
|
assert!(on.always_inject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The flag reached production as a hand-run UPDATE first, which a rebuilt
|
||||||
|
/// database would have silently dropped. This asserts the repo carries it,
|
||||||
|
/// so the cross-cutting skill cannot go back to being deliverable only by
|
||||||
|
/// an agent noticing it applies — the exact failure it was measured on.
|
||||||
|
#[test]
|
||||||
|
fn the_commit_protocol_ships_marked_for_full_delivery() {
|
||||||
|
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../../skills/foundation/workspace-repo-commit-protocol.md");
|
||||||
|
let text = std::fs::read_to_string(&path).expect("read the commit-protocol skill");
|
||||||
|
let (yaml, _) = split_frontmatter(&text).expect("frontmatter");
|
||||||
|
let fm: Frontmatter = serde_yaml::from_str(yaml).expect("parse frontmatter");
|
||||||
|
assert!(
|
||||||
|
fm.always_inject,
|
||||||
|
"workspace-repo-commit-protocol must be always_inject: it applies to everyone \
|
||||||
|
who writes, and under the index arm it scored Trigger=FAIL unread"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builtin_id_stable() {
|
fn builtin_id_stable() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -80,6 +80,13 @@ pub struct UpsertBuiltinSkill<'a> {
|
|||||||
pub when_to_use: Option<&'a str>,
|
pub when_to_use: Option<&'a str>,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
pub body: &'a str,
|
pub body: &'a str,
|
||||||
|
/// Deliver this skill's full body even under the `index` arm.
|
||||||
|
///
|
||||||
|
/// Carried from frontmatter so the decision lives beside the skill it is
|
||||||
|
/// about. It was a bare DB column first, which meant a rebuilt database
|
||||||
|
/// came up with every skill un-flagged and nothing in the repo recording
|
||||||
|
/// that any of them were meant to be injected.
|
||||||
|
pub always_inject: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Idempotent upsert for builtin skills. Bumps `current_version` +
|
/// Idempotent upsert for builtin skills. Bumps `current_version` +
|
||||||
@@ -116,8 +123,8 @@ pub async fn upsert_builtin(pool: &PgPool, b: UpsertBuiltinSkill<'_>) -> Result<
|
|||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO skills
|
"INSERT INTO skills
|
||||||
(id, name, title, author, description, when_to_use, tags,
|
(id, name, title, author, description, when_to_use, tags,
|
||||||
source_kind, workspace_id, current_version, body)
|
source_kind, workspace_id, current_version, body, always_inject)
|
||||||
VALUES ($1,$2,$2,'system',$3,$4,$5,'builtin',NULL,$6,$7)
|
VALUES ($1,$2,$2,'system',$3,$4,$5,'builtin',NULL,$6,$7,$8)
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
name = EXCLUDED.name,
|
name = EXCLUDED.name,
|
||||||
title = EXCLUDED.title,
|
title = EXCLUDED.title,
|
||||||
@@ -126,6 +133,11 @@ pub async fn upsert_builtin(pool: &PgPool, b: UpsertBuiltinSkill<'_>) -> Result<
|
|||||||
tags = EXCLUDED.tags,
|
tags = EXCLUDED.tags,
|
||||||
current_version = EXCLUDED.current_version,
|
current_version = EXCLUDED.current_version,
|
||||||
body = EXCLUDED.body,
|
body = EXCLUDED.body,
|
||||||
|
-- The FILE wins. An operator who flips this column by hand gets
|
||||||
|
-- it restored to what the frontmatter says on the next boot,
|
||||||
|
-- which is the point: builtins are code-managed, and a setting
|
||||||
|
-- that only exists in one database is a setting nobody can find.
|
||||||
|
always_inject = EXCLUDED.always_inject,
|
||||||
updated_at = now()",
|
updated_at = now()",
|
||||||
)
|
)
|
||||||
.bind(b.id)
|
.bind(b.id)
|
||||||
@@ -135,6 +147,7 @@ pub async fn upsert_builtin(pool: &PgPool, b: UpsertBuiltinSkill<'_>) -> Result<
|
|||||||
.bind(&b.tags)
|
.bind(&b.tags)
|
||||||
.bind(next_version)
|
.bind(next_version)
|
||||||
.bind(b.body)
|
.bind(b.body)
|
||||||
|
.bind(b.always_inject)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ name: workspace-repo-commit-protocol
|
|||||||
description: How to work inside /mission/repo — the mission's checked-out codebase — and how to commit meaningful changes back.
|
description: How to work inside /mission/repo — the mission's checked-out codebase — and how to commit meaningful changes back.
|
||||||
when_to_use: You are a coder, committer, or any role that edits code. Pin this at turn start so you never lose orientation.
|
when_to_use: You are a coder, committer, or any role that edits code. Pin this at turn start so you never lose orientation.
|
||||||
tags: [foundation, coding, git]
|
tags: [foundation, coding, git]
|
||||||
|
always_inject: true
|
||||||
---
|
---
|
||||||
|
|
||||||
# Mission repo + commit protocol
|
# Mission repo + commit protocol
|
||||||
|
|||||||
Reference in New Issue
Block a user