mandrel 1.91.0 → 1.92.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 (109) hide show
  1. package/.agents/README.md +5 -5
  2. package/.agents/docs/SDLC.md +75 -107
  3. package/.agents/docs/configuration.md +1 -1
  4. package/.agents/docs/quality-gates.md +8 -7
  5. package/.agents/docs/workflows.md +4 -10
  6. package/.agents/instructions.md +2 -2
  7. package/.agents/schemas/agentrc.schema.json +1 -1
  8. package/.agents/schemas/lifecycle/README.md +2 -1
  9. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  10. package/.agents/schemas/risk-verdict.schema.json +14 -1
  11. package/.agents/scripts/README.md +0 -46
  12. package/.agents/scripts/analyze-execution.js +76 -1
  13. package/.agents/scripts/check-doc-links.js +3 -0
  14. package/.agents/scripts/check-workflow-cli-lint.js +208 -0
  15. package/.agents/scripts/epic-audit-recheck.js +1 -1
  16. package/.agents/scripts/epic-plan-clarity.js +16 -6
  17. package/.agents/scripts/epic-plan-decompose.js +37 -59
  18. package/.agents/scripts/epic-plan-healthcheck.js +39 -6
  19. package/.agents/scripts/epic-plan-spec.js +41 -168
  20. package/.agents/scripts/epic-reconcile.js +7 -1
  21. package/.agents/scripts/lib/Logger.js +4 -4
  22. package/.agents/scripts/lib/audit-suite/index.js +4 -5
  23. package/.agents/scripts/lib/audit-suite/runner.js +6 -4
  24. package/.agents/scripts/lib/audit-suite/selector.js +2 -3
  25. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  26. package/.agents/scripts/lib/command-header.js +20 -0
  27. package/.agents/scripts/lib/config/github.js +1 -3
  28. package/.agents/scripts/lib/config-settings-schema.js +4 -3
  29. package/.agents/scripts/lib/degraded-mode.js +1 -1
  30. package/.agents/scripts/lib/label-constants.js +18 -3
  31. package/.agents/scripts/lib/label-taxonomy.js +14 -3
  32. package/.agents/scripts/lib/mutation/baseline-snapshot.js +7 -6
  33. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +18 -8
  34. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +3 -4
  35. package/.agents/scripts/lib/orchestration/docs-digest.js +2 -2
  36. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +4 -0
  37. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +22 -12
  38. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +6 -1
  39. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +18 -2
  40. package/.agents/scripts/{lifecycle-diff.js → lib/orchestration/lifecycle/ledger-diff.js} +10 -76
  41. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +1 -1
  42. package/.agents/scripts/lib/orchestration/plan-context.js +510 -0
  43. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +177 -0
  44. package/.agents/scripts/lib/orchestration/plan-metrics.js +445 -0
  45. package/.agents/scripts/lib/orchestration/plan-persist/amend.js +359 -0
  46. package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +127 -0
  47. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +978 -0
  48. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +191 -0
  49. package/.agents/scripts/lib/orchestration/plan-reachability.js +160 -0
  50. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +3 -3
  51. package/.agents/scripts/lib/orchestration/planning-risk.js +1 -1
  52. package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
  53. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +1 -1
  54. package/.agents/scripts/lib/orchestration/ticketing/reads.js +11 -2
  55. package/.agents/scripts/lib/plan-phase-cleanup.js +16 -0
  56. package/.agents/scripts/lib/presentation/manifest-persistence.js +2 -3
  57. package/.agents/scripts/lib/templates/spec-author-prompts.js +74 -0
  58. package/.agents/scripts/plan-context.js +186 -0
  59. package/.agents/scripts/plan-critics.js +227 -0
  60. package/.agents/scripts/plan-persist.js +383 -0
  61. package/.agents/scripts/pr-watch-with-update.js +8 -7
  62. package/.agents/scripts/run-lint.js +10 -11
  63. package/.agents/scripts/story-plan.js +19 -7
  64. package/.agents/scripts/sync-claude-commands.js +10 -5
  65. package/.agents/skills/core/knowledge-transfer/SKILL.md +11 -7
  66. package/.agents/workflows/audit-documentation.md +5 -7
  67. package/.agents/workflows/audit-lighthouse.md +1 -0
  68. package/.agents/workflows/audit-security.md +1 -0
  69. package/.agents/workflows/audit-to-stories.md +16 -5
  70. package/.agents/workflows/git-deliver.md +76 -243
  71. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  72. package/.agents/workflows/helpers/code-review.md +7 -6
  73. package/.agents/workflows/helpers/deliver-epic.md +5 -4
  74. package/.agents/workflows/helpers/plan-epic-reference.md +109 -96
  75. package/.agents/workflows/helpers/plan-epic.md +246 -999
  76. package/.agents/workflows/helpers/plan-story.md +14 -14
  77. package/.agents/workflows/helpers/scope-triage-gate.md +6 -4
  78. package/.agents/workflows/helpers/single-story-deliver.md +5 -4
  79. package/.agents/workflows/helpers/worktree-lifecycle.md +9 -7
  80. package/.agents/workflows/mandrel-update.md +144 -466
  81. package/.agents/workflows/plan.md +73 -143
  82. package/docs/CHANGELOG.md +13 -0
  83. package/lib/cli/registry.js +21 -3
  84. package/package.json +1 -1
  85. package/.agents/schemas/loop-unit.schema.json +0 -70
  86. package/.agents/scripts/assert-branch.js +0 -81
  87. package/.agents/scripts/check-loop-units.js +0 -204
  88. package/.agents/scripts/detect-merges.js +0 -111
  89. package/.agents/scripts/git-pr-quality-gate.js +0 -205
  90. package/.agents/scripts/git-rebase-and-resolve.js +0 -234
  91. package/.agents/scripts/hierarchy-gate.js +0 -192
  92. package/.agents/scripts/hydrate-context.js +0 -179
  93. package/.agents/scripts/lib/loop-units/validate-loop-unit.js +0 -197
  94. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +0 -167
  95. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +0 -69
  96. package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +0 -86
  97. package/.agents/scripts/loc-delta.js +0 -205
  98. package/.agents/scripts/render-manifest.js +0 -143
  99. package/.agents/scripts/run-audit-suite.js +0 -97
  100. package/.agents/scripts/select-audits.js +0 -155
  101. package/.agents/scripts/update-mutation-baseline.js +0 -189
  102. package/.agents/workflows/explain.md +0 -118
  103. package/.agents/workflows/git-merge-pr.md +0 -377
  104. package/.agents/workflows/helpers/epic-plan-decompose.md +0 -22
  105. package/.agents/workflows/helpers/epic-plan-spec.md +0 -22
  106. package/.agents/workflows/loops/README.md +0 -65
  107. package/.agents/workflows/loops/fix-failing-tests.md +0 -74
  108. package/.agents/workflows/loops/nightly-audit.md +0 -81
  109. package/.agents/workflows/loops/watch-ci.md +0 -68
