@jaybeeuu/agent-cortex 2.5.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - e87b2d0: Align the code, verify, document, and fix REPORT templates on the authoritative
8
+ `run-pipeline-stage` contract. Each template now emits `STAGE_COMPLETED` with its stage name
9
+ (`code`, `verify`, `document`, `fix`) plus `COMMIT`, `PUSHED`, `PR`, and
10
+ `OUTCOME: SUCCESS|BLOCKED`, and lists blocked work under `BLOCKING_ISSUES`. The unparsed
11
+ `VERIFY_OUTCOME`/`VERIFY_FAILURES` keys are gone, so an orchestrator can route every stage's
12
+ report.
13
+ - 83ed526: Stop `agent-cortex install claude` erasing Claude Code's `enabledPlugins` entry.
14
+
15
+ The settings merge now asserts only the `enabledPlugins` and `extraKnownMarketplaces` entries the
16
+ committed `claude/settings.json` template declares; entries it does not declare survive, so a
17
+ same-version re-run keeps the entry Claude Code writes when the plugin is enabled. Registration
18
+ also reports an installed-but-not-enabled plugin, naming `claude plugin enable <plugin>@<marketplace>`.
19
+
20
+ - 34bcea7: Type-check the extensions against the pi version that is actually installed.
21
+
22
+ The extension workspaces pinned `@earendil-works/pi-coding-agent`, `pi-agent-core`, and `pi-ai` at
23
+ `^0.78.1` for `import type` only, while `pi --version` reported 1.5.2. `pnpm typecheck` therefore
24
+ validated against an API that no installed pi provides. The pins now follow the running CLI at
25
+ `^1.5.2`, and the `agent-modes` `ExtensionAPI` test double gains the members 1.5.x declares:
26
+ `registerMarkdownTransformer`, `registerEntryRenderer`, `registerToolRenderer`, `registerVirtualModel`,
27
+ `registerMcpServer`, `unregisterMcpServer`, `getMcpServers`, `unregisterVirtualModel`, and
28
+ `getSettings`, plus the unsubscribe return value of `on`.
29
+
30
+ - fa84942: Move the composable agent-directory validator from
31
+ `skills/productivity/validate-agent-dir/scripts/` to `scripts/validate-agent-dir.mjs`. The
32
+ directory had no `SKILL.md`, so neither installer shipped it while it still looked like a shipped
33
+ skill, and the module validates this repository's own `agents/` layout — repo tooling, not a
34
+ portable skill. Its filesystem probes are now async (`validateAgentDir` returns a promise),
35
+ matching the no-`*Sync` rule for `scripts/`.
36
+
37
+ `agents/README.md` (Validation) and `test/validate-agent-dir.test.mjs` point at the new path. No
38
+ skill, agent, hook, or installer behaviour changes.
39
+
3
40
  ## 2.5.0
4
41
 
5
42
  ### Minor Changes
package/README.md CHANGED
@@ -292,7 +292,8 @@ symlinks), `SessionStart` + `Notification` hooks, and 2 MCP servers — writes a
292
292
  manifest at `~/.agent-cortex/.claude-plugin/marketplace.json` exposing `./claude`, and
293
293
  registers it with Claude Code, and merges the committed `claude/settings.json` template into
294
294
  `~/.claude/settings.json`: the template supplies defaults, personal values win, and the
295
- installer owns exactly `enabledPlugins` + `extraKnownMarketplaces` — every other key
295
+ installer owns exactly the `enabledPlugins` + `extraKnownMarketplaces` entries the template
296
+ declares — entries it does not declare are left alone — while every other key
296
297
  (`permissions`, `hooks`, `env`, `statusLine`, unknown keys) is preserved. It also writes the
297
298
  managed section of `~/.claude/CLAUDE.md` — the reply-shape block the pi `output-style`
298
299
  extension appends, taken from the same source file — and leaves every other line of that
@@ -360,10 +361,15 @@ agent-cortex install claude --require-register # register or fail the inst
360
361
 
361
362
  The install also merges the committed `claude/settings.json` template into
362
363
  `~/.claude/settings.json` (the settings file Claude Code reads at user scope). The template
