mandrel 2.37.0 → 2.39.0

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.
Files changed (162) hide show
  1. package/.agents/README.md +61 -24
  2. package/.agents/agents/plan-critic.md +1 -1
  3. package/.agents/docs/SDLC.md +40 -40
  4. package/.agents/docs/agentrc-reference.json +1 -4
  5. package/.agents/docs/configuration.md +18 -18
  6. package/.agents/docs/execution-reference.md +3 -3
  7. package/.agents/docs/quality-gates.md +2 -2
  8. package/.agents/docs/workflows.md +6 -6
  9. package/.agents/instructions.md +5 -5
  10. package/.agents/rules/changelog-style.md +3 -3
  11. package/.agents/rules/ci-remediation.md +1 -1
  12. package/.agents/rules/git-conventions-reference.md +4 -4
  13. package/.agents/schemas/agentrc.schema.json +15 -16
  14. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  15. package/.agents/schemas/validation-evidence.schema.json +1 -1
  16. package/.agents/scripts/acceptance-eval.js +1 -1
  17. package/.agents/scripts/apply-quality-bootstrap.js +1 -1
  18. package/.agents/scripts/audit-to-stories.js +2 -2
  19. package/.agents/scripts/check-doc-links.js +32 -4
  20. package/.agents/scripts/check-test-temp-hygiene.js +1 -1
  21. package/.agents/scripts/check-workflow-cli-lint.js +1 -1
  22. package/.agents/scripts/deliver-light.js +10 -10
  23. package/.agents/scripts/deliver-recover.js +1 -1
  24. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  25. package/.agents/scripts/evidence-gate.js +1 -1
  26. package/.agents/scripts/generate-skills-index.js +158 -75
  27. package/.agents/scripts/generate-workflows-doc.js +1 -1
  28. package/.agents/scripts/lib/audit-suite/selector.js +1 -1
  29. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +1 -1
  30. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +1 -1
  31. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +5 -5
  32. package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +1 -1
  33. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  34. package/.agents/scripts/lib/bdd-scenario-budget.js +1 -1
  35. package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
  36. package/.agents/scripts/lib/bdd-step-index.js +1 -1
  37. package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
  38. package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
  39. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +1 -1
  40. package/.agents/scripts/lib/changed-files.js +100 -9
  41. package/.agents/scripts/lib/checks/core-bare-clean.js +1 -1
  42. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +1 -1
  43. package/.agents/scripts/lib/cli-args.js +1 -1
  44. package/.agents/scripts/lib/command-header.js +1 -1
  45. package/.agents/scripts/lib/config/delivery-routing.js +1 -1
  46. package/.agents/scripts/lib/config/explain.js +1 -1
  47. package/.agents/scripts/lib/config/runners.js +2 -2
  48. package/.agents/scripts/lib/config/runtime.js +1 -1
  49. package/.agents/scripts/lib/config/temp-paths.js +2 -2
  50. package/.agents/scripts/lib/config-settings-schema-delivery.js +3 -3
  51. package/.agents/scripts/lib/config-settings-schema-quality.js +6 -6
  52. package/.agents/scripts/lib/config-settings-schema.js +28 -10
  53. package/.agents/scripts/lib/duplicate-search.js +1 -1
  54. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +4 -4
  55. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
  56. package/.agents/scripts/lib/findings/classify-finding.js +1 -1
  57. package/.agents/scripts/lib/findings/promote-finding.js +12 -12
  58. package/.agents/scripts/lib/findings/route-finding.js +2 -2
  59. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  60. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  61. package/.agents/scripts/lib/label-constants.js +2 -2
  62. package/.agents/scripts/lib/label-taxonomy.js +1 -1
  63. package/.agents/scripts/lib/onboard/init-tail.js +4 -4
  64. package/.agents/scripts/lib/onboard/scaffold-docs.js +2 -2
  65. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
  66. package/.agents/scripts/lib/orchestration/code-review.js +4 -4
  67. package/.agents/scripts/lib/orchestration/complexity-gate.js +5 -5
  68. package/.agents/scripts/lib/orchestration/deliver-recover.js +3 -3
  69. package/.agents/scripts/lib/orchestration/docs-digest.js +3 -3
  70. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +2 -2
  71. package/.agents/scripts/lib/orchestration/light-backstop.js +1 -1
  72. package/.agents/scripts/lib/orchestration/light-escalation.js +6 -6
  73. package/.agents/scripts/lib/orchestration/light-suitability.js +19 -19
  74. package/.agents/scripts/lib/orchestration/plan-context.js +4 -4
  75. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +2 -2
  76. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +2 -2
  77. package/.agents/scripts/lib/orchestration/plan-metrics.js +1 -1
  78. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +1 -1
  79. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +3 -3
  80. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +6 -6
  81. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -16
  82. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -3
  83. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +6 -6
  84. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +7 -7
  85. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -1
  86. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +3 -3
  87. package/.agents/scripts/lib/orchestration/remote-verifier.js +2 -2
  88. package/.agents/scripts/lib/orchestration/resolve-stories.js +4 -4
  89. package/.agents/scripts/lib/orchestration/run-epilogue.js +2 -2
  90. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +2 -2
  91. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +1 -1
  92. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +5 -1
  93. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +1 -1
  94. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +3 -3
  95. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -2
  96. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  97. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +14 -12
  98. package/.agents/scripts/lib/orchestration/ticket-validator.js +1 -1
  99. package/.agents/scripts/lib/orchestration/ticketing/reads.js +3 -3
  100. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
  101. package/.agents/scripts/lib/qa/resolve-qa-contract.js +58 -6
  102. package/.agents/scripts/lib/signals/schema.js +1 -1
  103. package/.agents/scripts/lib/skills/skills-index.js +168 -0
  104. package/.agents/scripts/lib/skills/walk-skill-files.js +133 -9
  105. package/.agents/scripts/lib/story-adjacency.js +2 -2
  106. package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
  107. package/.agents/scripts/lib/ticket-body-sections.js +1 -1
  108. package/.agents/scripts/lib/util/concurrent-map.js +1 -1
  109. package/.agents/scripts/lib/util/parse-id-list.js +1 -1
  110. package/.agents/scripts/lib/wave-runner/live-probe.js +4 -4
  111. package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
  112. package/.agents/scripts/lib/workflow-closure.js +1 -1
  113. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +2 -2
  114. package/.agents/scripts/lint-issue-body.js +3 -3
  115. package/.agents/scripts/plan-context.js +4 -4
  116. package/.agents/scripts/plan-critics.js +2 -2
  117. package/.agents/scripts/plan-persist.js +2 -2
  118. package/.agents/scripts/plan-run-epilogue.js +2 -2
  119. package/.agents/scripts/providers/github/board-add.js +1 -1
  120. package/.agents/scripts/providers/github/errors.js +1 -1
  121. package/.agents/scripts/providers/github/tickets.js +6 -6
  122. package/.agents/scripts/quality-preview.js +50 -9
  123. package/.agents/scripts/resolve-stories.js +3 -3
  124. package/.agents/scripts/resync-status-column.js +1 -1
  125. package/.agents/scripts/run-lint.js +1 -1
  126. package/.agents/scripts/single-story-close.js +2 -2
  127. package/.agents/scripts/single-story-init.js +5 -5
  128. package/.agents/scripts/stories-wave-tick.js +8 -8
  129. package/.agents/scripts/validate-skills.js +53 -66
  130. package/.agents/skills/core/idea-refinement/SKILL.md +5 -5
  131. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  132. package/.agents/skills/skills.index.json +1 -1
  133. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -1
  134. package/.agents/workflows/audit-data-model.md +1 -1
  135. package/.agents/workflows/audit-documentation.md +2 -2
  136. package/.agents/workflows/audit-to-stories.md +11 -11
  137. package/.agents/workflows/git-deliver.md +4 -4
  138. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  139. package/.agents/workflows/helpers/audit-lens-core.md +1 -1
  140. package/.agents/workflows/helpers/code-review.md +4 -4
  141. package/.agents/workflows/helpers/deliver-light.md +26 -26
  142. package/.agents/workflows/helpers/deliver-reference.md +15 -7
  143. package/.agents/workflows/helpers/deliver-story-reference.md +9 -4
  144. package/.agents/workflows/helpers/deliver-story.md +6 -7
  145. package/.agents/workflows/helpers/diagnose.md +2 -2
  146. package/.agents/workflows/helpers/parallel-tooling.md +1 -1
  147. package/.agents/workflows/helpers/plan-reference.md +9 -9
  148. package/.agents/workflows/helpers/qa-core.md +8 -8
  149. package/.agents/workflows/helpers/worktree-lifecycle.md +6 -6
  150. package/.agents/workflows/{deliver.md → mandrel-deliver.md} +15 -19
  151. package/.agents/workflows/{plan.md → mandrel-plan.md} +9 -9
  152. package/.agents/workflows/memory-consolidate.md +4 -4
  153. package/.agents/workflows/prototype.md +3 -3
  154. package/.agents/workflows/qa-assist.md +12 -12
  155. package/.agents/workflows/qa-explore.md +6 -6
  156. package/.agents/workflows/qa-run.md +14 -6
  157. package/README.md +7 -7
  158. package/docs/CHANGELOG.md +23 -0
  159. package/lib/cli/doctor.js +1 -1
  160. package/lib/cli/init.js +1 -1
  161. package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +1 -1
  162. package/package.json +1 -1
