mandrel 1.76.0 → 1.78.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 (68) hide show
  1. package/.agents/docs/configuration.md +2 -2
  2. package/.agents/docs/workflows.md +19 -0
  3. package/.agents/schemas/agentrc.schema.json +1 -1
  4. package/.agents/schemas/dispatch-manifest.json +1 -1
  5. package/.agents/schemas/lifecycle/loop.tick.schema.json +20 -0
  6. package/.agents/schemas/loop-unit.schema.json +70 -0
  7. package/.agents/schemas/validation-evidence.schema.json +2 -1
  8. package/.agents/scripts/audit-to-stories.js +43 -1
  9. package/.agents/scripts/check-doc-links.js +24 -1
  10. package/.agents/scripts/check-loop-units.js +204 -0
  11. package/.agents/scripts/epic-deliver-prepare.js +31 -0
  12. package/.agents/scripts/evidence-gate.js +48 -12
  13. package/.agents/scripts/generate-workflows-doc.js +37 -4
  14. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +141 -34
  15. package/.agents/scripts/lib/cli-args.js +6 -0
  16. package/.agents/scripts/lib/close-validation/process.js +61 -5
  17. package/.agents/scripts/lib/close-validation/runner.js +42 -9
  18. package/.agents/scripts/lib/config/temp-paths.js +1 -1
  19. package/.agents/scripts/lib/config/worktree-isolation.js +18 -3
  20. package/.agents/scripts/lib/config-resolver.js +4 -1
  21. package/.agents/scripts/lib/config-settings-schema-delivery.js +1 -1
  22. package/.agents/scripts/lib/git-branch-lifecycle.js +90 -0
  23. package/.agents/scripts/lib/loop-units/validate-loop-unit.js +197 -0
  24. package/.agents/scripts/lib/mandrel-catalog.js +36 -0
  25. package/.agents/scripts/lib/orchestration/auto-merge-cwd.js +128 -0
  26. package/.agents/scripts/lib/orchestration/column-sync.js +88 -9
  27. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +183 -0
  28. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +20 -2
  29. package/.agents/scripts/lib/orchestration/project-meta-cache.js +238 -0
  30. package/.agents/scripts/lib/orchestration/reassert-status-column.js +3 -1
  31. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +25 -2
  32. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +80 -14
  33. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +74 -25
  34. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +10 -1
  35. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +48 -1
  36. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +148 -4
  37. package/.agents/scripts/lib/orchestration/ticketing/transition.js +8 -1
  38. package/.agents/scripts/lib/story-body/story-body.js +76 -7
  39. package/.agents/scripts/lib/story-init/branch-initializer.js +29 -43
  40. package/.agents/scripts/lib/story-init/hierarchy-tracer.js +25 -4
  41. package/.agents/scripts/lib/story-init/task-graph-builder.js +22 -12
  42. package/.agents/scripts/lib/templates/decomposer-prompts.js +23 -0
  43. package/.agents/scripts/lib/validation-evidence.js +63 -25
  44. package/.agents/scripts/lib/worktree/node-modules-strategy.js +239 -31
  45. package/.agents/scripts/providers/github/branch-protection.js +1 -1
  46. package/.agents/scripts/providers/github/errors.js +53 -2
  47. package/.agents/scripts/providers/github/labels.js +1 -1
  48. package/.agents/scripts/providers/github/projects-v2-graphql.js +1 -1
  49. package/.agents/scripts/resync-status-column.js +5 -0
  50. package/.agents/scripts/run-coverage.js +85 -45
  51. package/.agents/scripts/run-lint.js +11 -0
  52. package/.agents/scripts/single-story-init.js +22 -29
  53. package/.agents/scripts/story-init.js +38 -63
  54. package/.agents/scripts/story-phase.js +46 -4
  55. package/.agents/scripts/sync-claude-commands.js +112 -29
  56. package/.agents/scripts/update-maintainability-baseline.js +19 -76
  57. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +5 -3
  58. package/.agents/workflows/helpers/acceptance-self-eval.md +27 -0
  59. package/.agents/workflows/helpers/deliver-epic.md +19 -2
  60. package/.agents/workflows/helpers/epic-deliver-story.md +50 -14
  61. package/.agents/workflows/helpers/single-story-deliver.md +12 -0
  62. package/.agents/workflows/loops/README.md +65 -0
  63. package/.agents/workflows/loops/fix-failing-tests.md +74 -0
  64. package/.agents/workflows/loops/nightly-audit.md +71 -0
  65. package/.agents/workflows/loops/watch-ci.md +68 -0
  66. package/docs/CHANGELOG.md +51 -0
  67. package/package.json +1 -1
  68. package/.agents/scripts/providers/github/transient-retry.js +0 -62
@@ -8,6 +8,18 @@
8
8
  * resolution, envelope assembly, and persistence flows through the unified
9
9
  * service.
10
10
  *