363
- supplies defaults and personal values win; the installer owns exactly `enabledPlugins` +
364
- `extraKnownMarketplaces`, so `permissions`, `hooks`, `env`, `statusLine` and unknown keys are
365
- never touched. A legacy symlinked `settings.json` is replaced with a real file rather than
366
- written through, and `--output` leaves the user's Claude config alone.
364
+ supplies defaults and personal values win; the installer owns exactly the `enabledPlugins` +
365
+ `extraKnownMarketplaces` entries the template declares, so entries it does not declare (including
366
+ the one Claude Code writes when you enable the plugin) survive, and `permissions`, `hooks`, `env`,
367
+ `statusLine` and unknown keys are never touched. A legacy symlinked `settings.json` is replaced
368
+ with a real file rather than written through, and `--output` leaves the user's Claude config
369
+ alone. When registration finds the plugin installed but the settings do not enable it, the install
370
+ warns and names `claude plugin enable <plugin>@<marketplace>` — a disabled plugin loads nothing.
371
+ That check reads the user-scope file only, so a plugin enabled at project or local scope still
372
+ warns; `claude plugin list` reports the effective state.
367
373
 
368
374
  It also writes the managed section of `~/.claude/CLAUDE.md` (Claude's user memory), delimited
369
375
  by `<!-- BEGIN agent-cortex:output-style -->` / `<!-- END agent-cortex:output-style -->`. The
package/agents/README.md CHANGED
@@ -245,7 +245,9 @@ agents, token-substituted skills, extensions) at `~/.agent-cortex/pi`.
245
245
 
246
246
  ## Validation
247
247
 
248
- Use the `validate-agent-dir` script to validate agent directories:
248
+ The composable layout is validated by the repo tooling at `scripts/validate-agent-dir.mjs` — a
249
+ module, not a shipped skill, with no CLI. Import `validateAgentDir` to check a directory, or run
250
+ its test suite:
249
251
 