@@ -8,6 +8,15 @@
8
8
  // generator output (ignoring the volatile `generatedAt` field) and exits
9
9
  // non-zero with a diff-style message if they diverge.
10
10
  //
11
+ // Two indexes, never one (Story #5135). The shipped manifest above is a
12
+ // committed payload file that `mandrel doctor` / `mandrel sync-agents`
13
+ // compare byte-for-byte against the installed package, so consumer-authored
14
+ // skills under the `.agents/local/skills/` zone MUST NOT be folded into it —
15
+ // a merged index would read as payload drift in every consumer that authored
16
+ // a skill, and those commands would refuse. Local skills are therefore
17
+ // indexed into their own `.agents/local/skills/skills.index.json`, inside
18
+ // the zone sync never prunes and drift never walks.
19
+ //
11
20
  // CLI surface:
12
21
  //
13
22
  // node generate-skills-index.js [--check] [--root <dir>] [--out <file>]
@@ -38,7 +47,18 @@ import { runAsCli } from './lib/cli-utils.js';
38
47
  import { formatGeneratedJson } from './lib/format-generated-json.js';
39
48
  import { Logger } from './lib/Logger.js';
40
49
  import { parseSkill } from './lib/skills/parse-skill.js';
41
- import { collectSkillFiles } from './lib/skills/walk-skill-files.js';
50
+ import {
51
+ diffManifests,
52
+ INDEX_FILENAME,
53
+ indexPathFor,
54
+ readManifest,
55
+ } from './lib/skills/skills-index.js';
56
+ import {
57
+ collectLocalSkillFiles,
58
+ collectSkillFiles,
59
+ LOCAL_SKILLS_SEGMENTS,
60
+ PAYLOAD_SKILLS_SEGMENTS,
61
+ } from './lib/skills/walk-skill-files.js';
42
62
 