11
+ * Story #4293: the CLI no longer injects a bespoke maintainability scorer.
12
+ * It now lets `refreshBaseline` resolve the canonical default scorer
13
+ * (`buildDefaultMaintainabilityScorer`) the same way `update-crap-baseline.js`
14
+ * and `update-coverage-baseline.js` route through their canonical defaults.
15
+ * The previously-injected `buildMaintainabilityScorer` was a stale copy of the
16
+ * canonical scorer that never received the `ignoreGlobs` fix on its diff-scope
17
+ * branch, so an ignored-but-changed file (e.g. one matched by
18
+ * `config-settings-schema*.js` or a consumer's `seed.mjs`) leaked into `rows`
19
+ * and dragged `rollup["*"].min` below the maintainability floor. The canonical
20
+ * default scorer applies the ignore filter on BOTH the full-scope walk and the
21
+ * diff-scope branch, eliminating the divergence at the source.
22
+ *
11
23
  * Surface:
12
24
  *
13
25
  * - `--diff-scope <ref>` (or `--diff-scope=<ref>`): explicitly scope the
@@ -18,13 +30,10 @@
18
30
  * Operators wanting a full rewrite must pass `--full-scope` (added by
19
31
  * Task #2214; see that Task's notes for the cut-over).
20
32
  *
21
- * The scoring step (escomplex / typhonjs maintainability index) is
22
- * injected as a scorer function via the service's `opts.scorer` seam.
23
33
  * Full-scope refreshes (`scope.mode === 'full'`) walk every configured
24
34
  * target directory; diff/explicit refreshes score only the files the
25
- * service hands in. This keeps the manual CLI byte-identical (per
26
- * AC-3 see Task #2212's byte-identity test) to whatever code path
27
- * story-close would have produced for the same scope.
35
+ * service hands in. Both paths drop `ignoreGlobs`-listed files via the
36
+ * canonical default scorer.
28
37
  */
29
38
 
30
39
  // Fail-fast if the framework's runtime deps are not installed — must be the
@@ -33,16 +42,10 @@
33
42
  import './lib/runtime-deps/ensure-installed.js';
34
43
  import path from 'node:path';
35
44
  import { parseDiffScopeFlag } from './lib/baselines/diff-scope-cli.js';
36
- import { filterExcludedRows } from './lib/baselines/kinds/maintainability.js';
37
45
  import { refreshBaseline } from './lib/baselines/refresh-service.js';
38
46
  import { getBaselineEpsilon } from './lib/config/quality.js';
39
- import {
40
- getBaselines,
41
- getQuality,
42
- resolveConfig,
43
- } from './lib/config-resolver.js';
47
+ import { getBaselines, resolveConfig } from './lib/config-resolver.js';
44
48
  import { Logger } from './lib/Logger.js';
45
- import { calculateAll, scanDirectory } from './lib/maintainability-utils.js';
46
49
 
47
50
  /**
48
51
  * Parse `--full-scope` (boolean opt-out flag).
@@ -54,60 +57,6 @@ function parseFullScopeFlag(argv = []) {
54
57
  return argv.includes('--full-scope');
55
58
  }
56
59
 
57
- /**
58
- * Build the per-kind scorer the service will invoke. The scorer receives
59
- * `(files, { fullScope })`:
60
- *
61
- * - `fullScope === true`: ignore `files`, walk every configured target
62
- * directory, score every supported source file, return rows.
63
- * - `fullScope === false`: `files` is the resolved (diff or explicit)
64
- * scope. Score only those that fall under a configured target
65
- * directory; rows outside that set are dropped (the service / writer
66
- * preserves their prior-on-disk entries verbatim).
67
- *
68
- * The scorer is `cwd`-aware: the service passes its `cwd` through so all
69
- * path normalisation stays consistent with diff-scope derivation.
70
- */
71
- function buildMaintainabilityScorer({ targetDirs, ignoreGlobs = [], logger }) {
72
- return async function maintainabilityScorer(files, opts) {
73
- const cwd = opts?.cwd ?? process.cwd();
74
- let absPaths;
75
- if (opts?.fullScope) {
76
- absPaths = [];
77
- for (const dir of targetDirs) {
78
- const abs = path.isAbsolute(dir) ? dir : path.resolve(cwd, dir);
79
- logger.info(`[Maintainability] Scanning ${dir}...`);
80
- scanDirectory(abs, absPaths, { cwd, ignoreGlobs });
81
- }
82
- } else {
83
- // Files come in as canonical POSIX repo-relative paths from the
84
- // service. Resolve to absolute paths for the scorer, but only keep
85
- // the ones that fall under a configured target dir — rows outside
86
- // those roots are the gate's responsibility, not the baseline's.
87
- const targetAbsDirs = targetDirs.map((dir) =>
88
- path.isAbsolute(dir) ? dir : path.resolve(cwd, dir),
89
- );
90
- absPaths = [];
91
- for (const rel of files ?? []) {
92
- const abs = path.resolve(cwd, rel);
93
- const underTarget = targetAbsDirs.some(
94
- (root) => abs === root || abs.startsWith(`${root}${path.sep}`),
95
- );
96
- if (underTarget) absPaths.push(abs);
97
- }
98
- }
99
-
100
- logger.info(
101
- `[Maintainability] Calculating scores for ${absPaths.length} files...`,
102
- );
103
- const scores = await calculateAll(absPaths);
104
- const rows = Object.entries(scores).map(([p, mi]) => ({ path: p, mi }));
105
- // Story #2467 / Task #2494: drop files the escomplex kernel can't parse
106
- // so they stop landing as `mi: 0` phantom entries in the baseline.
107
- return filterExcludedRows(rows);
108
- };
109
- }
110
-
111
60
  async function main() {
112
61
  const argv = process.argv.slice(2);
113
62
  const diffScopeRef = parseDiffScopeFlag(argv);
@@ -120,9 +69,6 @@ async function main() {
120
69
  }
121
70
 
122
71
  const config = resolveConfig();
123
- const miQuality = getQuality(config).maintainability;
124
- const targetDirs = miQuality.targetDirs;
125
- const ignoreGlobs = miQuality.ignoreGlobs ?? [];
126
72
  const baselinePath = getBaselines(config).maintainability.path;
127
73
  const absBaselinePath = path.isAbsolute(baselinePath)
128
74
  ? baselinePath
@@ -140,21 +86,18 @@ async function main() {
140
86
  );
141
87
  }
142
88
 
143
- const scorer = buildMaintainabilityScorer({
144
- targetDirs,
145
- ignoreGlobs,
146
- logger: Logger,
147
- });
148
-
149
89
  // Task #2214 (Epic #2173, AC-2): flag-omission now defaults to
150
90
  // diff-scope. The pre-migration default was a full regenerate; operators
151
91
  // wanting that behaviour must now pass `--full-scope` explicitly. This is
152
92
  // a deliberate breaking CLI behaviour change — see docs/CHANGELOG.md.
93
+ //
94
+ // Story #4293: no `scorer` is injected — the service resolves the canonical
95
+ // default maintainability scorer, which applies `ignoreGlobs` on both the
96
+ // full-scope walk and the diff-scope branch.
153
97
  const refreshOpts = {
154
98
  kind: 'maintainability',
155
99
  writePath: absBaselinePath,
156
100
  epsilon,
157
- scorer,
158
101
  };
159
102
  if (fullScope) {
160
103
  refreshOpts.fullScope = true;
@@ -206,6 +206,7 @@ They are NOT top-level ticket fields.
206
206
 
207
207
  - **slug**: MUST be hyphen-case (`^[a-z0-9][a-z0-9-]*$`). Do not use underscores.
208
208
  - **goal** (in body string): One sentence stating WHY this Story exists within the Epic.
209
+ - **reason_to_exist** (REQUIRED, encoded in the body `<!-- meta: {...} -->` comment — NOT a top-level ticket field): One sentence stating the single coherent reason this Story exists, distinct from the broader `goal` prose. Every Story MUST carry a non-empty `reason_to_exist`; it is the machine-checkable form of the cohesion rule (**one Story = one coherent change with one reason to exist**). The `epic-plan-consolidate` critic flags any Story whose body carries no non-empty reason, and the sizing validator (`ticket-validator-sizing.js`) emits a deterministic **soft** `missing-reason-to-exist` finding as the runtime backstop. Encode it as `<!-- meta: {"reason_to_exist": "..."} -->`.
209
210
  - **changes** (in body string): Each entry is an object `{ path, assumption }` where `assumption` is one of `creates | refactors-existing | deletes`. The Phase 8 validator probes the base branch for every declared path and rejects the decompose when the declared assumption contradicts reality: `creates` against an existing path is an error, `refactors-existing` / `deletes` against a missing path is an error. Use `refactors-existing` for in-place edits to a file already on `main`; `creates` for net-new files; `deletes` for removals. Acceptable path shapes include explicit files (`src/components/Foo.tsx`), glob patterns (`tests/e2e/*.spec.ts`, `**/*.astro`), and module identifiers that resolve to files.
210
211
  - **references** (in body string, optional): Object-form entries `{ path, assumption: "exists" }` for paths the Story **reads** but does not modify (test fixtures it relies on, sibling modules it imports, feature files it scans). The validator probes these like `changes` and rejects the decompose when an `exists` path is absent on the base branch. Use this list to make read-dependencies explicit so a hallucinated or stale assumption surfaces at planning time rather than execution time.
211
212
  - **NEW-FILE CONTRACT (must-follow)**: Any path the Story references in `goal`, `acceptance`, or `verify` that does **not** already exist on `main` MUST also appear in the same Story's `changes` array with `assumption: "creates"`. The freshness validator probes `main` for every referenced code path and rejects the decompose when a missing path is absent from `changes` — even when the Story is the one authoring the file. Example: a Story creating `tests/lib/foo.test.js` whose `verify` runs `node --test tests/lib/foo.test.js` MUST include `{ "path": "tests/lib/foo.test.js", "assumption": "creates" }` in `changes`, otherwise the validator emits a freshness miss and the decompose round trips for a re-emit.
@@ -266,10 +267,11 @@ Declaring `wide` with a non-empty reason **lifts the `hardFiles` rejection** —
266
267
 
267
268
  #### BINDING ACCEPTANCE vs ADVISORY CHANGES (authoring altitude)
268
269
 
269
- `acceptance[]` and `verify[]` are the **binding contract** the executor MUST satisfy — they are the sole definition of "done." `changes[]` and `references[]` are an **advisory implementation sketch**: your best prediction of the file footprint, which the executor is permitted to revise when the real codebase diverges from the sketch. Author at that altitude:
270
+ The canonical altitude + New-File Contract wording is single-sourced in `AUTHORING_ALTITUDE_GUIDANCE` in `ticket-validator-sizing.js` (Story #4272); the rendered decomposer prompt interpolates the same strings, so do not restate a divergent version here. The three canonical statements:
270
271
 
271
- - Write `acceptance[]`/`verify[]` to capture the **outcome**, independent of any one file layout. Do NOT pin an incidental implementation detail (an internal helper name, a private file path) into an acceptance item that the advisory `changes[]` is free to reshape assert the observable behaviour instead.
272
- - Keep `changes[]`/`references[]` as the honest predicted footprint. They still pass through the structural file-assumption gate (the `creates`/`refactors-existing`/`deletes` probes against the base branch) and the New-File Contract unchanged advisory does NOT mean unvalidated. The executor's latitude to revise the approach never licenses skipping `acceptance[]`/`verify[]` or any `rules/security-baseline.md` MUST.
272
+ - **Binding contract vs advisory sketch.** `acceptance[]` and `verify[]` are the Story's **binding contract** — the executor MUST satisfy them exactly, and they are the only definition of "done." `changes[]` and `references[]` are an **advisory implementation sketch**: your best prediction of the file footprint, which the executor MAY revise when the real codebase diverges from the sketch. Author `acceptance[]` / `verify[]` to assert the **outcome** independent of any one file layout never pin an incidental implementation detail (an internal helper name, a private file path) into an acceptance item that the advisory `changes[]` is free to reshape; assert the observable behaviour instead.
273
+ - **New-File Contract.** Any path named in a Story's `goal`, `acceptance`, or `verify` that does NOT already exist on `main` MUST also appear in that Story's `changes[]` with `assumption: "creates"`; otherwise the freshness validator rejects the decompose even when the Story is the one authoring the file.
274
+ - **Advisory does not mean unvalidated.** `changes[]` paths still pass the base-branch file-assumption probes (a `creates` against an existing path still fails), the New-File Contract still holds, and the executor's latitude to revise the approach never licenses skipping `acceptance[]` / `verify[]` or relaxing any `rules/security-baseline.md` MUST.
273
275
 
274
276
  #### NAVIGATE-DON'T-DEEP-LINK (signed-in acceptance scenarios)
275
277
 
@@ -43,6 +43,33 @@ per-criterion, mid-delivery, and evaluates the actual work product.
43
43
  optional advisory pre-flight — a criterion cannot be scored `met` without
44
44
  the supporting `verify[]` evidence where a `verify[]` command is relevant
45
45
  to it.
46
+ - **Shares `lint` / `typecheck` evidence with close (Story #4250).** When a
47
+ `verify[]` command is **byte-identical** to a close-validation gate — in
48
+ practice only the cheap, command-identical `lint` and `typecheck` gates
49
+ (`npm run lint` and the resolved `project.commands.typecheck`) — the
50
+ critic MUST run it through `evidence-gate.js` so a passing run records an
51
+ evidence entry in the **same keyspace** `close-validation/runner.js`
52
+ consults. Run it in the **same Story worktree** the close validates (the
53
+ HEAD-sha key enforces "unchanged HEAD") and pass the exact gate name:
54
+
55
+ ```bash
56
+ # Epic-attached Story:
57
+ node <main-repo>/.agents/scripts/evidence-gate.js \
58
+ --epic-id <epicId> --scope-id <storyId> --gate lint \
59
+ --worktree <worktree> -- npm run lint
60
+
61
+ # Standalone Story (no parent Epic) — use --standalone, omit --epic-id:
62
+ node <main-repo>/.agents/scripts/evidence-gate.js \
63
+ --standalone --scope-id <storyId> --gate typecheck \
64
+ --worktree <worktree> -- <resolved typecheck command>
65
+ ```
66
+
67
+ Close's `shouldSkip` then short-circuits that gate when HEAD is
68
+ unchanged; a redraft round (HEAD moves) correctly busts it. **Never**
69
+ run the coverage / CRAP suite through `evidence-gate.js` to stamp it
70
+ fresh — a false-fresh coverage record without `coverage-final.json`
71
+ silently weakens the floor. Limit the evidence-share to `lint` and
72
+ `typecheck`.
46
73
  - Emits a verdict file under `temp/` conforming to
47
74
  [`acceptance-eval-verdict.schema.json`](../../schemas/acceptance-eval-verdict.schema.json):
48
75
  one `{ index, criterion, verdict: met|partial|unmet, evidence,
@@ -167,11 +167,25 @@ Validates `type::epic`, enumerates `type::story` descendants, parses
167
167
  (to enumerate the open Story set), and upserts the `epic-run-state`
168
168
  checkpoint in the per-Story-status shape (a flat `stories` map seeded at
169
169
  `pending`, plus the global `concurrencyCap`). Treat the printed JSON as
170
- `state`: `{ epicId, storyCount, concurrencyCap, stories, checkpointInitializedAt }`.
170
+ `state`: `{ epicId, storyCount, concurrencyCap, stories, prdId, techSpecId, checkpointInitializedAt }`.
171
171
  `stories` is the flat dispatch hint (`{ storyId, worktree, title }` per open
172
172
  Story); the ready-set `tick` (Phase 2) decides which to dispatch on each
173
173
  beat. Flip the Epic to `agent::executing` (idempotent) after the CLI returns.
174
174
 
175
+ **Epic linkages resolved once (Story #4253).** The envelope also carries
176
+ `prdId` and `techSpecId` — the Epic's linked PRD / Tech-Spec issue ids,
177
+ resolved a **single** time here from the Epic snapshot prepare already
178
+ holds (no extra fetch). Capture both and thread them into **every**
179
+ per-Story `story-init.js` invocation (§ 2b → `epic-deliver-story` Step 0)
180
+ as `--prd <prdId> --tech-spec <techSpecId>`. This collapses the N
181
+ per-Story `getEpic` round-trips (one per child, each in its own process
182
+ with its own provider cache) to this one parent-side resolution — the
183
+ immutable Epic issue is invariant for the lifetime of a delivery run.
184
+ When a linkage is `null` (the Epic links no PRD or Tech Spec), omit the
185
+ corresponding flag; the child's `story-init.js` then falls back to its
186
+ own `getEpic` resolution for the missing id, preserving graceful
187
+ degradation.
188
+
175
189
  > **Preflight guards (Story #3482 / F-workflow-guards).** Before the
176
190
  > snapshot phase runs — and before any worktree is created — prepare runs
177
191
  > two **fail-closed** guards
@@ -334,7 +348,10 @@ matching `story.dispatch.end` record is appended later by
334
348
  `epic-execute-record-wave.js` (via `emit-story-dispatch-end.js`, Story #3900)
335
349
  after the Agent return is recorded in § 2c.
336
350
 
337
- Each Agent call's prompt must (1) name the Story + Epic ids, (2)
351
+ Each Agent call's prompt must (1) name the Story + Epic ids **and the
352
+ `prdId` / `techSpecId` from the Phase 1 prepare envelope** (Story #4253) so
353
+ the child can thread `--prd <prdId> --tech-spec <techSpecId>` into its
354
+ `story-init.js` Step 0 — omit whichever flag is `null`, (2)
338
355
  instruct the child to invoke `helpers/epic-deliver-story <storyId>`
339
356
  (whose Step 4 defines the child's return shape), (3) remind the child
340
357
  of the **non-interactive contract** (no clarifying questions;
@@ -77,9 +77,22 @@ the parent's permissions but have **no input channel** mid-run.
77
77
  Run from the **main checkout** (the worktree does not exist yet):
78
78
 
79
79
  ```bash
80
- node .agents/scripts/story-init.js --story <storyId>
80
+ node .agents/scripts/story-init.js --story <storyId> \
81
+ --prd <prdId> --tech-spec <techSpecId>
81
82
  ```
82
83
 
84
+ **Thread the Epic linkages (Story #4253).** The parent `/deliver` resolved
85
+ the Epic's `prdId` / `techSpecId` **once** in its Phase 1 prepare and passed
86
+ them into your dispatch prompt. Forward them as `--prd` / `--tech-spec` so
87
+ this `story-init.js` run **skips** the per-Story `getEpic` round-trip — the
88
+ two ids are invariant for the whole delivery run, so re-fetching the
89
+ immutable Epic per Story is pure waste (and secondary-rate-limit pressure
90
+ during wide fan-out). **Omit** whichever flag the prompt reported as `null`;
91
+ `story-init.js` then falls back to its own `getEpic` resolution for the
92
+ missing id (graceful degradation on a missing Epic linkage). When dispatched
93
+ interactively with neither flag, drop both — the legacy single-fetch path is
94
+ unchanged.
95
+
83
96
  > **Execution mode (sub-agents must read).** This command typically takes
84
97
  > 3–6 minutes when the worktree's per-tree install runs. Invoke it
85
98
  > **synchronously** with the Bash tool's maximum timeout
@@ -139,11 +152,15 @@ the Story-level rollup the parent `/deliver` aggregator reads).
139
152
  Run a single Story-implementation phase against the inline `acceptance[]`
140
153
  / `verify[]` arrays on the Story body.
141
154
 
142
- 1. Flip the snapshot to the `implementing` phase:
155
+ 1. Flip the snapshot to the `implementing` phase. Pass `--epic <epicId>`
156
+ and `--branch story-<storyId>` from the Step 0 envelope so the render
157
+ skips the `readEpicIdFromStory` / `resolveStoryBranch` GitHub reads
158
+ (pass these same flags to **every** `story-phase.js` call below):
143
159
 
144
160
  ```bash
145
161
  node .agents/scripts/story-phase.js \
146
- --story <storyId> --phase implementing
162
+ --story <storyId> --epic <epicId> --branch story-<storyId> \
163
+ --phase implementing
147
164
  ```
148
165
 
149
166
  2. Read the Story body's inline `acceptance[]` and `verify[]` arrays
@@ -166,11 +183,14 @@ Run a single Story-implementation phase against the inline `acceptance[]`
166
183
  evidence** — they are no longer optional advisory pre-flight.
167
184
 
168
185
  5. Once the eval loop returns `proceed`, flip the snapshot to `closing`
169
- and proceed to Step 3:
186
+ and proceed to Step 3 (Step 3 invokes close directly — it no longer
187
+ re-renders the `closing` snapshot, so this is the single `closing`
188
+ render):
170
189
 
171
190
  ```bash
172
191
  node .agents/scripts/story-phase.js \
173
- --story <storyId> --phase closing
192
+ --story <storyId> --epic <epicId> --branch story-<storyId> \
193
+ --phase closing
174
194
  ```
175
195
 
176
196
  6. If blocked (including by the eval loop reaching its round cap with
@@ -180,7 +200,8 @@ Run a single Story-implementation phase against the inline `acceptance[]`
180
200
 
181
201
  ```bash
182
202
  node .agents/scripts/story-phase.js \
183
- --story <storyId> --phase blocked
203
+ --story <storyId> --epic <epicId> --branch story-<storyId> \
204
+ --phase blocked
184
205
  ```
185
206
 
186
207
  ### Step 1a — Bounded acceptance self-eval loop (**required, not optional**)
@@ -195,6 +216,18 @@ The critic reads the Story's inline `acceptance[]` / `verify[]` arrays from the
195
216
 
196
217
  Epic-attached specifics for this path:
197
218
 
219
+ - **Critic evidence-share** (Story #4250). When the critic runs a `verify[]`
220
+ command that is byte-identical to a close gate (`lint` / `typecheck`), it
221
+ records the pass into the Epic-keyed evidence keyspace via `--epic-id
222
+ <epicId>` so `story-close.js` short-circuits the gate at unchanged HEAD.
223
+ Run it in the **Story worktree** (`.worktrees/story-<storyId>`):
224
+
225
+ ```bash
226
+ node .agents/scripts/evidence-gate.js \
227
+ --epic-id <epicId> --scope-id <storyId> --gate lint \
228
+ --worktree .worktrees/story-<storyId> -- npm run lint
229
+ ```
230
+
198
231
  - **Gate invocation** (pass `--epic <epicId>` so the per-criterion signal lands
199
232
  on the Epic-scoped stream):
200
233
 
@@ -235,16 +268,18 @@ fine as advisory pre-flight.)
235
268
 
236
269
  ## Step 3 — Close (`story-close.js`)
237
270
 
238
- Flip the snapshot to the closing phase, then invoke close. Pass the
239
- main-checkout path via `--cwd` so the merge and branch deletion run
240
- against the main repo (branches checked out in a worktree cannot be
271
+ Step 1 item 5 already flipped the snapshot to the `closing` phase, so this
272
+ step does **not** re-render it (the duplicate render was removed). Invoke
273
+ close directly. Pass the parent Epic id via `--epic <epicId>` from the
274
+ Step 0 envelope so close skips re-parsing the Epic hierarchy off the
275
+ Story body (which also closes the malformed-`Epic:`-line failure mode).
276
+ Pass the main-checkout path via `--cwd` so the merge and branch deletion
277
+ run against the main repo (branches checked out in a worktree cannot be
241
278
  deleted from themselves):
242
279
 
243
280
  ```bash
244
- node .agents/scripts/story-phase.js \
245
- --story <storyId> --phase closing
246
-
247
- node <main-repo>/.agents/scripts/story-close.js --story <storyId> --cwd <main-repo>
281
+ node <main-repo>/.agents/scripts/story-close.js \
282
+ --story <storyId> --epic <epicId> --cwd <main-repo>
248
283
  ```
249
284
 
250
285
  In single-tree mode, `--cwd` defaults to `PROJECT_ROOT`. The script merges
@@ -261,7 +296,8 @@ After close, upsert a terminal snapshot:
261
296
 
262
297
  ```bash
263
298
  node .agents/scripts/story-phase.js \
264
- --story <storyId> --phase done
299
+ --story <storyId> --epic <epicId> --branch story-<storyId> \
300
+ --phase done
265
301
  ```
266
302
 
267
303
  ---
@@ -215,6 +215,18 @@ proceed / redraft / block decision) is the single-homed include
215
215
 
216
216
  Standalone specifics for this path:
217
217
 
218
+ - **Critic evidence-share** (Story #4250). When the critic runs a `verify[]`
219
+ command that is byte-identical to a close gate (`lint` / `typecheck`), it
220
+ records the pass into the standalone evidence keyspace via `--standalone`
221
+ (no parent Epic to key on) so Step 3's close short-circuits the gate at
222
+ unchanged HEAD. Run it in the **Story worktree** (`workCwd` from Step 0):
223
+
224
+ ```bash
225
+ node <main-repo>/.agents/scripts/evidence-gate.js \
226
+ --standalone --scope-id <storyId> --gate lint \
227
+ --worktree <workCwd> -- npm run lint
228
+ ```
229
+
218
230
  - **Gate invocation** (omit `--epic` — there is no parent Epic):
219
231
 
220
232
  ```bash
@@ -0,0 +1,65 @@
1
+ # Loop units (`.agents/workflows/loops/`)
2
+
3
+ A **loop unit** is a markdown file that defines one unit of *recurring* work
4
+ with a checkable definition of done. Each file's leading YAML frontmatter
5
+ carries a `loop:` block — a cadence, a goal, an optional `verify` oracle, a
6
+ round cap, and an exhaustion policy — validated against
7
+ [`.agents/schemas/loop-unit.schema.json`](../../schemas/loop-unit.schema.json)
8
+ by `node .agents/scripts/check-loop-units.js` (wired into `npm run lint`).
9
+
10
+ This directory is the **one** namespaced exception to the flat slash-command
11
+ projection. Files here project to `.claude/commands/loops/<name>.md` and are
12
+ invoked as the namespaced `/loops:<name>` command (flat fallback
13
+ `/loops-<name>` on hosts that flatten subdirectory commands). Every other
14
+ top-level workflow projects flat as `/<name>`; `helpers/` is not projected at
15
+ all.
16
+
17
+ ## What a loop unit is — and is not
18
+
19
+ A loop unit ships **content and contract**, not a runner. It declares:
20
+
21
+ - **the action** — what one round does;
22
+ - **the goal** — the standing objective each round works toward;
23
+ - **the `verify` oracle** — the runnable check that proves a round is complete
24
+ (required for `self-paced` cadence, optional for `interval` / `cron`); and
25
+ - **the observability / escalation contract** — the `maxRounds` backstop, the
26
+ `onExhaust` policy, and the explicit "stop & escalate" conditions in the body.
27
+
28
+ It does **not** ship the loop driver. **Cadence and iteration are owned by the
29
+ host** — Claude Code's built-in `/loop` (self-paced or interval) and
30
+ `/schedule` (cron). Mandrel deliberately ships **no** `/goal` or `/loop`
31
+ runner of its own. The full rationale, and why this division exists, is fixed
32
+ in the ADR:
33
+
34
+ > [`docs/decisions/loop-units-division-of-labor.md`](../../../docs/decisions/loop-units-division-of-labor.md)
35
+ > — *Loop units: mandrel owns content + oracle + contract; the host owns
36
+ > cadence + iteration; no runner shipped.*
37
+
38
+ Read that ADR before adding a runner, a scheduler, or a `/goal` command to the
39
+ framework — the decision to **not** build one is deliberate.
40
+
41
+ ## Cadence → host mapping
42
+
43
+ | Cadence | `verify` | Driven by | Starter unit |
44
+ | ------------- | -------- | --------------------------------- | -------------------------------------------------------------- |
45
+ | `self-paced` | required | `/loop` (no interval) | [`fix-failing-tests.md`](fix-failing-tests.md) — red → green |
46
+ | `interval` | optional | `/loop <interval>` (e.g. `/loop 5m`) | [`watch-ci.md`](watch-ci.md) — poll a PR's checks |
47
+ | `cron` | optional | `/schedule` (cron-driven) | [`nightly-audit.md`](nightly-audit.md) — nightly audit sweep |
48
+
49
+ A `self-paced` unit **must** carry a `verify` oracle because nothing external
50
+ paces it — the oracle is the only signal that tells the host when to stop.
51
+ `interval` and `cron` units are paced by an external scheduler, so a
52
+ terminating oracle is optional; they observe, report, and yield each tick.
53
+
54
+ ## Authoring a new loop unit
55
+
56
+ 1. Create `.agents/workflows/loops/<name>.md` with a `loop:` frontmatter block
57
+ (`cadence` + `goal` required; add `verify` for `self-paced`).
58
+ 2. Give it a `description:` so it shows up in the generated catalog
59
+ ([`.agents/docs/workflows.md`](../../docs/workflows.md), **Loops namespace**).
60
+ 3. Body sections: **Action** (what one round does), **Goal & done-signal** (the
61
+ objective and the oracle/stop check), **Stop & escalate** (when to hand back
62
+ rather than loop).
63
+ 4. Run `node .agents/scripts/check-loop-units.js` (or `npm run lint`) to
64
+ validate the frontmatter, then `npm run sync:commands` to project it to
65
+ `/loops:<name>` and `npm run docs:gen` to refresh the catalog.
@@ -0,0 +1,74 @@
1
+ ---
2
+ description: >-
3
+ Self-paced convergence loop that drives a red test suite to green. Each round
4
+ reads the latest failure, applies the smallest fix, and re-runs the verify
5
+ oracle (`npm test`); the loop terminates when the oracle exits 0. The host
6
+ (`/loop`) owns iteration and pacing — mandrel supplies the action, the goal,
7
+ and the terminating oracle.
8
+ loop:
9
+ cadence: self-paced
10
+ goal: >-
11
+ Drive the project's test suite from red to green by fixing the root cause of
12
+ each failure, one round at a time, until the verify oracle passes.
13
+ verify: npm test
14
+ maxRounds: 10
15
+ onExhaust: hand-back
16
+ ---
17
+
18
+ # /loops:fix-failing-tests — drive a red suite to green
19
+
20
+ A **self-paced convergence loop**. The host (`/loop` with no interval) decides
21
+ when to run the next round; this unit supplies the action each round performs,
22
+ the standing goal, and the runnable `verify` oracle that tells the host when to
23
+ stop. When `npm test` exits 0, the goal is met and the loop terminates.
24
+
25
+ > **Scope.** This loop fixes the **root cause** of failing tests. It does not
26
+ > delete, skip, `.only`, or weaken assertions to force a green bar — that is an
27
+ > escalation condition, not a round (see **Stop & escalate** below).
28
+
29
+ ## Action
30
+
31
+ Each round:
32
+
33
+ 1. **Read the latest failure.** Run the verify oracle (`npm test`) and read the
34
+ first failing assertion — name, file, and the expected-vs-actual diff. Fix
35
+ one failure cluster per round; do not fan out across unrelated failures in a
36
+ single round.
37
+ 2. **Diagnose the root cause.** Decide whether the failure is in the production
38
+ code under test or in the test's own setup/expectation. Prefer the
39
+ smallest change that makes the assertion honest — fix the code when the test
40
+ encodes the intended contract; fix the test only when it asserts the wrong
41
+ thing and you can state why in one sentence.
42
+ 3. **Apply the smallest fix.** Make the minimal edit that addresses the
43
+ diagnosed cause. Avoid speculative refactors — convergence depends on each
44
+ round changing exactly one thing.
45
+ 4. **Re-run the oracle.** Run `npm test` again. A reduced failure count is
46
+ progress; a new failure introduced by the fix means the diagnosis was wrong
47
+ — revert and re-diagnose rather than stacking another fix on top.
48
+
49
+ ## Goal & done-signal
50
+
51
+ - **Goal:** the test suite passes — every test green, no skipped-to-hide
52
+ failures.
53
+ - **Done-signal (the oracle):** `npm test` exits 0. This is the single
54
+ terminating check the host `/loop` evaluates after each round. When it
55
+ passes, stop — the loop is complete.
56
+ - **Backstop:** `maxRounds: 10`. If the oracle is still red after ten rounds,
57
+ the `onExhaust: hand-back` policy returns control to the caller with a
58
+ summary rather than looping indefinitely.
59
+
60
+ ## Stop & escalate
61
+
62
+ Stop the loop and hand back (do **not** keep iterating) when:
63
+
64
+ - **The same failure survives the same class of fix twice.** Per the
65
+ anti-thrashing protocol, a repeated fix against an unchanged failure means
66
+ the diagnosis is wrong — stop and report what you tried.
67
+ - **A fix would weaken the contract.** If the only way to make the bar green is
68
+ to delete a test, add `.skip` / `.only`, or relax an assertion to match buggy
69
+ behaviour, that is a product decision, not a loop round. Stop and surface it.
70
+ - **The failure is environmental, not a code defect** (missing service, absent
71
+ credential, a flaky timing-dependent test). The loop cannot converge on an
72
+ external cause — report the blocker so the operator can resolve it.
73
+ - **`maxRounds` is reached with the oracle still red.** Hand back a summary of
74
+ the remaining failures and the rounds spent.
@@ -0,0 +1,71 @@
1
+ ---
2
+ description: >-
3
+ Cron maintenance loop that runs a nightly audit sweep over the repository and
4
+ files actionable findings. Each run executes the audit workflows and routes
5
+ the results; the host (`/schedule` or a cron-driven `/loop`) owns the cadence.
6
+ verify is optional for a cron loop — the scheduler owns iteration, so this
7
+ unit ships the action and goal, not a terminating oracle.
8
+ loop:
9
+ cadence: cron
10
+ goal: >-
11
+ Keep the repository's standing health surfaced by running the audit sweep on
12
+ a nightly schedule and turning each fresh finding into an actionable, deduped
13
+ record so regressions are caught within a day rather than at release time.
14
+ maxRounds: 30
15
+ onExhaust: report
16
+ ---
17
+
18
+ # /loops:nightly-audit — scheduled maintenance audit sweep
19
+
20
+ A **cron maintenance loop**. The host (`/schedule`, or a cron-driven `/loop`)
21
+ owns the cadence and fires this unit once per scheduled window — typically
22
+ overnight. Because the scheduler owns iteration, this unit carries **no
23
+ `verify` oracle**: per the loop-unit schema, `verify` is required only for
24
+ `self-paced` cadence and optional for `interval` / `cron`. Each run is a single
25
+ sweep that observes, records, and yields until the next scheduled tick.
26
+
27
+ ## Action
28
+
29
+ Each scheduled run:
30
+
31
+ 1. **Run the audit sweep.** Execute the relevant audit workflows for the repo
32
+ (`/audit-security`, `/audit-clean-code`, `/audit-dependencies`,
33
+ `/audit-quality`, and any others the project relies on). Each audit writes a
34
+ structured `temp/audits/audit-*-results.md` report — that is the canonical
35
+ artifact this loop consumes, not free-form prose.
36
+ 2. **Diff against the prior night.** Compare the fresh findings against the last
37
+ sweep's reports and against already-open Issues. A finding seen before is
38
+ not new signal; only genuinely fresh or regressed findings warrant a record.
39
+ 3. **Route fresh findings.** Hand the new findings to `/audit-to-stories`, which
40
+ deduplicates against existing Issues by fingerprint and either chains into
41
+ `/plan` or opens standalone Stories. Do not open raw duplicate Issues —
42
+ dedup is the loop's job, not the operator's.
43
+ 4. **Report and yield.** Emit a short digest (sweeps run, new findings, Issues
44
+ opened or updated) and return control to the scheduler, which sleeps until
45
+ the next cron window.
46
+
47
+ ## Goal & done-signal
48
+
49
+ - **Goal:** the repository's health regressions are caught and turned into
50
+ actionable, deduplicated records within a day, without a human remembering to
51
+ run the audits by hand.
52
+ - **Done-signal:** the nightly sweep completed and every fresh finding has been
53
+ routed to a record (or explicitly judged a non-finding). A cron loop has no
54
+ self-evaluated oracle — the scheduler owns whether the loop runs again; this
55
+ unit simply finishes the night's sweep and yields.
56
+ - **Backstop:** `maxRounds: 30`. Roughly a month of nightly runs;
57
+ `onExhaust: report` emits a final digest and stops so a long-lived schedule
58
+ is renewed deliberately rather than running unbounded.
59
+
60
+ ## Stop & escalate
61
+
62
+ - **An audit cannot run** (a required tool is missing, the audit harness errors,
63
+ the working tree is dirty in a way that invalidates the sweep). Report the
64
+ failure for that audit and continue with the others — do not abort the whole
65
+ night because one audit broke.
66
+ - **A finding is high-severity and time-sensitive** (an exposed secret, a
67
+ critical CVE reachable in production). Surface it loudly in the digest rather
68
+ than letting it sit as one row among many — a nightly cadence is too slow for
69
+ an actively-exploitable finding.
70
+ - **`maxRounds` is reached.** Emit a final digest (`onExhaust: report`) so the
71
+ operator can renew or retire the schedule deliberately.