diff --git a/AGENTS.md b/AGENTS.md index fc8118c..e2ee58b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,7 +53,8 @@ hand. `aep:planning` is the instruction surface for this. Commit and push through `b10x-gates bot --repo . -- ` as `b10x-bot[bot]`; keep hooks. No credential or token machinery lives in this repository. A release is a bare annotated tag on -`main` after `CHANGELOG.md`, the workspace version and every carried plugin manifest agree; the +`main` after `CHANGELOG.md`, the workspace version, every carried plugin manifest and every +`**Skill version**` line agree; the release workflow reruns the gate and publishes the `b10x` archives, `SHA256SUMS` and `SETUP.md`, with the version's `CHANGELOG.md` section as the release notes. Source publication needs no Atlas checkout. diff --git a/CHANGELOG.md b/CHANGELOG.md index f3cd799..7746fc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [0.14.16] — 2026-09-26 + +Commands: operator-only entry points for work that is started by hand. + +- R3 gains a third kind of skill, the command: a verb name, `disable-model-invocation: true`, at + most 20 lines of body, handing off to exactly one activity skill of its plugin, with an + `agents/openai.yaml` that sets `policy.allow_implicit_invocation: false` for Codex. + `agentplugins-check` enforces each part. +- `worktree:cleanup` (`/worktree:cleanup`) reviews managed trees and removes only approved ids, + through `worktree:managing-worktrees`. +- `verified.json` moves `worktree` to 0.8.1 after `agentplugins-check tools` passed against it + (19 spelled commands). +- `aep:wave` (`/aep:wave`) and `aep:drive` (`/aep:drive`) start `aep:implementing` in wave or drive + mode. They are no longer retired names. +- The `**Skill version**` line in `aep:implementing`, `aep:planning` and `aep:migrating` read + 0.14.2; it now reads the plugin version, and the gate refuses a line that differs from + `.claude-plugin/plugin.json`. +- `b10x:routing` lists the three commands. + ## [0.14.15] — 2026-09-26 `worktree:managing-worktrees` follows `worktree` 0.8.0; skills verified against `aep` 0.60.0, `ess` diff --git a/Cargo.lock b/Cargo.lock index cdb6d8f..d73755a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "agentplugins-check" -version = "0.14.15" +version = "0.14.16" dependencies = [ "clap", "serde", @@ -76,7 +76,7 @@ dependencies = [ [[package]] name = "b10x" -version = "0.14.15" +version = "0.14.16" dependencies = [ "clap", "serde", diff --git a/Cargo.toml b/Cargo.toml index fd433ad..20ea0be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/agentplugins-check", "crates/b10x"] [workspace.package] -version = "0.14.15" +version = "0.14.16" edition = "2021" rust-version = "1.85" license = "Apache-2.0" diff --git a/README.md b/README.md index f99b7b1..c684305 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,10 @@ What each plugin ships: - skills: [`init`](plugins/ess/skills/init/SKILL.md) · [`upgrade`](plugins/ess/skills/upgrade/SKILL.md) · [`hardening`](plugins/ess/skills/hardening/SKILL.md) · [`retrofitting`](plugins/ess/skills/retrofitting/SKILL.md) · [`specifying`](plugins/ess/skills/specifying/SKILL.md) · [`testing-conformance`](plugins/ess/skills/testing-conformance/SKILL.md) - agents: [`author`](plugins/ess/agents/author.md) · [`conformance`](plugins/ess/agents/conformance.md) · [`retrofitter`](plugins/ess/agents/retrofitter.md) - [`aep`](plugins/aep/) · [docs](website/docs/plugins/aep.md) - - skills: [`init`](plugins/aep/skills/init/SKILL.md) · [`upgrade`](plugins/aep/skills/upgrade/SKILL.md) · [`implementing`](plugins/aep/skills/implementing/SKILL.md) · [`migrating`](plugins/aep/skills/migrating/SKILL.md) · [`planning`](plugins/aep/skills/planning/SKILL.md) + - skills: [`init`](plugins/aep/skills/init/SKILL.md) · [`upgrade`](plugins/aep/skills/upgrade/SKILL.md) · [`drive`](plugins/aep/skills/drive/SKILL.md) · [`implementing`](plugins/aep/skills/implementing/SKILL.md) · [`migrating`](plugins/aep/skills/migrating/SKILL.md) · [`planning`](plugins/aep/skills/planning/SKILL.md) · [`wave`](plugins/aep/skills/wave/SKILL.md) - agents: [`adversary`](plugins/aep/agents/adversary.md) · [`decomposer`](plugins/aep/agents/decomposer.md) · [`implementor`](plugins/aep/agents/implementor.md) · [`plan-critic-acceptance`](plugins/aep/agents/plan-critic-acceptance.md) · [`plan-critic-design`](plugins/aep/agents/plan-critic-design.md) · [`plan-critic-parallel-safety`](plugins/aep/agents/plan-critic-parallel-safety.md) · [`plan-critic-scope`](plugins/aep/agents/plan-critic-scope.md) · [`plan-reviewer`](plugins/aep/agents/plan-reviewer.md) · [`reverse-engineer`](plugins/aep/agents/reverse-engineer.md) · [`security-reviewer`](plugins/aep/agents/security-reviewer.md) · [`story-scoper`](plugins/aep/agents/story-scoper.md) - [`worktree`](plugins/worktree/) · [docs](website/docs/plugins/worktree.md) - - skills: [`init`](plugins/worktree/skills/init/SKILL.md) · [`upgrade`](plugins/worktree/skills/upgrade/SKILL.md) · [`managing-worktrees`](plugins/worktree/skills/managing-worktrees/SKILL.md) + - skills: [`init`](plugins/worktree/skills/init/SKILL.md) · [`upgrade`](plugins/worktree/skills/upgrade/SKILL.md) · [`cleanup`](plugins/worktree/skills/cleanup/SKILL.md) · [`managing-worktrees`](plugins/worktree/skills/managing-worktrees/SKILL.md) - [`connectors`](plugins/connectors/) · [docs](website/docs/plugins/connectors.md) - skills: [`init`](plugins/connectors/skills/init/SKILL.md) · [`upgrade`](plugins/connectors/skills/upgrade/SKILL.md) · [`integrating`](plugins/connectors/skills/integrating/SKILL.md) - [`b10x`](plugins/b10x/) · [docs](website/docs/plugins/b10x.md) diff --git a/crates/agentplugins-check/src/concept.rs b/crates/agentplugins-check/src/concept.rs index 14c5765..f34149d 100644 --- a/crates/agentplugins-check/src/concept.rs +++ b/crates/agentplugins-check/src/concept.rs @@ -3,8 +3,13 @@ //! after one had been agreed. //! //! - **R2** one plugin per product; plugin name = product id = the CLI it drives. -//! - **R3** a skill is an activity named in `-ing` form, one or two words, never its plugin's name. +//! - **R3** a skill is a lifecycle skill (`init`, `upgrade`), an activity named in `-ing` form, or +//! a command: a verb the operator alone starts (`disable-model-invocation: true`), at most +//! [`COMMAND_LINES`] lines, handing off to exactly one activity of its plugin. None is named after +//! its plugin. //! - **R4** an agent is owned by exactly one skill of its plugin, which lists it under `## Agents`. +//! - **R5** a skill quotes no CLI version, and a `**Skill version X**` line names the version its +//! plugin's `.claude-plugin/plugin.json` carries. //! - **R7** every `:` id written in this repository resolves. //! - **R8** one README row, one plugin page and one sidebar entry per plugin; the README stays short, //! besides a generated tree of every skill and agent. @@ -147,6 +152,132 @@ pub fn activity(name: &str) -> bool { && words[0].ends_with("ing") } +/// R3: whether a skill name is a command: one or two hyphen-joined lowercase words, the first a verb +/// that does not end in `ing` (which would make it an activity). +#[must_use] +pub fn command_name(name: &str) -> bool { + let words: Vec<&str> = name.split('-').collect(); + (1..=2).contains(&words.len()) + && words + .iter() + .all(|word| !word.is_empty() && word.bytes().all(|b| b.is_ascii_lowercase())) + && !words[0].ends_with("ing") +} + +/// The most lines a command's body may have (R3): past that it is carrying behaviour, which +/// belongs in the activity skill it hands off to. +const COMMAND_LINES: usize = 20; + +/// The body of a markdown file after its frontmatter, without leading or trailing blank lines. +fn body(text: &str) -> &str { + let rest = text + .strip_prefix("---\n") + .and_then(|rest| rest.find("\n---").map(|end| &rest[end + 4..])) + .unwrap_or(text); + rest.trim() +} + +/// Whether the frontmatter sets `key: true`. +fn frontmatter_true(text: &str, key: &str) -> bool { + let Some(block) = text + .strip_prefix("---\n") + .and_then(|rest| rest.find("\n---").map(|end| &rest[..end])) + else { + return false; + }; + block.lines().any(|line| { + line.strip_prefix(key) + .and_then(|rest| rest.strip_prefix(':')) + .is_some_and(|value| value.trim() == "true") + }) +} + +/// R3 over one command skill of `plugin`. `skills` are the plugin's skill folders and `activities` +/// the subset that are activities; a command hands off to exactly one of those. +/// +/// A command is started by the operator only (`disable-model-invocation: true`), so a model never +/// picks it in place of the activity; it is short, so the behaviour stays in one place; and it +/// names the activity it hands off to, so a rename of that activity breaks the gate, not the +/// command. +#[must_use] +pub fn command( + plugin: &str, + folder: &str, + text: &str, + skills: &BTreeSet, + activities: &BTreeSet, +) -> Vec { + let mut problems = Vec::new(); + if folder == plugin { + problems.push(format!("R3 `{plugin}:{folder}` is named after its plugin")); + return problems; + } + if !command_name(folder) { + problems.push(format!( + "R3 `{plugin}:{folder}` is neither an activity (one or two words, the first ending in `ing`) nor a command (one or two words, the first a verb)" + )); + return problems; + } + if !frontmatter_true(text, "disable-model-invocation") { + problems.push(format!( + "R3 command `{plugin}:{folder}` does not set `disable-model-invocation: true`; only the operator starts a command" + )); + } + let body = body(text); + let lines = body.lines().count(); + if lines > COMMAND_LINES { + problems.push(format!( + "R3 command `{plugin}:{folder}` has {lines} lines of body; the most is {COMMAND_LINES}, and the procedure belongs in the activity it hands off to" + )); + } + let own: BTreeSet = [plugin.to_owned()].into(); + let mut handoffs = BTreeSet::new(); + for (_, name) in body.lines().flat_map(|line| ids(line, &own)) { + if !skills.contains(&name) { + problems.push(format!( + "R3 command `{plugin}:{folder}` names `{plugin}:{name}`, which is no skill of `{plugin}`" + )); + } else if activities.contains(&name) { + handoffs.insert(name); + } + } + match handoffs.len() { + 1 => {} + 0 => problems.push(format!( + "R3 command `{plugin}:{folder}` hands off to no activity skill of `{plugin}`; name the one it hands off to as `{plugin}:`" + )), + n => problems.push(format!( + "R3 command `{plugin}:{folder}` names {n} activity skills ({}); a command hands off to exactly one", + handoffs.into_iter().collect::>().join(", ") + )), + } + problems +} + +/// R3: whether a skill's `agents/openai.yaml` makes it operator-only in Codex, which reads +/// `policy.allow_implicit_invocation` rather than Claude Code's `disable-model-invocation`. +#[must_use] +pub fn codex_operator_only(yaml: Option<&str>) -> bool { + yaml.and_then(|text| serde_yaml::from_str::(text).ok()) + .and_then(|document| { + document + .get("policy")? + .get("allow_implicit_invocation")? + .as_bool() + }) + == Some(false) +} + +/// R5: the `**Skill version X**` line a skill may carry, with its 1-based line number. +#[must_use] +pub fn skill_version(text: &str) -> Option<(usize, String)> { + text.lines().enumerate().find_map(|(number, line)| { + let rest = line.split_once("**Skill version ")?.1; + let (version, _) = rest.split_once("**")?; + Some((number + 1, version.trim().to_owned())) + }) +} + /// The agent names a skill lists under its `## Agents` heading, one `` - `name` `` bullet each. #[must_use] pub fn listed_agents(skill: &str) -> Vec { @@ -176,7 +307,13 @@ fn plugin( ) -> (BTreeSet, BTreeSet) { let directory = root.join("plugins").join(name); let mut skills = BTreeSet::new(); + let mut activities = BTreeSet::new(); + let mut commands = Vec::new(); let mut owners: BTreeMap> = BTreeMap::new(); + let manifest = read(&directory.join(".claude-plugin/plugin.json")) + .ok() + .and_then(|text| serde_json::from_str::(&text).ok()) + .and_then(|document| document.get("version")?.as_str().map(str::to_owned)); for path in entries(&directory.join("skills")) { let Some(folder) = path.file_name().and_then(|n| n.to_str()).map(str::to_owned) else { continue; @@ -185,24 +322,47 @@ fn plugin( problems.push(format!("R3 `{name}:{folder}` has no SKILL.md")); continue; }; - if !LIFECYCLE.contains(&folder.as_str()) && !activity(&folder) { - problems.push(format!( - "R3 `{name}:{folder}` is not an activity name (one or two words, the first ending in `ing`)" - )); - } - if !LIFECYCLE.contains(&folder.as_str()) && folder == name { - problems.push(format!("R3 `{name}:{folder}` is named after its plugin")); + if activity(&folder) { + if folder == name { + problems.push(format!("R3 `{name}:{folder}` is named after its plugin")); + } + activities.insert(folder.clone()); + } else if !LIFECYCLE.contains(&folder.as_str()) { + commands.push((folder.clone(), text.clone())); } if frontmatter_name(&text).as_deref() != Some(folder.as_str()) { problems.push(format!( "R3 `{name}:{folder}` declares another `name:` in its frontmatter" )); } + if let Some((line, version)) = skill_version(&text) { + if manifest.as_deref() != Some(version.as_str()) { + problems.push(format!( + "R5 plugins/{name}/skills/{folder}/SKILL.md:{line} says skill version {version}; `.claude-plugin/plugin.json` carries {}", + manifest.as_deref().unwrap_or("no version") + )); + } + } for agent in listed_agents(&text) { owners.entry(agent).or_default().push(folder.clone()); } skills.insert(folder); } + for (folder, text) in &commands { + problems.extend(command(name, folder, text, &skills, &activities)); + let yaml = read( + &directory + .join("skills") + .join(folder) + .join("agents/openai.yaml"), + ) + .ok(); + if command_name(folder) && folder != name && !codex_operator_only(yaml.as_deref()) { + problems.push(format!( + "R3 command `{name}:{folder}` has no `agents/openai.yaml` with `policy.allow_implicit_invocation: false`; Codex would let the model start it" + )); + } + } for lifecycle in LIFECYCLE { if !skills.contains(*lifecycle) { problems.push(format!( @@ -539,6 +699,7 @@ pub fn check(root: &Path, plugins: &Plugins) -> Result<(), String> { #[cfg(test)] mod tests { use super::*; + use std::fmt::Write as _; #[test] fn activity_names() { @@ -566,6 +727,139 @@ mod tests { } } + fn set(names: &[&str]) -> BTreeSet { + names.iter().map(|s| (*s).to_owned()).collect() + } + + fn command_text(flag: bool, body_lines: usize, handoff: &str) -> String { + let mut text = String::from("---\nname: cleanup\ndescription: Clean up.\n"); + if flag { + text.push_str("disable-model-invocation: true\n"); + } + text.push_str("argument-hint: \"[--repo ]\"\n---\n\n"); + writeln!(text, "Read `{handoff}` and follow it.").unwrap(); + for n in 1..body_lines { + writeln!(text, "- step {n}").unwrap(); + } + text + } + + fn worktree_command(folder: &str, text: &str) -> Vec { + let skills = set(&["init", "upgrade", "managing-worktrees", folder]); + let activities = set(&["managing-worktrees"]); + command("worktree", folder, text, &skills, &activities) + } + + #[test] + fn command_names_are_verbs_not_activities() { + for good in ["cleanup", "wave", "drive", "review-plan", "decompose"] { + assert!(command_name(good), "{good}"); + } + for bad in [ + "planning", + "managing-worktrees", + "Wave", + "", + "a-b-c", + "wave-", + ] { + assert!(!command_name(bad), "{bad}"); + } + } + + #[test] + fn a_valid_command_is_accepted() { + let text = command_text(true, 20, "worktree:managing-worktrees"); + assert_eq!(worktree_command("cleanup", &text), Vec::::new()); + } + + #[test] + fn a_command_without_disable_model_invocation_is_refused() { + let text = command_text(false, 5, "worktree:managing-worktrees"); + let problems = worktree_command("cleanup", &text); + assert!( + problems.len() == 1 && problems[0].contains("disable-model-invocation: true"), + "{problems:?}" + ); + } + + #[test] + fn a_command_over_twenty_lines_is_refused() { + let text = command_text(true, 21, "worktree:managing-worktrees"); + let problems = worktree_command("cleanup", &text); + assert!( + problems.len() == 1 && problems[0].contains("21 lines of body"), + "{problems:?}" + ); + } + + #[test] + fn a_command_naming_a_missing_skill_is_refused() { + let text = command_text(true, 5, "worktree:managing-trees"); + let problems = worktree_command("cleanup", &text); + assert!( + problems + .iter() + .any(|p| p.contains("`worktree:managing-trees`, which is no skill")), + "{problems:?}" + ); + assert!( + problems + .iter() + .any(|p| p.contains("hands off to no activity")), + "{problems:?}" + ); + } + + #[test] + fn a_command_handing_off_to_a_lifecycle_skill_or_two_activities_is_refused() { + let text = command_text(true, 5, "worktree:init"); + let problems = worktree_command("cleanup", &text); + assert!( + problems.len() == 1 && problems[0].contains("hands off to no activity"), + "{problems:?}" + ); + let skills = set(&["init", "upgrade", "planning", "implementing", "wave"]); + let activities = set(&["planning", "implementing"]); + let text = command_text(true, 5, "aep:implementing` after `aep:planning"); + let problems = command("aep", "wave", &text, &skills, &activities); + assert!( + problems.len() == 1 && problems[0].contains("2 activity skills"), + "{problems:?}" + ); + } + + #[test] + fn a_command_named_after_its_plugin_is_refused() { + let text = command_text(true, 5, "worktree:managing-worktrees") + .replace("name: cleanup", "name: worktree"); + let problems = worktree_command("worktree", &text); + assert!( + problems.len() == 1 && problems[0].contains("named after its plugin"), + "{problems:?}" + ); + } + + #[test] + fn a_command_is_operator_only_in_codex_too() { + let off = + "interface:\n display_name: \"X\"\npolicy:\n allow_implicit_invocation: false\n"; + assert!(codex_operator_only(Some(off))); + let on = off.replace("false", "true"); + assert!(!codex_operator_only(Some(&on))); + assert!(!codex_operator_only(Some( + "interface:\n display_name: \"X\"\n" + ))); + assert!(!codex_operator_only(None)); + } + + #[test] + fn a_skill_version_line_is_read_with_its_line_number() { + let text = "---\nname: x\n---\n\n**Skill version 0.14.15** — the version\n"; + assert_eq!(skill_version(text), Some((5, "0.14.15".to_owned()))); + assert_eq!(skill_version("# no version line\n"), None); + } + #[test] fn cli_versions_are_found_and_skill_versions_are_not() { assert_eq!(quoted_versions("at AEP 0.55.0 the verb"), ["aep 0.55.0"]); diff --git a/crates/agentplugins-check/src/main.rs b/crates/agentplugins-check/src/main.rs index 8e6d478..4734dc3 100644 --- a/crates/agentplugins-check/src/main.rs +++ b/crates/agentplugins-check/src/main.rs @@ -403,16 +403,6 @@ const RETIRED: &[Retired] = &[ new: "b10x:init", wire_next: &[], }, - Retired { - old: "aep:wave", - new: "aep:implementing", - wire_next: &[], - }, - Retired { - old: "aep:drive", - new: "aep:implementing", - wire_next: &[], - }, Retired { old: "aep:story-migration", new: "aep:migrating", diff --git a/plugins/aep/.claude-plugin/plugin.json b/plugins/aep/.claude-plugin/plugin.json index 54b1cd1..5592823 100644 --- a/plugins/aep/.claude-plugin/plugin.json +++ b/plugins/aep/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "aep", "displayName": "AEP", "description": "Plan governed work in the AEP artifact store and deliver it in reviewed waves: decomposition, plan critique, reverse engineering, story scoping, implementation and adversarial review.", - "version": "0.14.15", + "version": "0.14.16", "author": { "name": "Beyond10x" }, diff --git a/plugins/aep/.codex-plugin/plugin.json b/plugins/aep/.codex-plugin/plugin.json index f8134a6..0fb8be7 100644 --- a/plugins/aep/.codex-plugin/plugin.json +++ b/plugins/aep/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "aep", - "version": "0.14.15", + "version": "0.14.16", "description": "Plan governed work in the AEP artifact store and deliver it in reviewed waves.", "author": { "name": "Beyond10x" diff --git a/plugins/aep/skills/drive/SKILL.md b/plugins/aep/skills/drive/SKILL.md new file mode 100644 index 0000000..02dd847 --- /dev/null +++ b/plugins/aep/skills/drive/SKILL.md @@ -0,0 +1,16 @@ +--- +name: drive +description: Start one governed AEP run over a single story, started by the operator as /aep:drive . Hands off to aep:implementing in drive mode. +disable-model-invocation: true +argument-hint: "" +--- + +# Drive one story + +Load `aep:implementing` and run it in **drive** mode: read its +[references/drive.md](../implementing/references/drive.md) in full before acting. + +- The story is the one id in `$ARGUMENTS`. With none or several, ask for exactly one and stop. +- Say the reference's one line on what a driven run costs and how it usually ends, before launching. +- Launch one run, print its run id and how to follow it, and stop. +- Move no artifact, and relay every refusal from `aep`, `metaharness` or the driver unedited. diff --git a/plugins/aep/skills/drive/agents/openai.yaml b/plugins/aep/skills/drive/agents/openai.yaml new file mode 100644 index 0000000..bce9900 --- /dev/null +++ b/plugins/aep/skills/drive/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "Drive a Story" + short_description: "Start one governed AEP run over a single story" + default_prompt: "Use $drive to start one governed run over the story I name and print its run id." +policy: + allow_implicit_invocation: false diff --git a/plugins/aep/skills/implementing/SKILL.md b/plugins/aep/skills/implementing/SKILL.md index 0a258c7..0cc2610 100644 --- a/plugins/aep/skills/implementing/SKILL.md +++ b/plugins/aep/skills/implementing/SKILL.md @@ -3,7 +3,7 @@ name: implementing description: Implement accepted AEP work, in one of two modes. A wave picks the stories that can be implemented at once, proposes the wave for approval, dispatches one implementor per story into its own worktree, sends each result to the adversary and merges what goes green. A drive hands one story to a governed `metaharness aep drive` run and reports the run id. Use when the operator asks to implement, build or deliver planned stories, to pick or start the next wave, to implement several stories in parallel or fan out across sub-agents, to drive a story or start a governed run, or asks why a wave's rules are instructions and a drive's are enforced. A wave proposes first and stops; a drive starts one run and reports; neither moves an artifact itself. --- -**Skill version 0.14.2** — the version in `.claude-plugin/plugin.json`; a wave's stage-1 proposal quotes it. +**Skill version 0.14.16** — the version in `.claude-plugin/plugin.json`; a wave's stage-1 proposal quotes it. # Implementing accepted work @@ -19,6 +19,9 @@ Pick the mode from the request: "drive", "driven" or "governed run" means **driv building, delivering, a wave or parallel work means **wave**. If the request fits neither, ask one question that names both. Then read that mode's reference in full; this page only chooses. +The operator can also name the mode directly: `/aep:wave [story-id…]` (`aep:wave`) or +`/aep:drive ` (`aep:drive`), two commands that load this skill in that mode. + ## Rules for both modes - Implement only stories the store shows as accepted; neither mode moves an artifact itself. diff --git a/plugins/aep/skills/migrating/SKILL.md b/plugins/aep/skills/migrating/SKILL.md index 3341239..7708876 100644 --- a/plugins/aep/skills/migrating/SKILL.md +++ b/plugins/aep/skills/migrating/SKILL.md @@ -3,7 +3,7 @@ name: migrating description: Migrate a repository's legacy work tracking — story trees, TODO.md, plan and issue documents — into the governed AEP planning store, without deleting or rewriting the sources. Use when the user asks to migrate, import, port or convert an existing backlog into AEP, when a repository is adopting AEP and already has work written down somewhere, or when a store has been adopted beside a legacy backlog nobody retired. Read it before creating the first artifact in a repository that already tracks work in markdown. --- -**Skill version 0.14.2** — the version in `.claude-plugin/plugin.json`. +**Skill version 0.14.16** — the version in `.claude-plugin/plugin.json`. # Migrating legacy tracking into the store diff --git a/plugins/aep/skills/planning/SKILL.md b/plugins/aep/skills/planning/SKILL.md index e909d89..c7bd9dd 100644 --- a/plugins/aep/skills/planning/SKILL.md +++ b/plugins/aep/skills/planning/SKILL.md @@ -3,7 +3,7 @@ name: planning description: Plan engineering work in a governed markdown artifact store — create, relate, move and validate epics, stories, tasks and initiatives through the `aep` CLI. Use when the user mentions planning, a backlog, an epic, a story, a task, decomposing or breaking down work, an artifact's status ("move this to active", "what is still in draft?", "why can't this be implemented?"), or when the project contains a `.engineering/planning/` directory. Use it at adoption too — the user asks to adopt AEP, to migrate from or replace the track plugin, to start a first backlog, or works in a repository with no `.engineering/` directory at all — because § 5 says how a first store is populated and it is worth nothing after one has been hand-written. Also use before editing any file under `.engineering/planning/`. --- -**Skill version 0.14.2** — the version in `.claude-plugin/plugin.json`. +**Skill version 0.14.16** — the version in `.claude-plugin/plugin.json`. # Planning in a governed artifact store diff --git a/plugins/aep/skills/wave/SKILL.md b/plugins/aep/skills/wave/SKILL.md new file mode 100644 index 0000000..f765e8a --- /dev/null +++ b/plugins/aep/skills/wave/SKILL.md @@ -0,0 +1,17 @@ +--- +name: wave +description: Start an AEP implementation wave, started by the operator as /aep:wave. Hands off to aep:implementing in wave mode, which proposes the wave and stops for approval. +disable-model-invocation: true +argument-hint: "[story-id...]" +--- + +# Start a wave + +Load `aep:implementing` and run it in **wave** mode: read its +[references/wave.md](../implementing/references/wave.md) in full before acting. + +- Candidates: the story ids in `$ARGUMENTS`; with none, the stories the store shows as accepted. +- Do stage 1 only: scope the candidates, write the wave page, print the stage-1 proposal with the + skill version line from `aep:implementing`, and stop for the operator's approval. +- Stage 2 starts only on that approval, exactly as the reference describes. +- Move no artifact, and relay every refusal from `aep` or the gate unedited. diff --git a/plugins/aep/skills/wave/agents/openai.yaml b/plugins/aep/skills/wave/agents/openai.yaml new file mode 100644 index 0000000..298d1a3 --- /dev/null +++ b/plugins/aep/skills/wave/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "Start a Wave" + short_description: "Propose an AEP implementation wave and stop for approval" + default_prompt: "Use $wave to scope the accepted stories, propose one implementation wave, and stop for my approval." +policy: + allow_implicit_invocation: false diff --git a/plugins/b10x/.claude-plugin/plugin.json b/plugins/b10x/.claude-plugin/plugin.json index 2ad2727..2366abd 100644 --- a/plugins/b10x/.claude-plugin/plugin.json +++ b/plugins/b10x/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "b10x", "displayName": "Beyond10x", "description": "Set up, upgrade and check the Beyond10x plugins and binaries, route work to them, and create portable plugins.", - "version": "0.14.15", + "version": "0.14.16", "author": { "name": "Beyond10x" }, diff --git a/plugins/b10x/.codex-plugin/plugin.json b/plugins/b10x/.codex-plugin/plugin.json index a57c215..7e8e847 100644 --- a/plugins/b10x/.codex-plugin/plugin.json +++ b/plugins/b10x/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "b10x", - "version": "0.14.15", + "version": "0.14.16", "description": "Set up, upgrade and check the Beyond10x plugins and binaries, route work to them, and create portable plugins.", "author": { "name": "Beyond10x" diff --git a/plugins/b10x/skills/routing/SKILL.md b/plugins/b10x/skills/routing/SKILL.md index eaff26a..07f2410 100644 --- a/plugins/b10x/skills/routing/SKILL.md +++ b/plugins/b10x/skills/routing/SKILL.md @@ -34,6 +34,15 @@ Route the request; do not reproduce a specialist plugin's full workflow. | Create, inspect, finish, or safely clean Git worktrees | `worktree:managing-worktrees` | | Set up providers, inspect Connector readiness, or invoke configured integrations through the CLI | `connectors:integrating` | +Three entry points are commands: only the operator starts them, and a model cannot invoke them. +When a request matches one, route to the activity it hands off to and name the command to the operator. + +| Command | Hands off to | +|---|---| +| `/aep:wave [story-id…]` (`aep:wave`) | `aep:implementing`, wave mode | +| `/aep:drive ` (`aep:drive`) | `aep:implementing`, drive mode | +| `/worktree:cleanup` (`worktree:cleanup`) | `worktree:managing-worktrees` | + ## Preserve boundaries - Do not treat this plugin as a substitute for the routed specialist. diff --git a/plugins/connectors/.claude-plugin/plugin.json b/plugins/connectors/.claude-plugin/plugin.json index 307d83f..0e819d3 100644 --- a/plugins/connectors/.claude-plugin/plugin.json +++ b/plugins/connectors/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "connectors", - "version": "0.14.15", + "version": "0.14.16", "description": "Set up, inspect, and invoke governed integrations through the connectors CLI.", "author": { "name": "Beyond10x" }, "license": "Apache-2.0", diff --git a/plugins/connectors/.codex-plugin/plugin.json b/plugins/connectors/.codex-plugin/plugin.json index 12325ee..8508bac 100644 --- a/plugins/connectors/.codex-plugin/plugin.json +++ b/plugins/connectors/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "connectors", - "version": "0.14.15", + "version": "0.14.16", "description": "Set up, inspect, and invoke governed integrations through the connectors CLI.", "author": { "name": "Beyond10x" }, "license": "Apache-2.0", diff --git a/plugins/ess/.claude-plugin/plugin.json b/plugins/ess/.claude-plugin/plugin.json index 2fa22df..5268bda 100644 --- a/plugins/ess/.claude-plugin/plugin.json +++ b/plugins/ess/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "ess", "displayName": "ESS", "description": "Write, retrofit, validate and project Executable System Specifications, and hold implementations to them with conformance suites.", - "version": "0.14.15", + "version": "0.14.16", "author": { "name": "Beyond10x" }, diff --git a/plugins/ess/.codex-plugin/plugin.json b/plugins/ess/.codex-plugin/plugin.json index 8f277fd..69bf7a1 100644 --- a/plugins/ess/.codex-plugin/plugin.json +++ b/plugins/ess/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ess", - "version": "0.14.15", + "version": "0.14.16", "description": "Write, retrofit, validate and project Executable System Specifications, and hold implementations to them with conformance suites.", "author": { "name": "Beyond10x" diff --git a/plugins/worktree/.claude-plugin/plugin.json b/plugins/worktree/.claude-plugin/plugin.json index f08c10c..98ce7e2 100644 --- a/plugins/worktree/.claude-plugin/plugin.json +++ b/plugins/worktree/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "worktree", "displayName": "Worktree", "description": "Create, lease, finish, audit and safely clean isolated Git worktrees through the worktree CLI.", - "version": "0.14.15", + "version": "0.14.16", "author": { "name": "Beyond10x" }, diff --git a/plugins/worktree/.codex-plugin/plugin.json b/plugins/worktree/.codex-plugin/plugin.json index 371252b..0481e39 100644 --- a/plugins/worktree/.codex-plugin/plugin.json +++ b/plugins/worktree/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "worktree", - "version": "0.14.15", + "version": "0.14.16", "description": "Create, lease, finish, audit and safely clean isolated Git worktrees through the worktree CLI.", "author": { "name": "Beyond10x" diff --git a/plugins/worktree/skills/cleanup/SKILL.md b/plugins/worktree/skills/cleanup/SKILL.md new file mode 100644 index 0000000..8f00a19 --- /dev/null +++ b/plugins/worktree/skills/cleanup/SKILL.md @@ -0,0 +1,21 @@ +--- +name: cleanup +description: Review and safely clean up managed Git worktrees, started by the operator as /worktree:cleanup. Hands off to worktree:managing-worktrees for the procedure. +disable-model-invocation: true +argument-hint: "[--repo ] [--id ...]" +--- + +# Clean up worktrees + +Read `worktree:managing-worktrees` in full, then follow its *Finish and clean up* and *Audit and +recovery* sections for the trees named in `$ARGUMENTS`, or for the current repository if none are. + +1. `worktree inspect --repo ` (add `--id ` per tree) and read every retention blocker. +2. For a tree whose work must not be published, `worktree archive ` instead of publishing. +3. Release only your own lease, then `worktree finish ` for each tree that is done. +4. `worktree gc --repo --dry-run --id ` for each tree, and show the results as a table. +5. `worktree gc --repo --apply --id ` only for the ids the operator approved + from that table. + +Never force a removal, delete a tree by hand, or clear another session's lease. End with every tree +that was kept and the reason the dry-run or inspect gave for keeping it. diff --git a/plugins/worktree/skills/cleanup/agents/openai.yaml b/plugins/worktree/skills/cleanup/agents/openai.yaml new file mode 100644 index 0000000..aed5657 --- /dev/null +++ b/plugins/worktree/skills/cleanup/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "Clean Up Worktrees" + short_description: "Review and safely remove finished managed worktrees" + default_prompt: "Use $cleanup to review this repository's managed worktrees, finish and garbage-collect only the ones I approve, and report what was kept and why." +policy: + allow_implicit_invocation: false diff --git a/verified.json b/verified.json index 449b122..4ca8b18 100644 --- a/verified.json +++ b/verified.json @@ -1,5 +1,5 @@ { "aep": "0.60.0", "ess": "0.35.0", - "worktree": "0.8.0" + "worktree": "0.8.1" } diff --git a/website/docs/plugins/aep.md b/website/docs/plugins/aep.md index 3514184..c979085 100644 --- a/website/docs/plugins/aep.md +++ b/website/docs/plugins/aep.md @@ -47,6 +47,14 @@ It provides: - an adversary role that checks the result against scope, evidence, and repository invariants; - the `implementing` skill, in drive mode: one governed `metaharness aep drive` run over a single story. +Two commands start either mode by hand. Only you start them, never the model, and each hands off +to `aep:implementing`: + +| command | what it does | +|---|---| +| `/aep:wave [story-id…]` | scopes the candidates, writes the wave page, proposes the wave and stops for your approval | +| `/aep:drive ` | says what a driven run costs, starts one governed run, prints its run id and stops | + This plugin builds on AEP's planning substrate. It does not replace the repository gate, invent lifecycle moves, or give implementors authority beyond their assigned unit. diff --git a/website/docs/plugins/worktree.md b/website/docs/plugins/worktree.md index 92521ed..a84c5fe 100644 --- a/website/docs/plugins/worktree.md +++ b/website/docs/plugins/worktree.md @@ -12,6 +12,7 @@ or audits old linked worktrees. |---|---| | `worktree:init` | install the `worktree` CLI, activate a workspace, check it | | `worktree:managing-worktrees` | create, lease, finish, inspect and clean worktrees | +| `worktree:cleanup` | command: `/worktree:cleanup` reviews the managed trees, finishes and garbage-collects only the ids you approve, and reports what it kept and why | | `worktree:upgrade` | check the plugin and CLI, offer the upgrade | ```text @@ -34,6 +35,11 @@ session leases, and publish wanted commits before finishing. Cleanup is review-b offline, unmanaged, and out-of-policy trees are retained. Work merged as rebased or cherry-picked copies is recoverable when an advertised ref carries every unique commit's exact patch. +`/worktree:cleanup [--repo ] [--id …]` starts that review by hand. It is a command: +only you start it, never the model, and it hands off to `worktree:managing-worktrees` for every +step — `inspect`, `archive` for work that must not be published, `finish`, `gc --dry-run --id`, +then `gc --apply --id` for the ids you approved. It never forces a removal. + `worktree inspect --repo ` reports actual Git state, storage, ignored files, leases and retention blockers. It defaults to one repository; use `--workspace` to inspect the wider profile. Add `--refresh` for current remote recovery evidence. Inspection does not infer story completion diff --git a/website/docs/structure.md b/website/docs/structure.md index 40f061e..41a2725 100644 --- a/website/docs/structure.md +++ b/website/docs/structure.md @@ -12,22 +12,22 @@ tools` checks the skills against the newest CLI releases. A change that breaks a |---|---| | **R1 marketplace** | One marketplace, `b10x`, in both the Claude Code and Codex formats. Every plugin lives in this repository. | | **R2 plugin** | One plugin per product. The plugin, the product and the CLI it drives share one name: `aep`, `ess`, `worktree`, `connectors`. The front door is `b10x`, with the `b10x` CLI. | -| **R3 skill** | Every plugin has two lifecycle skills: `init` (set it up and take the first step) and `upgrade` (check it and offer the upgrade). Every other skill is an activity, named in `-ing` form, one or two words: `aep:planning`. | +| **R3 skill** | Every plugin has two lifecycle skills: `init` (set it up and take the first step) and `upgrade` (check it and offer the upgrade). Every other skill is an activity or a command. An activity is named in `-ing` form, one or two words: `aep:planning`. A command is an entry point only the operator starts: named with a verb, one or two words (`worktree:cleanup`); it sets `disable-model-invocation: true`, has at most 20 lines of body, and names the one activity skill of its plugin it hands off to; its `agents/openai.yaml` sets `policy.allow_implicit_invocation: false`, the Codex form of the same flag. No skill is named after its plugin. | | **R4 agent** | An agent is a role: `implementor`, `author`. Exactly one skill of the same plugin owns it and lists it under `## Agents`. | -| **R5 content** | A skill describes its CLI's newest release and quotes no CLI version. `agentplugins-check tools` runs every spelled command against that release. | +| **R5 content** | A skill describes its CLI's newest release and quotes no CLI version. `agentplugins-check tools` runs every spelled command against that release. A `**Skill version X**` line names the version in its plugin's `.claude-plugin/plugin.json`. | | **R6 distribution** | `SETUP.md` and `b10x` install everything; CLIs come prebuilt or from `cargo`. Retired names live only in `catalog.json`, and setup migrates them. | | **R7 references** | Every `:` written in this repository names a file that exists. | | **R8 docs** | One README row, one page under `plugins/` and one sidebar entry per plugin. The README is one paragraph, that table, and the generated tree of every skill and agent, each linked to its file. | ## The plugins -| plugin | lifecycle | activities | agents | -|---|---|---|---| -| `b10x` | `init` (guided onboarding), `upgrade` | `routing`, `authoring-plugins` | — | -| `aep` | `init`, `upgrade` | `planning`, `migrating`, `implementing` (wave or drive mode) | `planning`: decomposer, four plan critics, plan reviewer, reverse engineer · `implementing`: story scoper, implementor, adversary, security reviewer | -| `ess` | `init`, `upgrade` | `specifying`, `retrofitting`, `testing-conformance`, `hardening` | `specifying`: author · `retrofitting`: retrofitter · `testing-conformance`: conformance | -| `worktree` | `init`, `upgrade` | `managing-worktrees` | — | -| `connectors` | `init`, `upgrade` | `integrating` | — | +| plugin | lifecycle | activities | commands | agents | +|---|---|---|---|---| +| `b10x` | `init` (guided onboarding), `upgrade` | `routing`, `authoring-plugins` | — | — | +| `aep` | `init`, `upgrade` | `planning`, `migrating`, `implementing` (wave or drive mode) | `wave`, `drive` (both hand off to `implementing`) | `planning`: decomposer, four plan critics, plan reviewer, reverse engineer · `implementing`: story scoper, implementor, adversary, security reviewer | +| `ess` | `init`, `upgrade` | `specifying`, `retrofitting`, `testing-conformance`, `hardening` | — | `specifying`: author · `retrofitting`: retrofitter · `testing-conformance`: conformance | +| `worktree` | `init`, `upgrade` | `managing-worktrees` | `cleanup` (hands off to `managing-worktrees`) | — | +| `connectors` | `init`, `upgrade` | `integrating` | — | — | Every plugin carries this repository's version. The CLIs have their own versions; `b10x` installs their newest release and `b10x check` says at session start when one is behind.