@@ -240,13 +240,17 @@ export async function runDecomposePhase(
240
240
  * @param {typeof defaultRunPlanHealthcheck} args.runHealthcheckFn
241
241
  * @returns {Promise<{ ok: boolean, waived?: boolean, reason?: string|null }>}
242
242
  */
243
- function enforceFanOutGate(findings, allowLargeFanOut) {
243
+ export function enforceFanOutGate(
244
+ findings,
245
+ allowLargeFanOut,
246
+ tag = 'epic-plan-decompose',
247
+ ) {
244
248
  const fanOut = (findings ?? []).filter((f) => f.kind === 'fan-out-warning');
245
249
  if (fanOut.length === 0) return;
246
250
  if (allowLargeFanOut) {
247
251
  for (const f of fanOut) {
248
252
  Logger.warn(
249
- `[epic-plan-decompose] Persisting a large-fan-out deletion: ` +
253
+ `[${tag}] Persisting a large-fan-out deletion: ` +
250
254
  `Task "${f.taskSlug}" deletes "${f.path}" with ${f.callSiteCount} ` +
251
255
  `call site(s) (threshold ${f.threshold}). Operator override --allow-large-fan-out.`,
252
256
  );
@@ -260,7 +264,7 @@ function enforceFanOutGate(findings, allowLargeFanOut) {
260
264
  )
261
265
  .join('\n');
262
266
  throw new Error(
263
- `[epic-plan-decompose] ${fanOut.length} Task(s) declare large-fan-out deletions:\n${lines}\n\n` +
267
+ `[${tag}] ${fanOut.length} Task(s) declare large-fan-out deletions:\n${lines}\n\n` +
264
268
  `Split each deletion into a subsystem-by-subsystem migration across multiple Stories, ` +
265
269
  `or rerun --allow-large-fan-out after confirming the deletion is intentional.`,
266
270
  );
@@ -284,24 +288,30 @@ function enforceFanOutGate(findings, allowLargeFanOut) {
284
288
  *
285
289
  * @param {object[]} findings
286
290
  */
287
- function surfaceSoftConflictFindings(findings) {
291
+ export function surfaceSoftConflictFindings(
292
+ findings,
293
+ tag = 'epic-plan-decompose',
294
+ ) {
288
295
  const soft = (findings ?? []).filter(
289
296
  (f) => f?.severity === 'soft' && f?.kind !== 'fan-out-warning',
290
297
  );
291
298
  if (soft.length === 0) return;
292
299
  Logger.warn(
293
- `[epic-plan-decompose] ${soft.length} soft cross-Story conflict finding(s) — review before approving the plan:`,
300
+ `[${tag}] ${soft.length} soft cross-Story conflict finding(s) — review before approving the plan:`,
294
301
  );
295
302
  for (const finding of soft) {
296
- Logger.warn(
297
- `[epic-plan-decompose] soft conflict: ${renderHardConflictError(finding)}`,
298
- );
303
+ Logger.warn(`[${tag}] soft conflict: ${renderHardConflictError(finding)}`);
299
304
  }
300
305
  }
301
306
 
302
- async function runHealthcheckGate({ epicId, epic, runHealthcheckFn }) {
307
+ export async function runHealthcheckGate({
308
+ epicId,
309
+ epic,
310
+ runHealthcheckFn,
311
+ tag = 'epic-plan-decompose',
312
+ }) {
303
313
  Logger.info(
304
- `[epic-plan-decompose] Running post-plan readiness healthcheck for Epic #${epicId}...`,
314
+ `[${tag}] Running post-plan readiness healthcheck for Epic #${epicId}...`,
305
315
  );
306
316
  let result;
307
317
  try {
@@ -321,13 +331,13 @@ async function runHealthcheckGate({ epicId, epic, runHealthcheckFn }) {
321
331
  const waived = labels.includes(PLANNING_HEALTHCHECK_WAIVED);
322
332
  if (waived) {
323
333
  Logger.warn(
324
- `[epic-plan-decompose] Healthcheck failed for Epic #${epicId} but '${PLANNING_HEALTHCHECK_WAIVED}' is applied — proceeding with agent::ready handoff. Reason: ${result?.reason ?? '(no reason reported)'}`,
334
+ `[${tag}] Healthcheck failed for Epic #${epicId} but '${PLANNING_HEALTHCHECK_WAIVED}' is applied — proceeding with agent::ready handoff. Reason: ${result?.reason ?? '(no reason reported)'}`,
325
335
  );
326
336
  return { ok: false, waived: true, reason: result?.reason ?? null };
327
337
  }
328
338
 
329
339
  throw new Error(
330
- `[epic-plan-decompose] Refusing agent::ready handoff for Epic #${epicId}: ` +
340
+ `[${tag}] Refusing agent::ready handoff for Epic #${epicId}: ` +
331
341
  `post-plan healthcheck failed (${result?.reason ?? '(no reason reported)'}). ` +
332
342
  `Resolve the failing check(s), or apply the '${PLANNING_HEALTHCHECK_WAIVED}' ` +
333
343
  `label to the Epic to override and rerun the persist phase.`,
@@ -113,7 +113,12 @@ export async function buildAuthoringContext(
113
113
  settings = {},
114
114
  opts = {},
115
115
  ) {
116
- const epic = await provider.getEpic(epicId);
116
+ // Epic #4474 (M3 PR2): `opts.epic` is an optional prefetched Epic object
117
+ // so a caller that already holds the issue (the folded `plan-context.js`
118
+ // envelope build, which also needs the raw body for clarity scoring and
119
+ // re-plan detection) does not pay a second provider fetch. Absent, the
120
+ // fetch behaviour is unchanged.
121
+ const epic = opts.epic ?? (await provider.getEpic(epicId));
117
122
  if (!epic) {
118
123
  throw new Error(`Epic #${epicId} not found.`);
119
124
  }
@@ -568,7 +568,20 @@ export function diff({ spec, state, ghState } = {}) {
568
568
 
569
569
  for (const entity of flattenSpec(spec)) {
570
570
  seenSpecSlugs.add(entity.slug);
571
- const mapped = mapping[entity.slug];
571
+ // Epic #4474 (PR3) — a mapping entry without a numeric `issueNumber`
572
+ // is a partial-failure tombstone: the apply's state writer projects an
573
+ // entry per spec slug, but only completed creations carry a number
574
+ // (`buildState` seeds new slugs with `issueNumber: null`). Treating
575
+ // such an entry as "mapped" made a `--resume` after a rate-limit crash
576
+ // silently skip every failed creation (no Create emitted; the content
577
+ // hash matches, so no Update either — a lossy resume). A numberless
578
+ // entry is therefore unmapped here → Create. No duplicate risk: any
579
+ // live same-title issue was already reseeded onto the slug (with its
580
+ // number) by `reseedMappingFromGh` before the diff runs.
581
+ const mappedEntry = mapping[entity.slug];
582
+ const mapped = Number.isInteger(mappedEntry?.issueNumber)
583
+ ? mappedEntry
584
+ : undefined;
572
585
 
573
586
  if (!mapped) {
574
587
  plan.creates.push(
@@ -630,9 +643,12 @@ export function diff({ spec, state, ghState } = {}) {
630
643
  }
631
644
  }
632
645
 
633
- // Closes — anything in mapping not seen in spec.
646
+ // Closes — anything in mapping not seen in spec. A numberless
647
+ // partial-failure tombstone (see above) has no live issue to close;
648
+ // dropping it from the spec simply drops the entry.
634
649
  for (const [slug, mapped] of Object.entries(mapping)) {
635
650
  if (seenSpecSlugs.has(slug)) continue;
651
+ if (!Number.isInteger(mapped?.issueNumber)) continue;
636
652
  plan.closes.push(
637
653
  closeOp({
638
654
  slug,
@@ -1,35 +1,22 @@
1
- #!/usr/bin/env node
2
- /* node:coverage ignore file */
3
-
4
1
  /**
5
- * lifecycle-diff.js — assertion modes over a lifecycle ledger file for
6
- * the repeatability invariants. The structural-`diff` helpers below are
7
- * exported for unit tests that pin the diff contract.
2
+ * ledger-diff.js — structural-diff and ordering-assertion helpers over a
3
+ * lifecycle ledger (`temp/epic-<id>/lifecycle.ndjson`) for the
4
+ * repeatability invariants.
8
5
  *
9
- * Usage:
10
- * node .agents/scripts/lifecycle-diff.js --assert <mode> <ledger>
11
- * `mode` is one of:
12
- * merge-gate-ordering — epic.merge.armed must be preceded
13
- * by epic.merge.ready (same seqId
14
- * chain; armed.seqId > ready.seqId).
15
- * reconcile-ordering — pr.created must be preceded by
16
- * acceptance.reconcile.ok.
17
- * — exits 0 on pass; exits 1 with a structured message on fail.
6
+ * Relocated from the retired `.agents/scripts/lifecycle-diff.js` CLI
7
+ * (#4482): the assertion helpers are the live surface — they pin the
8
+ * cross-listener ordering contracts in the lifecycle invariant tests —
9
+ * while the CLI wrapper had no workflow, script, or CI consumer.
18
10
  *
19
11
  * Invariants are derived from Tech Spec #2189 § Repeatability Acceptance
20
12
  * Criteria.
21
13
  */
22
14
 
23
- import { readFileSync } from 'node:fs';
24
- import { parseArgs } from 'node:util';
25
-
26
- import { runAsCli } from './lib/cli-utils.js';
27
-
28
15
  /**
29
16
  * Parse an NDJSON lifecycle ledger into an array of records. Blank
30
17
  * lines tolerated; malformed JSON throws with line number. Duplicated
31
- * from `lib/orchestration/lifecycle/trace-logger.js` to avoid coupling
32
- * the CLI to the listener surface.
18
+ * from `trace-logger.js` to keep the diff surface decoupled from the
19
+ * listener surface.
33
20
  */
34
21
  export function parseLedgerText(text) {
35
22
  const out = [];
@@ -41,7 +28,7 @@ export function parseLedgerText(text) {
41
28
  out.push(JSON.parse(line));
42
29
  } catch (_err) {
43
30
  throw new Error(
44
- `lifecycle-diff: malformed JSON in ledger on line ${i + 1}: ${line.slice(0, 80)}`,
31
+ `ledger-diff: malformed JSON in ledger on line ${i + 1}: ${line.slice(0, 80)}`,
45
32
  );
46
33
  }
47
34
  }
@@ -151,56 +138,3 @@ export function assertReconcileOrdering(records) {
151
138
  }
152
139
  return { ok: true };
153
140
  }
154
-
155
- const ASSERTIONS = new Map([
156
- ['merge-gate-ordering', assertMergeGateOrdering],
157
- ['reconcile-ordering', assertReconcileOrdering],
158
- ]);
159
-
160
- function loadLedger(p) {
161
- return parseLedgerText(readFileSync(p, 'utf8'));
162
- }
163
-
164
- async function main() {
165
- const { values, positionals } = parseArgs({
166
- options: {
167
- assert: { type: 'string' },
168
- },
169
- allowPositionals: true,
170
- args: process.argv.slice(2),
171
- });
172
-
173
- // --assert <mode> <ledger>
174
- if (!values.assert) {
175
- process.stderr.write('Usage: lifecycle-diff --assert <mode> <ledger>\n');
176
- return 2;
177
- }
178
- if (positionals.length !== 1) {
179
- process.stderr.write(
180
- 'lifecycle-diff --assert <mode> requires exactly one positional ledger path\n',
181
- );
182
- return 2;
183
- }
184
- const assertion = ASSERTIONS.get(values.assert);
185
- if (!assertion) {
186
- process.stderr.write(
187
- `lifecycle-diff: unknown --assert mode "${values.assert}". Valid: ${[...ASSERTIONS.keys()].join(', ')}\n`,
188
- );
189
- return 2;
190
- }
191
- const records = loadLedger(positionals[0]);
192
- const result = assertion(records);
193
- if (result.ok) {
194
- process.stdout.write(`[lifecycle-diff] PASS ${values.assert}\n`);
195
- return 0;
196
- }
197
- process.stderr.write(
198
- `[lifecycle-diff] FAIL ${values.assert}: ${result.reason}\n`,
199
- );
200
- return 1;
201
- }
202
-
203
- await runAsCli(import.meta.url, main, {
204
- source: 'lifecycle-diff',
205
- propagateExitCode: true,
206
- });
@@ -199,7 +199,7 @@ export class AcceptanceReconciler {
199
199
  ? epicIdFromPayload
200
200
  : this.epicId;
201
201
 
202
- // 1. Start emit. The downstream lifecycle-diff `reconcile-ordering`
202
+ // 1. Start emit. The downstream ledger-diff `reconcile-ordering`
203
203
  // assert checks for `acceptance.reconcile.ok` before `pr.created`,
204
204
  // but operators want to see the `.start` marker in the trace too.
205
205
  try {