43
63
  const GENERATOR_ID = 'generate-skills-index.js@1';
44
64
 
@@ -103,8 +123,8 @@ function projectEntry(parsed) {
103
123
  * Build the manifest object (without `generatedAt`) by walking the tree
104
124
  * and projecting each parsed SKILL.md into an index entry.
105
125
  */
106
- export function buildManifestBody(repoRoot) {
107
- const skillFiles = collectSkillFiles(repoRoot);
126
+ export function buildManifestBody(repoRoot, collect = collectSkillFiles) {
127
+ const skillFiles = collect(repoRoot);
108
128
  const skills = skillFiles.map((absPath) =>
109
129
  projectEntry(parseSkill(absPath, { repoRoot })),
110
130
  );
@@ -118,8 +138,8 @@ export function buildManifestBody(repoRoot) {
118
138
  * Build the full manifest with `generatedAt`. `nowIso` is injected so
119
139
  * tests can pin the timestamp deterministically.
120
140
  */
121
- export function buildManifest(repoRoot, { nowIso } = {}) {
122
- const body = buildManifestBody(repoRoot);
141
+ export function buildManifest(repoRoot, { nowIso, collect } = {}) {
142
+ const body = buildManifestBody(repoRoot, collect);
123
143
  return {
124
144
  generatedAt: nowIso ?? new Date().toISOString(),
125
145
  generator: body.generator,
@@ -143,66 +163,147 @@ export function serializeManifest(manifest) {
143
163
  }
144
164
 
145
165
  /**
146
- * Read the on-disk manifest as a parsed object, or null when missing /
147
- * unparseable. The --check pipeline distinguishes "missing" (drift) from
148
- * "unparseable" (drift) via the returned `reason` channel.
166
+ * Resolve the manifest output path given (root, optional explicit
167
+ * override).
168
+ */
169
+ function resolveOutPath(root, override) {
170
+ return override
171
+ ? path.resolve(override)
172
+ : indexPathFor(root, PAYLOAD_SKILLS_SEGMENTS);
173
+ }
174
+
175
+ /**
176
+ * Resolve the local-zone manifest path. Deliberately NOT overridable by
177
+ * `--out`: that flag redirects the payload manifest (tests stage fixture
178
+ * trees with it), and letting it also move the local manifest would let one
179
+ * invocation write both indexes to the same file.
180
+ */
181
+ function resolveLocalOutPath(root) {
182
+ return indexPathFor(root, LOCAL_SKILLS_SEGMENTS);
183
+ }
184
+
185
+ /**
186
+ * Write one manifest through the project formatter so a regeneration on a
187
+ * clean tree leaves no format drift behind.
149
188
  */
150
- function readOnDiskManifest(outPath) {
151
- if (!fs.existsSync(outPath)) {
152
- return { manifest: null, reason: 'missing' };
189
+ function writeManifest(manifest, outPath, root) {
190
+ const serialized = serializeManifest(manifest);
191
+ const opts = { cwd: root, filename: INDEX_FILENAME };
192
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
193
+ fs.writeFileSync(
194
+ outPath,
195
+ formatGeneratedJson(serialized, opts) ?? serialized,
196
+ );
197
+ }
198
+
199
+ /**
200
+ * Write (or reap) the local-zone manifest. A consumer who deletes their last
201
+ * local skill would otherwise be left with a stale index reporting skills
202
+ * that no longer exist, so an emptied zone removes the artifact rather than
203
+ * leaving it behind.
204
+ */
205
+ function writeLocalManifest(localFresh, localOutPath, root) {
206
+ const rel = path.relative(root, localOutPath).split(path.sep).join('/');
207
+ if (localFresh === null) {
208
+ if (fs.existsSync(localOutPath)) {
209
+ fs.rmSync(localOutPath);
210
+ Logger.info(`removed ${rel} (no local skills remain)`);
211
+ }
212
+ return;
153
213
  }
154
- let src;
155
- try {
156
- src = fs.readFileSync(outPath, 'utf8');
157
- } catch (err) {
158
- return { manifest: null, reason: `read-error: ${err.message}` };
214
+ writeManifest(localFresh, localOutPath, root);
215
+ Logger.info(`wrote ${rel} (${localFresh.skills.length} entries)`);
216
+ }
217
+
218
+ /**
219
+ * Compare the local-zone manifest against fresh generator output. Returns
220
+ * null when in sync (including the common case of no local skills and no
221
+ * artifact), or a diff-style message.
222
+ */
223
+ function checkLocalManifest(localFresh, localOutPath) {
224
+ const exists = fs.existsSync(localOutPath);
225
+ if (localFresh === null) {
226
+ return exists
227
+ ? 'local skills.index.json drift detected: the local skills zone is ' +
228
+ 'empty but .agents/local/skills/skills.index.json still exists — ' +
229
+ "run 'node .agents/scripts/generate-skills-index.js' to reap it"
230
+ : null;
159
231
  }
160
- try {
161
- return { manifest: JSON.parse(src), reason: null };
162
- } catch (err) {
163
- return { manifest: null, reason: `parse-error: ${err.message}` };
232
+ if (!exists) {
233
+ return (
234
+ 'local skills.index.json drift detected: missing — run ' +
235
+ "'node .agents/scripts/generate-skills-index.js' to write it"
236
+ );
164
237
  }
238
+ const { manifest: disk } = readManifest(localOutPath);
239
+ return diffManifests(disk, localFresh, 'local skills.index.json');
165
240
  }
166
241
 
167
242
  /**
168
- * Compare two manifests ignoring `generatedAt`. Returns null when they
169
- * match, or a short diff-style message when they diverge.
243
+ * Build the local zone's manifest plan for this invocation: its output path,
244
+ * and a fresh manifest when the consumer has authored any local skill (null
245
+ * otherwise, which is the signal to reap a stale artifact).
246
+ *
247
+ * Split out of `run` so the payload path and the local path each read as one
248
+ * step there rather than interleaving.
249
+ *
250
+ * @param {string} root
251
+ * @param {Date} now
252
+ * @returns {{ localFresh: object | null, localOutPath: string }}
170
253
  */
171
- function diffManifestsIgnoringTimestamp(diskManifest, freshManifest) {
172
- if (diskManifest === null) {
173
- return 'on-disk manifest is missing or unreadable';
174
- }
175
- const a = { ...diskManifest };
176
- const b = { ...freshManifest };
177
- delete a.generatedAt;
178
- delete b.generatedAt;
179
- const sa = JSON.stringify(a);
180
- const sb = JSON.stringify(b);
181
- if (sa === sb) return null;
182
- // Surface a structural summary rather than a full JSON dump.
183
- const diskCount = Array.isArray(diskManifest.skills)
184
- ? diskManifest.skills.length
185
- : 'n/a';
186
- const freshCount = Array.isArray(freshManifest.skills)
187
- ? freshManifest.skills.length
188
- : 'n/a';
189
- const summary = [
190
- 'skills.index.json drift detected:',
191
- ` on-disk entries: ${diskCount}`,
192
- ` generated entries: ${freshCount}`,
193
- " run 'node .agents/scripts/generate-skills-index.js' to refresh",
194
- ].join('\n');
195
- return summary;
254
+ function buildLocalPlan(root, now) {
255
+ const localOutPath = resolveLocalOutPath(root);
256
+ const localFresh =
257
+ collectLocalSkillFiles(root).length > 0
258
+ ? buildManifest(root, {
259
+ nowIso: now.toISOString(),
260
+ collect: collectLocalSkillFiles,
261
+ })
262
+ : null;
263
+ return { localFresh, localOutPath };
196
264
  }
197
265
 
198
266
  /**
199
- * Resolve the manifest output path given (root, optional explicit
200
- * override).
267
+ * Render the freshness line's entry counts, naming the local zone only when
268
+ * one exists.
269
+ *
270
+ * @param {object} fresh
271
+ * @param {object | null} localFresh
272
+ * @returns {string}
201
273
  */
202
- function resolveOutPath(root, override) {
203
- return override
204
- ? path.resolve(override)
205
- : path.join(root, '.agents', 'skills', 'skills.index.json');
274
+ function describeCounts(fresh, localFresh) {
275
+ const base = `${fresh.skills.length} entries`;
276
+ return localFresh === null
277
+ ? base
278
+ : `${base}, ${localFresh.skills.length} local`;
279
+ }
280
+
281
+ /**
282
+ * `--check` mode: compare both manifests against fresh generator output and
283
+ * report the first drift found, payload first.
284
+ *
285
+ * Lives outside `run` so the entry point reads as "resolve inputs, then check
286
+ * or write" — and so the check path's branches are not charged to a function
287
+ * that also owns argument resolution.
288
+ *
289
+ * @param {{ outPath: string, fresh: object, localOutPath: string, localFresh: object | null }} plan
290
+ * @returns {{ status: number, output: string }}
291
+ */
292
+ function checkBothManifests({ outPath, fresh, localOutPath, localFresh }) {
293
+ const { manifest: disk, reason } = readManifest(outPath);
294
+ if (disk === null) {
295
+ return { status: 1, output: `${INDEX_FILENAME} drift detected: ${reason}` };
296
+ }
297
+ const drift =
298
+ diffManifests(disk, fresh, INDEX_FILENAME) ??
299
+ checkLocalManifest(localFresh, localOutPath);
300
+ if (drift !== null) {
301
+ return { status: 1, output: drift };
302
+ }
303
+ Logger.info(
304
+ `${INDEX_FILENAME} is fresh (${describeCounts(fresh, localFresh)})`,
305
+ );
306
+ return { status: 0, output: '' };
206
307
  }
207
308
 
208
309
  /**
@@ -225,35 +326,17 @@ export function run({ argv = [], now = new Date(), repoRoot } = {}) {
225
326
  : (repoRoot ?? defaultRepoRoot());
226
327
  const outPath = resolveOutPath(root, parsed.out);
227
328
  const fresh = buildManifest(root, { nowIso: now.toISOString() });
329
+ const { localFresh, localOutPath } = buildLocalPlan(root, now);
228
330
 
229
331
  if (parsed.check) {
230
- const { manifest: disk, reason } = readOnDiskManifest(outPath);
231
- if (disk === null) {
232
- return {
233
- status: 1,
234
- output: `skills.index.json drift detected: ${reason}`,
235
- };
236
- }
237
- const diff = diffManifestsIgnoringTimestamp(disk, fresh);
238
- if (diff === null) {
239
- Logger.info(
240
- `skills.index.json is fresh (${fresh.skills.length} entries)`,
241
- );
242
- return { status: 0, output: '' };
243
- }
244
- return { status: 1, output: diff };
332
+ return checkBothManifests({ outPath, fresh, localOutPath, localFresh });
245
333
  }
246
334
 
247
- const serialized = serializeManifest(fresh);
248
- const opts = { cwd: root, filename: 'skills.index.json' };
249
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
250
- fs.writeFileSync(
251
- outPath,
252
- formatGeneratedJson(serialized, opts) ?? serialized,
253
- );
335
+ writeManifest(fresh, outPath, root);
254
336
  Logger.info(
255
337
  `wrote ${path.relative(root, outPath).split(path.sep).join('/')} (${fresh.skills.length} entries)`,
256
338
  );
339
+ writeLocalManifest(localFresh, localOutPath, root);
257
340
  return { status: 0, output: '' };
258
341
  }
259
342
 
@@ -93,7 +93,7 @@ export function renderWorkflowsDoc(catalog, loopCatalog = []) {
93
93
  'Every command file lives at `.agents/workflows/<name>.md` and is projected',
94
94
  'into a flat `.claude/commands/` tree by `npm run sync:commands` (kept',
95
95
  'current at install time and on every `mandrel sync`/`update`) so it shows',
96
- 'up as a bare `/<name>` slash command (e.g. `/deliver`). The projection',
96
+ 'up as a bare `/<name>` slash command (e.g. `/mandrel-deliver`). The projection',
97
97
  'writes only `.claude/commands/<name>.md` — there is no plugin manifest and no',
98
98
  'marketplace listing. The commands load in every Claude Code environment.',
99
99
  '',
@@ -701,7 +701,7 @@ export function changeSetLacksSiblingTest(changedFiles) {
701
701
  * callers MUST pass the requested Epic's own branch ref (e.g.
702
702
  * `refs/heads/epic/<id>`) so the change set is pinned to that Epic's branch
703
703
  * rather than whatever HEAD the shared checkout happens to sit on. Under two
704
- * concurrent `/deliver` runs sharing one checkout, diffing against
704
+ * concurrent `/mandrel-deliver` runs sharing one checkout, diffing against
705
705
  * `HEAD` silently resolves the *other* Epic's change set (Story #3362). When
706
706
  * `headRef` cannot be resolved in the repo, the selector returns a
707
707
  * `degraded: true` envelope (or hard-fails in gate-mode) instead of diffing
@@ -140,7 +140,7 @@ const AUDIT_STORY_AXIS_LABELS = Object.freeze([
140
140
  name: AGENT_LABELS.READY,
141
141
  color: hex(LABEL_COLORS.AGENT),
142
142
  description:
143
- 'Parking state — frozen dispatch manifest exists; awaiting local /deliver',
143
+ 'Parking state — frozen dispatch manifest exists; awaiting local /mandrel-deliver',
144
144
  },
145
145
  {
146
146
  name: RISK_LABELS.HIGH,
@@ -292,7 +292,7 @@ export function buildStoryBody({ group, edges = [], issueByGroupKey = null }) {
292
292
  };
293
293
 
294
294
  // The `---` / `blocked by #N` footer is the canonical serializer's own, so
295
- // the body round-trips through `parse()` and `/deliver`'s resolver reads the
295
+ // the body round-trips through `parse()` and `/mandrel-deliver`'s resolver reads the
296
296
  // ordering from the same place it reads every other Story's.
297
297
  const canonicalSections = serialize(storyBody, {
298
298
  includeFooter: storyBody.depends_on.length > 0,
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * lib/audit-to-stories/seed-from-findings.js
3
3
  *
4
- * Build the `/plan --seed`-shaped one-pager markdown that the audit-to-
5
- * stories Single-plan grouping path emits for `/plan` to consume.
4
+ * Build the `/mandrel-plan --seed`-shaped one-pager markdown that the audit-to-
5
+ * stories Single-plan grouping path emits for `/mandrel-plan` to consume.
6
6
  *
7
7
  * The seed renders the canonical one-pager sections so the authoring
8
8
  * pass can sharpen it without having to invent context:
@@ -10,7 +10,7 @@
10
10
  * - Recommended Direction (rollup of recommendations by dimension)
11
11
  * - Key Assumptions (carries the source-report links forward)
12
12
  * - MVP Scope (the proposed Stories, one bullet per group)
13
- * - Key Files (explicit file paths so `/plan` authoring has concrete
13
+ * - Key Files (explicit file paths so `/mandrel-plan` authoring has concrete
14
14
  * anchors)
15
15
  * - Not Doing (out-of-scope items by convention)
16
16
  *
@@ -97,8 +97,8 @@ function formatMVPScope(groups) {
97
97
  const dims = g.dimensions.join(' / ');
98
98
  const file = g.files[0] ? ` (\`${g.files[0]}\`)` : '';
99
99
  // Carry each group's fingerprint (and location-based semantic-key)
100
- // footer into the seed so a Story authored from it via `/plan` inherits
101
- // the dedup identity — without this the recommended `/plan --seed-file`
100
+ // footer into the seed so a Story authored from it via `/mandrel-plan` inherits
101
+ // the dedup identity — without this the recommended `/mandrel-plan --seed-file`
102
102
  // path is invisible to the next sweep's dedup (Story #4626). The footers
103
103
  // are HTML comments, so they never render in the visible one-pager but
104
104
  // stay machine-readable for the dedup probe.
@@ -17,7 +17,7 @@
17
17
  * edges**. Both halves are written:
18
18
  *
19
19
  * 1. The **body footer** (`---` / `blocked by #N`) — canonical, parsed by
20
- * `/deliver`'s resolver, and the fallback when the dependencies API is
20
+ * `/mandrel-deliver`'s resolver, and the fallback when the dependencies API is
21
21
  * unavailable.
22
22
  * 2. The **native `blocked_by` relation** — visible in the GitHub UI,
23
23
  * readable without parsing markdown, and settable by an operator later.
@@ -13,7 +13,7 @@
13
13
  * The verification is **static**: we inspect `package.json` for a known BDD
14
14
  * runner dependency, and consult a small lookup table of which runners
15
15
  * support which pending/skip tag. We do not boot the runner. This keeps
16
- * `/plan` Phase 7 hermetic and offline.
16
+ * `/mandrel-plan` Phase 7 hermetic and offline.
17
17
  *
18
18
  * **Workspace awareness (Story #2956).** In a pnpm / npm / yarn monorepo the
19
19
  * BDD runner is rarely a root devDependency — it lives in the workspace
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * bdd-scenario-budget.js — envelope byte budget for the `bddScenarios`
3
- * `/plan` context-envelope field (Story #4977).
3
+ * `/mandrel-plan` context-envelope field (Story #4977).
4
4
  *
5
5
  * `bdd-scenario-scanner.js`'s `scanBddScenarios` stays a faithful, uncapped
6
6
  * index of the project's `.feature` corpus — that scan is also used
@@ -1,5 +1,5 @@
1
1
  /**
2
- * bdd-scenario-scanner.js — Gherkin scenario index for /plan Phase 7.
2
+ * bdd-scenario-scanner.js — Gherkin scenario index for /mandrel-plan Phase 7.
3
3
  *
4
4
  * Story #2637 (sibling to #2636's file-assumption gate; the #2634 and #2635
5
5
  * planner-grounding siblings were retired in Story #4811). The Acceptance
@@ -8,7 +8,7 @@
8
8
  * alone — it never inspects the consumer project's existing `.feature`
9
9
  * files. Planned ACs frequently duplicate scenarios that already exist or
10
10
  * re-specify behaviour the codebase already proves; the duplication is
11
- * only discovered (at best) during `/deliver` or (at worst) after
11
+ * only discovered (at best) during `/mandrel-deliver` or (at worst) after
12
12
  * a redundant PR ships.
13
13
  *
14
14
  * `scanBddScenarios` walks every configured feature root, parses each
@@ -21,7 +21,7 @@
21
21
  * have a matching scenario.
22
22
  *
23
23
  * Determinism is load-bearing: the matcher is keyword-based, not
24
- * embedding-based, so re-running `/plan` against the same
24
+ * embedding-based, so re-running `/mandrel-plan` against the same
25
25
  * acceptance spec produces the same disposition annotations.
26
26
  */
27
27
 
@@ -9,7 +9,7 @@
9
9
  * 1. **Discovery** — which files under a scope's `stepRoots` hold step
10
10
  * definitions, and which `.feature` files sit under its `featureRoots`.
11
11
  * Feature discovery is delegated to `listFeatureFiles` in
12
- * `bdd-scenario-scanner.js` rather than copied: /plan's scenario index and
12
+ * `bdd-scenario-scanner.js` rather than copied: /mandrel-plan's scenario index and
13
13
  * this gate must agree on what counts as a feature file, and two walkers
14
14
  * would eventually disagree.
15
15
  * 2. **Matching** — turn each definition's Cucumber expression or regular
@@ -7,7 +7,7 @@
7
7
  * - `enforce_admins: true` — admins do not bypass the prGate suite.
8
8
  * - `required_pull_request_reviews.required_approving_review_count: 0` —
9
9
  * CI is the gate; the operator monitors and iterates the open PR
10
- * to green via `/deliver`'s Phase 7 watch loop.
10
+ * to green via `/mandrel-deliver`'s Phase 7 watch loop.
11
11
  *
12
12
  * Behaviour rules
13
13
  * ---------------
@@ -112,8 +112,8 @@ export function buildManualInstructions({ stagePaths, baseBranch }) {
112
112
  ` git push -u origin ${baseBranch}`,
113
113
  '',
114
114
  'Story delivery runs in git worktrees that check out tracked files only,',
115
- 'so the .agents/ wiring MUST be committed before any /deliver or',
116
- '/deliver run — otherwise the worktree has no scripts and breaks.',
115
+ 'so the .agents/ wiring MUST be committed before any /mandrel-deliver or',
116
+ '/mandrel-deliver run — otherwise the worktree has no scripts and breaks.',
117
117
  ].join('\n');
118
118
  }
119
119
 
@@ -325,7 +325,7 @@ function renderConformanceWorkflow() {
325
325
  # Generated by agents-bootstrap-github. Runs the canonical story-body parser
326
326
  # against human-opened type::story issues and comments when the
327
327
  # body does not round-trip, instead of letting the supported human entry
328
- # points (e.g. /plan from an existing Epic ID) fail silently later. The lint
328
+ # points (e.g. /mandrel-plan from an existing Epic ID) fail silently later. The lint
329
329
  # informs; it never fails the issue. Re-run /agents-bootstrap-github to refresh.
330
330
  name: Issue Body Conformance
331
331
 
@@ -122,13 +122,98 @@ export function getChangedFiles({
122
122
  return parseNameOnlyStdout(res.stdout);
123
123
  }
124
124
 
125
+ /**
126
+ * A full-length hex object id, as `git rev-parse` prints it. Used to reject
127
+ * anything that is not a resolved commit — a stubbed git interface in a test
128
+ * answers every `gitSpawn` with the same canned stdout, and a file list must
129
+ * never be mistaken for a merge head.
130
+ */
131
+ const OBJECT_ID_RE = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
132
+
133
+ /**
134
+ * Resolve the commit an in-progress merge is merging **in**, or `null` when no
135
+ * merge is in progress.
136
+ *
137
+ * Story #5131. `git diff --cached` with no commit argument diffs the index
138
+ * against `HEAD`, and during a merge `HEAD` is still the pre-merge tip — so a
139
+ * base-sync merge commit (`git merge --no-edit origin/<base>`, which
140
+ * `single-story-close`'s base-sync phase tells the operator to run by hand)
141
+ * put every file the base branch had landed into the staged scope. The
142
+ * pre-commit MI/CRAP gate then blocked the resolution commit for deltas
143
+ * belonging to already-landed, already-gated work, with no remedy: the preview
144
+ * is a delta against the baseline, not a baseline comparison, so no baseline
145
+ * refresh could silence it.
146
+ *
147
+ * Two details are load-bearing:
148
+ *
149
+ * - **Ask git, never the filesystem.** `.git` is a *file*, not a directory,
150
+ * in the linked worktrees this repo delivers from, so an
151
+ * `existsSync('.git/MERGE_HEAD')` probe would be silently inert exactly
152
+ * where deliveries happen. `rev-parse --verify` resolves the ref through
153
+ * git's own worktree-aware lookup.
154
+ * - **`--verify` fails closed on an octopus merge.** It refuses a
155
+ * `MERGE_HEAD` naming more than one head, which lands here as `null` — the
156
+ * pre-#5131 behaviour. Narrowing the scope wrongly would hide a real
157
+ * regression; widening it only restores the status quo.
158
+ *
159
+ * Never throws: a merge is either detectable or it is not, and an
160
+ * undetectable one must degrade to the plain cached diff rather than fail the
161
+ * gate.
162
+ *
163
+ * @param {object} [params]
164
+ * @param {string} [params.cwd=process.cwd()]
165
+ * @param {ReturnType<typeof createGitInterface>} [params.git]
166
+ * @returns {string | null} The merge head's object id, or `null`.
167
+ */
168
+ export function resolveMergeHead({ cwd = process.cwd(), git } = {}) {
169
+ const gitIface = git ?? createGitInterface({});
170
+ let res;
171
+ try {
172
+ res = gitIface.gitSpawn(cwd, 'rev-parse', '-q', '--verify', 'MERGE_HEAD');
173
+ } catch {
174
+ return null;
175
+ }
176
+ if (res?.status !== 0) return null;
177
+ const sha = (res.stdout ?? '').trim();
178
+ return OBJECT_ID_RE.test(sha) ? sha : null;
179
+ }
180
+
181
+ /**
182
+ * Read the index file list against an explicit base, shared by
183
+ * `getStagedFiles` and `resolvePreviewScope` so the merge head is resolved
184
+ * once per scope resolution rather than once per caller.
185
+ *
186
+ * @param {object} params
187
+ * @param {string} params.cwd
188
+ * @param {ReturnType<typeof createGitInterface>} params.git
189
+ * @param {string | null} params.mergeHead
190
+ * @returns {string[]}
191
+ */
192
+ function stagedFilesAgainst({ cwd, git, mergeHead }) {
193
+ const args = ['diff', '--name-only', '--cached'];
194
+ if (mergeHead) args.push(mergeHead);
195
+ const res = git.gitSpawn(cwd, ...args);
196
+ if (res.status !== 0) {
197
+ const detail = res.stderr || res.stdout || `exit ${res.status}`;
198
+ throw new Error(`[staged] unable to read cached diff: ${detail}`);
199
+ }
200
+ return parseNameOnlyStdout(res.stdout);
201
+ }
202
+
125
203
  /**
126
204
  * Resolve paths in the index (staged for commit). Used by `quality-preview
127
205
  * --staged` so pre-commit gates score only the commit payload, not unstaged
128
206
  * working-tree edits.
129
207
  *
130
208
  * Semantics:
131
- * - Runs `git diff --name-only --cached`.
209
+ * - Runs `git diff --name-only --cached`, which diffs the index against
210
+ * `HEAD`.
211
+ * - **During a merge**, diffs the index against `MERGE_HEAD` instead
212
+ * (Story #5131), so the scope is the merging branch's own contribution
213
+ * plus its conflict resolutions — not the base branch's incoming work.
214
+ * `git merge-base HEAD MERGE_HEAD` would *not* do: diffing the index
215
+ * against the fork point re-admits everything the base branch landed since
216
+ * it, which is the whole defect.
132
217
  * - Returns forward-slash-normalized repo-relative paths.
133
218
  * - Non-zero git exit throws — staged mode must not silently widen scope.
134
219
  *
@@ -139,12 +224,11 @@ export function getChangedFiles({
139
224
  */
140
225
  export function getStagedFiles({ cwd = process.cwd(), git } = {}) {
141
226
  const gitIface = git ?? createGitInterface({});
142
- const res = gitIface.gitSpawn(cwd, 'diff', '--name-only', '--cached');
143
- if (res.status !== 0) {
144
- const detail = res.stderr || res.stdout || `exit ${res.status}`;
145
- throw new Error(`[staged] unable to read cached diff: ${detail}`);
146
- }
147
- return parseNameOnlyStdout(res.stdout);
227
+ return stagedFilesAgainst({
228
+ cwd,
229
+ git: gitIface,
230
+ mergeHead: resolveMergeHead({ cwd, git: gitIface }),
231
+ });
148
232
  }
149
233
 
150
234
  /**
@@ -154,6 +238,11 @@ export function getStagedFiles({ cwd = process.cwd(), git } = {}) {
154
238
  * is ignored. Otherwise a `changedSinceRef` limits to that three-dot diff;
155
239
  * when both are absent the caller runs in full-repo mode (`scopeSet: null`).
156
240
  *
241
+ * In `staged` scope, `diffRef` carries the in-progress merge head when there
242
+ * is one (Story #5131) and `null` otherwise, so a caller can tell the operator
243
+ * *why* the scope narrowed. `scope` stays `'staged'` either way — the merge is
244
+ * a property of the base the index is read against, not a different mode.
245
+ *
157
246
  * @param {object} [params]
158
247
  * @param {boolean} [params.staged=false]
159
248
  * @param {string | null} [params.changedSinceRef=null]
@@ -172,8 +261,10 @@ export function resolvePreviewScope({
172
261
  git,
173
262
  } = {}) {
174
263
  if (staged) {
175
- const files = getStagedFiles({ cwd, git });
176
- return { scopeSet: new Set(files), scope: 'staged', diffRef: null };
264
+ const gitIface = git ?? createGitInterface({});
265
+ const mergeHead = resolveMergeHead({ cwd, git: gitIface });
266
+ const files = stagedFilesAgainst({ cwd, git: gitIface, mergeHead });
267
+ return { scopeSet: new Set(files), scope: 'staged', diffRef: mergeHead };
177
268
  }
178
269
  if (changedSinceRef) {
179
270
  try {
@@ -26,7 +26,7 @@ export default {
26
26
  severity: 'blocker',
27
27
  // 'npm-test' keeps the test-wrapper preflight's refusal claim true: a
28
28
  // poisoned shared config is caught before a suite (or N concurrent
29
- // /deliver workers) inherits it, not only at close/retro time (#4580).
29
+ // /mandrel-deliver workers) inherits it, not only at close/retro time (#4580).
30
30
  scope: ['story-close', 'retro', 'npm-test'],
31
31
  autoCorrect: 'refuse-and-print',
32
32
  detect(state) {
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * Scope: 'story-close', 'retro'. The check runs at every
13
13
  * preflight surface that has the opportunity to invoke Story init —
14
- * primarily `/deliver`'s init step — and surfaces as a retro audit
14
+ * primarily `/mandrel-deliver`'s init step — and surfaces as a retro audit
15
15
  * signal if the failure mode resurfaces during a sprint.
16
16
  *
17
17
  * The check is `refuse-and-print`. Auto-rewriting an orchestration call
@@ -253,7 +253,7 @@ export function parseSprintArgs(
253
253
  process.env.AGENT_WORKTREE_ROOT ||
254
254
  null,
255
255
  recutOf: parseTicketId(values['recut-of']),
256
- // Story #4253: pre-resolved Epic linkage threaded by the /deliver
256
+ // Story #4253: pre-resolved Epic linkage threaded by the /mandrel-deliver
257
257
  // fan-out so `single-story-init.js` can skip redundant Epic lookups when
258
258
  // the parent already threaded Epic context (pre-v2; field retained for
259
259
  // CLI compatibility).
@@ -36,7 +36,7 @@ export function applyHeader(content, header) {
36
36
  * True when a workflow opts out of slash-command projection via a
37
37
  * `command: false` key in its YAML frontmatter (#4482). Used for dual-use
38
38
  * lens files (e.g. `audit-security.md`) that stay in
39
- * the payload as `/deliver` audit-suite prompts but must NOT surface as
39
+ * the payload as `/mandrel-deliver` audit-suite prompts but must NOT surface as
40
40
  * standalone slash commands because the host ships a native equivalent.
41
41
  *
42
42
  * Both `sync-claude-commands.js` (projection + orphan-reap) and the
@@ -36,7 +36,7 @@ export const DELIVERY_ROUTING_DEFAULTS = Object.freeze({
36
36
  /** @type {'minimal'|'standard'|'strict'} */
37
37
  ceremonyProfile: 'standard',
38
38
  /**
39
- * When true (default), attended `/deliver` lands through merge in one
39
+ * When true (default), attended `/mandrel-deliver` lands through merge in one
40
40
  * close (`--wait-merge` semantics) instead of stopping at `agent::closing`.
41
41
  * Operators opt out per-run with `--no-wait-merge`.
42
42
  */