250
252
  ```bash
251
253
  node --test test/validate-agent-dir.test.mjs
@@ -120,8 +120,8 @@ if (parsed.command === "install") {
120
120
  // materialises the plugin into the home install root (~/.agent-cortex/claude)
121
121
  // with copied, token-substituted skills, writes the marketplace manifest at
122
122
  // ~/.agent-cortex/.claude-plugin/marketplace.json, merges the committed
123
- // claude/settings.json template into ~/.claude/settings.json (owning only
124
- // enabledPlugins + extraKnownMarketplaces), and registers it with Claude Code
123
+ // claude/settings.json template into ~/.claude/settings.json (asserting only
124
+ // the enabledPlugins + extraKnownMarketplaces entries the template declares), and registers it with Claude Code
125
125
  // (marketplace add → install → update); `--output <dir>` is the generate-only
126
126
  // form (tests/CI validation), and the generated claude/ plugin subtree is
127
127
  // never committed — only the settings template is.
@@ -147,6 +147,9 @@ if (parsed.command === "install") {
147
147
  root: /** @type {string} */ (result.marketplaceRoot),
148
148
  manifest: /** @type {object} */ (result.marketplaceManifest),
149
149
  pluginVersion: result.pluginVersion,
150
+ // The enablement check reads the same file this install just merged:
151
+ // a plugin Claude Code does not enable loads nothing.
152
+ ...(result.settings ? { settingsPath: result.settings.path } : {}),
150
153
  dryRun: parsed.dryRun ?? false,
151
154
  requireRegister: parsed.requireRegister ?? false,
152
155
  });
@@ -85,13 +85,16 @@ const PACKAGE_ROOT = join(MODULE_DIR, "..", "..");
85
85
  */
86
86
 
87
87
  /**
88
- * Registration plan/outcome returned by registerClaude.
88
+ * Register plan/outcome returned by registerClaude.
89
89
  * @typedef {object} RegisterClaudeResult
90
90
  * @property {string} marketplace
91
91
  * @property {string} plugin
92
92
  * @property {string[][]} commands
93
93
  * @property {boolean} dryRun
94
94
  * @property {boolean} registered
95
+ * @property {boolean | null} enabled Whether the user's settings enable the plugin;
96
+ * null when the settings could not be read or
97
+ * its state is unconfirmed.
95
98
  * @property {string[]} skipped
96
99
  */
97
100
 
@@ -400,9 +403,13 @@ async function buildHandAuthored(root) {
400
403
  * - template keys fill in defaults the live file lacks; every value already
401
404
  * present in the live file wins (deeply), and unknown keys are preserved,
402
405
  * so personal config survives re-install;
403
- * - `enabledPlugins` and `extraKnownMarketplaces` are CLI-owned: they are
404
- * rewritten from the template outright. `permissions`, `hooks`, `env`,
405
- * `statusLine` and every other key are never touched.
406
+ * - `enabledPlugins` and `extraKnownMarketplaces` are CLI-owned in the other
407
+ * direction: the entries the template declares are asserted (the template
408
+ * value wins for those), and entries it does not declare are kept. A
409
+ * template key that is empty or absent declares nothing and therefore owns
410
+ * nothing — silence never deletes, so a re-install cannot erase the entry
411
+ * Claude Code writes when the user enables the plugin. `permissions`,
412
+ * `hooks`, `env`, `statusLine` and every other key are never touched.
406
413
  * @param {{ root: string, target: string, dryRun: boolean, warn: Warn }} options
407
414
  * @returns {Promise<ManagedFileResult | null>}
408
415
  */
@@ -410,16 +417,32 @@ async function installSettings({ root, target, dryRun, warn }) {
410
417
  const template = await readTemplateJson(join(root, SETTINGS_TEMPLATE), warn);
411
418
  if (template === null) return null;
412
419
 
413
- const merged = mergeSettings(template, await readJsonObject(target, warn));
420
+ const existing = await readJsonObject(target, warn);
421
+ const merged = mergeSettings(template, existing);
414
422
  for (const key of SETTINGS_OWNED_KEYS) {
415
- if (key in template) merged[key] = template[key];
416
- else delete merged[key];
423
+ const value = mergeOwnedSetting(template[key], existing[key]);
424
+ if (value === undefined) delete merged[key];
425
+ else merged[key] = value;
417
426
  }
418
427
 
419
428
  const content = `${JSON.stringify(merged, null, 2)}\n`;
420
429
  return writeManagedFile({ target, content, dryRun });
421
430
  }
422
431
 
432
+ /**
433
+ * Merge one CLI-owned settings key. The template asserts the entries it
434
+ * declares — its value wins for exactly those — and every entry it does not
435
+ * declare survives from the live file. An empty or absent template key declares
436
+ * nothing, so the live value passes through untouched: silence never deletes.
437
+ * @param {unknown} template
438
+ * @param {unknown} existing
439
+ * @returns {unknown}
440
+ */
441
+ function mergeOwnedSetting(template, existing) {
442
+ if (!isPlainObject(template) || !isPlainObject(existing)) return existing === undefined ? template : existing;
443
+ return { ...existing, ...template };
444
+ }
445
+
423
446
  /**
424
447
  * Merge the committed template over the live file. Template keys fill in
425
448
  * defaults, live values win (deeply), and keys the template does not know about
@@ -1045,6 +1068,11 @@ async function pluginLoadVerdict(claudeBin, pluginId) {
1045
1068
  * When that list cannot be read, or does not list the plugin, the load state is
1046
1069
  * unconfirmed: the install warns and skips the success line.
1047
1070
  *
1071
+ * A loaded plugin that the user's settings do not enable loads nothing either:
1072
+ * Claude Code reads `enabledPlugins` from that file, and registration skips the
1073
+ * install as "already installed", so the run warns with the command that fixes
1074
+ * it rather than reporting a working install.
1075
+ *
1048
1076
  * @param {object} options
1049
1077
  * @param {string} [options.root] Marketplace root carrying .claude-plugin/…
1050
1078
  * (defaults to the repo containing this module)
@@ -1052,6 +1080,8 @@ async function pluginLoadVerdict(claudeBin, pluginId) {
1052
1080
  * @param {boolean} [options.dryRun] Plan and report without running or writing
1053
1081
  * @param {object} [options.manifest] Pre-parsed marketplace manifest (skips the file read)
1054
1082
  * @param {string} [options.pluginVersion] Materialised plugin version (default: read from plugin.json)
1083
+ * @param {string} [options.settingsPath] Claude user settings the enablement check reads
1084
+ * (default ~/.claude/settings.json)
1055
1085
  * @param {boolean} [options.requireRegister] Exit non-zero when registration cannot run
1056
1086
  * @param {(msg: string) => void} [options.warn] Warning sink (default: console.warn)
1057
1087
  * @returns {Promise<RegisterClaudeResult>}
@@ -1062,6 +1092,7 @@ export async function registerClaude({
1062
1092
  dryRun = false,
1063
1093
  manifest,
1064
1094
  pluginVersion,
1095
+ settingsPath = DEFAULT_SETTINGS_PATH,
1065
1096
  requireRegister = false,
1066
1097
  warn = DEFAULT_WARN,
1067
1098
  } = {}) {
@@ -1072,7 +1103,7 @@ export async function registerClaude({
1072
1103
  const version = pluginVersion ?? (await readMaterialisedPluginVersion(root, manifestObj));
1073
1104
  const home = resolve(root);
1074
1105
 
1075
- const summary = /** @type {RegisterClaudeResult} */ ({ marketplace, plugin, commands: [], dryRun, registered: true, skipped: [] });
1106
+ const summary = /** @type {RegisterClaudeResult} */ ({ marketplace, plugin, commands: [], dryRun, registered: true, enabled: null, skipped: [] });
1076
1107
  console.log(`Registering plugin "${pluginId}" with Claude Code (via ${claudeBin})…`);
1077
1108
 
1078
1109
  const manualCommands = [
@@ -1156,9 +1187,22 @@ export async function registerClaude({
1156
1187
  `verify with "${claudeBin} plugin list" (restart Claude Code to apply)`,
1157
1188
  );
1158
1189
  } else {
1159
- console.log(
1160
- `Installed — verify with "claude plugin list" / "claude plugin details ${plugin}" (restart Claude Code to apply).`,
1161
- );
1190
+ // Installed but disabled is a plugin that loads nothing: Claude Code gates
1191
+ // every skill and agent on the user's enabledPlugins entry, and the install
1192
+ // path above skips a same-version re-install as "already installed", so
1193
+ // this is the only place that state is reported.
1194
+ const enabled = (await readJsonObject(settingsPath, warn)).enabledPlugins?.[pluginId] === true;
1195
+ summary.enabled = enabled;
1196
+ if (enabled) {
1197
+ console.log(
1198
+ `Installed and enabled — verify with "claude plugin list" / "claude plugin details ${plugin}" (restart Claude Code to apply).`,
1199
+ );
1200
+ } else {
1201
+ warn(
1202
+ `"${pluginId}" is installed but not enabled in ${settingsPath} — Claude Code loads nothing from a disabled plugin; ` +
1203
+ `enable it with "${claudeBin} plugin enable ${pluginId}" and restart Claude Code`,
1204
+ );
1205
+ }
1162
1206
  }
1163
1207
  return summary;
1164
1208
  }
@@ -8,7 +8,7 @@
8
8
  "typecheck": "tsc --noEmit"
9
9
  },
10
10
  "devDependencies": {
11
- "@earendil-works/pi-coding-agent": "^0.78.1",
11
+ "@earendil-works/pi-coding-agent": "^1.5.2",
12
12
  "@types/node": "^22.0.0",
13
13
  "tsx": "^4.0.0",
14
14
  "typescript": "^5.0.0"
@@ -64,6 +64,8 @@ function createFakePi(flagValues: Record<string, boolean | string> = {}): FakePi
64
64
  const list = handlers.get(event) ?? [];
65
65
  list.push(handler);
66
66
  handlers.set(event, list);
67
+ // `ExtensionAPI.on` returns an unsubscribe function.
68
+ return () => {};
67
69
  },
68
70
  registerFlag: (name, options) => {
69
71
  flags.push([name, options]);
@@ -77,8 +79,16 @@ function createFakePi(flagValues: Record<string, boolean | string> = {}): FakePi
77
79
  registerShortcut: () => {},
78
80
  registerTool: () => {},
79
81
  registerMessageRenderer: () => {},
82
+ registerMarkdownTransformer: () => {},
83
+ registerEntryRenderer: () => {},
84
+ registerToolRenderer: () => {},
80
85
  registerProvider: () => {},
81
86
  unregisterProvider: () => {},
87
+ registerMcpServer: () => {},
88
+ unregisterMcpServer: () => {},
89
+ unregisterVirtualModel: () => {},
90
+ registerVirtualModel: () => {},
91
+ getMcpServers: () => [],
82
92
  sendMessage: () => {},
83
93
  sendUserMessage: () => {},
84
94
  appendEntry: () => {},
@@ -89,6 +99,9 @@ function createFakePi(flagValues: Record<string, boolean | string> = {}): FakePi
89
99
  throw new Error("not implemented");
90
100
  },
91
101
  getAllTools: () => [],
102
+ getSettings: () => {
103
+ throw new Error("not implemented");
104
+ },
92
105
  getCommands: () => [],
93
106
  setModel: async () => false,
94
107
  getThinkingLevel: () => "off",
@@ -8,7 +8,7 @@
8
8
  "typecheck": "tsc --noEmit"
9
9
  },
10
10
  "devDependencies": {
11
- "@earendil-works/pi-coding-agent": "^0.78.1",
11
+ "@earendil-works/pi-coding-agent": "^1.5.2",
12
12
  "@types/node": "^22.0.0",
13
13
  "tsx": "^4.0.0",
14
14
  "typescript": "^5.0.0"
@@ -16,12 +16,13 @@
16
16
  * The model call itself is gone. It went through pi-ai's `complete()`, a value
17
17
  * import of pi's runtime, and that import stopped the extension loading outside
18
18
  * the agent-cortex checkout (see test/pi-extensions-load.test.mjs). The
19
- * ExtensionAPI promises no completion call. Newer hosts (pi 1.4.0) expose
20
- * `ModelRegistry.complete`, but the installed host (0.73.1) does not. Calling it
21
- * would need an untyped cast to a method that exists on one version only.
22
- * Extensions also make no internal LLM calls (AGENTS.md), so this feature stays
23
- * off. Both callers fall back to the truncation they already used when the model
24
- * failed. The `model` key is no longer read and is ignored if present.
19
+ * ExtensionAPI promises no completion call. The installed host (pi 1.5.2) does
20
+ * expose `ModelRegistry.complete` through `ExtensionContext.modelRegistry`, but
21
+ * calling it would make the extensions depend on a method that exists on later
22
+ * hosts only. Extensions also make no internal LLM calls (AGENTS.md), so this
23
+ * feature stays off. Both callers fall back to the truncation they already used
24
+ * when the model failed. The `model` key is no longer read and is ignored if
25
+ * present.
25
26
  */
26
27
 
27
28
  import { homedir } from "node:os";
@@ -8,7 +8,7 @@
8
8
  "typecheck": "tsc --noEmit"
9
9
  },
10
10
  "devDependencies": {
11
- "@earendil-works/pi-coding-agent": "^0.78.1",
11
+ "@earendil-works/pi-coding-agent": "^1.5.2",
12
12
  "@types/node": "^22.0.0",
13
13
  "tsx": "^4.0.0",
14
14
  "typescript": "^5.0.0"
@@ -8,7 +8,7 @@
8
8
  "typecheck": "tsc --noEmit"
9
9
  },
10
10
  "devDependencies": {
11
- "@earendil-works/pi-coding-agent": "^0.78.1",
11
+ "@earendil-works/pi-coding-agent": "^1.5.2",
12
12
  "@types/node": "^22.0.0",
13
13
  "tsx": "^4.0.0",
14
14
  "typescript": "^5.0.0"
@@ -8,7 +8,7 @@
8
8
  "typecheck": "tsc --noEmit"
9
9
  },
10
10
  "devDependencies": {
11
- "@earendil-works/pi-coding-agent": "^0.78.1",
11
+ "@earendil-works/pi-coding-agent": "^1.5.2",
12
12
  "@types/node": "^22.0.0",
13
13
  "tsx": "^4.0.0",
14
14
  "typescript": "^5.0.0"
@@ -8,9 +8,9 @@
8
8
  "typecheck": "tsc --noEmit"
9
9
  },
10
10
  "devDependencies": {
11
- "@earendil-works/pi-agent-core": "^0.78.1",
12
- "@earendil-works/pi-ai": "^0.78.1",
13
- "@earendil-works/pi-coding-agent": "^0.78.1",
11
+ "@earendil-works/pi-agent-core": "^1.5.2",
12
+ "@earendil-works/pi-ai": "^1.5.2",
13
+ "@earendil-works/pi-coding-agent": "^1.5.2",
14
14
  "@types/node": "^22.0.0",
15
15
  "tsx": "^4.0.0",
16
16
  "typebox": "^1.1.38",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaybeeuu/agent-cortex",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "description": "Personal PI package with custom agents, skills, and extensions",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -2,12 +2,15 @@
2
2
  // Validator for the composable agent directory layout: `agent.md` plus at
3
3
  // least one harness subdirectory with a valid `frontmatter.json`.
4
4
  //
5
- // Zero dependencies: import the pure functions, or run the test suite at
6
- // `test/validate-agent-dir.test.mjs`.
5
+ // Repo tooling, not a shipped skill: the composable layout exists only in this
6
+ // repository's `agents/` tree, and installs carry flat rendered agents. Run the
7
+ // test suite at `test/validate-agent-dir.test.mjs`.
7
8
 
8
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
9
+ import { readdir, readFile } from "node:fs/promises";
9
10
  import { join } from "node:path";
10
11
 
12
+ import { isDirectory, isFile } from "./lib/fs.mjs";
13
+
11
14
  // ─── Types ────────────────────────────────────────────────────────────────────
12
15
 
13
16
  /**
@@ -96,28 +99,31 @@ export function validateFrontmatter(fm) {
96
99
 
97
100
  /**
98
101
  * @param {string} agentDirPath
99
- * @returns {ValidationResult}
102
+ * @returns {Promise<ValidationResult>}
100
103
  */
101
- export function validateAgentDir(agentDirPath) {
104
+ export async function validateAgentDir(agentDirPath) {
102
105
  /** @type {string[]} */
103
106
  const errors = [];
104
107
 
105
108
  // Check agent.md exists
106
109
  const agentMdPath = join(agentDirPath, "agent.md");
107
- if (!existsSync(agentMdPath)) {
110
+ if (!(await isFile(agentMdPath))) {
108
111
  errors.push("missing agent.md");
109
112
  }
110
113
 
111
114
  // Check for at least one harness subdirectory with frontmatter.json
112
- if (!existsSync(agentDirPath)) {
115
+ if (!(await isDirectory(agentDirPath))) {
113
116
  return { ok: false, errors: ["agent directory does not exist"] };
114
117
  }
115
118
 
116
- const entries = readdirSync(agentDirPath);
117
- const harnessDirs = entries.filter((entry) => {
118
- const fullPath = join(agentDirPath, entry);
119
- return statSync(fullPath).isDirectory() && entry !== "node_modules";
120
- });
119
+ const entries = await readdir(agentDirPath);
120
+ /** @type {string[]} */
121
+ const harnessDirs = [];
122
+ for (const entry of entries) {
123
+ if (entry !== "node_modules" && (await isDirectory(join(agentDirPath, entry)))) {
124
+ harnessDirs.push(entry);
125
+ }
126
+ }
121
127
 
122
128
  if (harnessDirs.length === 0) {
123
129
  errors.push("no harness subdirectories found (need at least one with frontmatter.json)");
@@ -126,12 +132,12 @@ export function validateAgentDir(agentDirPath) {
126
132
  // Validate each harness directory
127
133
  for (const harnessDir of harnessDirs) {
128
134
  const frontmatterPath = join(agentDirPath, harnessDir, "frontmatter.json");
129
- if (!existsSync(frontmatterPath)) {
135
+ if (!(await isFile(frontmatterPath))) {
130
136
  continue; // Skip directories without frontmatter.json
131
137
  }
132
138
 
133
139
  try {
134
- const content = readFileSync(frontmatterPath, "utf-8");
140
+ const content = await readFile(frontmatterPath, "utf-8");
135
141
  const fm = JSON.parse(content);
136
142
  const result = validateFrontmatter(fm);
137
143
  if (!result.ok) {
@@ -88,4 +88,15 @@ The deliverable this feature produces and the problem it solves.
88
88
  | review | Review | verify |
89
89
  | document | Document | review |
90
90
 
91
- `maxFixRounds: 4` — the maximum fix-retry rounds allowed for a stage's verify loop before the PR gate. Adjust stages only in `pipeline.json`; the script and templates follow it.
91
+ `maxFixRounds: 4` — the maximum fix-retry rounds allowed for a stage's verify loop before the PR gate. Adjust stages only in `pipeline.json`; the script and templates follow it.
92
+
93
+ ## Stage templates
94
+
95
+ `templates/` holds one prompt skeleton per stage; a dispatcher fills one to compose that stage's
96
+ chore prompt. The REPORT contract is not defined here —
97
+ `skills/workflow/run-pipeline-stage/prompts/stage-runner.md` owns it and every template mirrors it.
98
+ Drift between the two fails silently: a worker that follows a drifted template emits a well-formed
99
+ block with keys no one reads, so routing stops at that stage. Drift has already broken routing
100
+ twice. Compare a template with the authority whenever either changes. The intended guard is a lint
101
+ parity check beside `scripts/check-skill-frontmatter.mjs`, not a test — a test here would assert on
102
+ file text, which is a content inventory, and `style-tests` bans that shape.
@@ -31,10 +31,21 @@ Implement the task described above using a test-driven approach:
31
31
 
32
32
  Make only the changes required to complete this task — do not refactor unrelated code.
33
33
 
34
- End your response with a ---REPORT--- block:
34
+ Commit and push the stage's work before reporting (see _Deliver before reporting_ in
35
+ `prompts/stage-runner.md`). A dirty worktree counts as a failed stage.
36
+
37
+ End your response with a ---REPORT--- block. The `run-pipeline-stage` skill's
38
+ `prompts/stage-runner.md` holds the authoritative REPORT contract — if this block and that
39
+ prompt differ, follow the prompt and correct this template.
35
40
  ---REPORT---
36
41
  BEAD_ID: <id>
37
- STAGE_COMPLETED: coding
42
+ STAGE_COMPLETED: code
38
43
  SUMMARY: <2–3 sentence summary of what was done>
39
44
  FILES_CHANGED: <comma-separated list, or "none">
45
+ COMMIT: <sha of the stage commit, or "none">
46
+ PUSHED: <yes|no>
47
+ PR: none
48
+ OUTCOME: <SUCCESS|BLOCKED>
49
+ BLOCKING_ISSUES: ← include only if OUTCOME is BLOCKED; one line per issue
50
+ - <what blocks the stage and what would unblock it>
40
51
  ---
@@ -31,10 +31,21 @@ Review the changes made and compare them against the existing documentation in t
31
31
  when no decisions or macro behaviour changed.
32
32
  6. Do not modify source code — documentation only.
33
33
 
34
- End your response with a ---REPORT--- block:
34
+ Commit and push the docs, and open or update the feature PR before reporting (see _Deliver
35
+ before reporting_ in `prompts/stage-runner.md`). A dirty worktree counts as a failed stage.
36
+
37
+ End your response with a ---REPORT--- block. The `run-pipeline-stage` skill's
38
+ `prompts/stage-runner.md` holds the authoritative REPORT contract — if this block and that
39
+ prompt differ, follow the prompt and correct this template.
35
40
  ---REPORT---
36
41
  BEAD_ID: <id>
37
- STAGE_COMPLETED: documenting
42
+ STAGE_COMPLETED: document
38
43
  SUMMARY: <2–3 sentence summary of what was documented, or "No documentation changes required">
39
44
  FILES_CHANGED: <comma-separated list of docs touched, or "none">
45
+ COMMIT: <sha of the stage commit, or "none">
46
+ PUSHED: <yes|no>
47
+ PR: <feature PR url>
48
+ OUTCOME: <SUCCESS|BLOCKED>
49
+ BLOCKING_ISSUES: ← include only if OUTCOME is BLOCKED; one line per issue
50
+ - <what blocks the stage and what would unblock it>
40
51
  ---
@@ -18,10 +18,21 @@ Apply only the changes described in the Task section above. Do not make any othe
18
18
 
19
19
  **Use context7** when working with any third-party library — fetch live docs to ensure you use the correct, up-to-date API for the version in this project.
20
20
 
21
- End your response with a ---REPORT--- block:
21
+ Commit and push the fix before reporting (see _Deliver before reporting_ in
22
+ `prompts/stage-runner.md`). A dirty worktree counts as a failed stage.
23
+
24
+ End your response with a ---REPORT--- block. The `run-pipeline-stage` skill's
25
+ `prompts/stage-runner.md` holds the authoritative REPORT contract — if this block and that
26
+ prompt differ, follow the prompt and correct this template.
22
27
  ---REPORT---
23
28
  BEAD_ID: <id>
24
- STAGE_COMPLETED: fixing
29
+ STAGE_COMPLETED: fix
25
30
  SUMMARY: <2–3 sentence summary of what was changed>
26
31
  FILES_CHANGED: <comma-separated list, or "none">
32
+ COMMIT: <sha of the stage commit, or "none">
33
+ PUSHED: <yes|no>
34
+ PR: none
35
+ OUTCOME: <SUCCESS|BLOCKED>
36
+ BLOCKING_ISSUES: ← include only if OUTCOME is BLOCKED; one line per issue
37
+ - <what remains unresolved and what would unblock it>
27
38
  ---
@@ -21,23 +21,29 @@ Discover and run the project's test and lint commands, then report the outcome.
21
21
  1. `package.json` `scripts` — look for keys named `test`, `lint`, `check`, `typecheck`, or similar.
22
22
  2. `Makefile` — look for targets named `test`, `lint`, `check`.
23
23
  3. Other well-known config files (e.g. `pyproject.toml`, `Cargo.toml`) — look for test/lint targets.
24
- 4. If no commands are found, report PASS with summary "No test or lint commands found".
24
+ 4. If no commands are found, report `OUTCOME: SUCCESS` with summary "No test or lint commands found".
25
25
 
26
26
  **Step 2 — Run each discovered command** in the project root. Capture stdout and stderr.
27
27
 
28
28
  **Step 3 — Evaluate**:
29
- - If all commands exit 0: `VERIFY_OUTCOME: PASS`.
30
- - If any command exits non-zero: `VERIFY_OUTCOME: FAIL`. List each failure concisely.
29
+ - If all commands exit 0: `OUTCOME: SUCCESS`.
30
+ - If any command exits non-zero: `OUTCOME: BLOCKED`. List each failure concisely under
31
+ `BLOCKING_ISSUES`.
31
32
 
32
33
  Do not fix failures — report them only.
33
34
 
34
- End your response with a ---REPORT--- block:
35
+ End your response with a ---REPORT--- block. The `run-pipeline-stage` skill's
36
+ `prompts/stage-runner.md` holds the authoritative REPORT contract — if this block and that
37
+ prompt differ, follow the prompt and correct this template.
35
38
  ---REPORT---
36
39
  BEAD_ID: <id>
37
- STAGE_COMPLETED: verifying
40
+ STAGE_COMPLETED: verify
38
41
  SUMMARY: <what commands were run and overall outcome>
39
42
  FILES_CHANGED: none
40
- VERIFY_OUTCOME: <PASS|FAIL>
41
- VERIFY_FAILURES: ← only if VERIFY_OUTCOME is FAIL
42
- - <test/lint failure summary>
43
+ COMMIT: <sha, or "none" if this stage added no commit>
44
+ PUSHED: <yes|no>
45
+ PR: none
46
+ OUTCOME: <SUCCESS|BLOCKED>
47
+ BLOCKING_ISSUES: ← include only if OUTCOME is BLOCKED; one line per failure
48
+ - <command, its exit status, and the failure summary>
43
49
  ---
@@ -123,6 +123,9 @@ BLOCKING_ISSUES: ← only if OUTCOME is BLOCKED
123
123
  - <specific blocking issue>
124
124
  ```
125
125
 
126
+ Stage-chore templates under `create-task/templates/` mirror this contract. A template that drifts
127
+ still emits a well-formed block with unread keys, so routing fails silently.
128
+
126
129
  Subagents report facts — do not ask them to suggest or predict the next step. A REPORT claiming
127
130
  `OUTCOME: SUCCESS` with `PUSHED: no`, or a `COMMIT` that is not on the branch, records a failed
128
131
  stage: the commit is the evidence, the report is the claim.