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.
- package/.agents/docs/configuration.md +2 -2
- package/.agents/docs/workflows.md +19 -0
- package/.agents/schemas/agentrc.schema.json +1 -1
- package/.agents/schemas/dispatch-manifest.json +1 -1
- package/.agents/schemas/lifecycle/loop.tick.schema.json +20 -0
- package/.agents/schemas/loop-unit.schema.json +70 -0
- package/.agents/schemas/validation-evidence.schema.json +2 -1
- package/.agents/scripts/audit-to-stories.js +43 -1
- package/.agents/scripts/check-doc-links.js +24 -1
- package/.agents/scripts/check-loop-units.js +204 -0
- package/.agents/scripts/epic-deliver-prepare.js +31 -0
- package/.agents/scripts/evidence-gate.js +48 -12
- package/.agents/scripts/generate-workflows-doc.js +37 -4
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +141 -34
- package/.agents/scripts/lib/cli-args.js +6 -0
- package/.agents/scripts/lib/close-validation/process.js +61 -5
- package/.agents/scripts/lib/close-validation/runner.js +42 -9
- package/.agents/scripts/lib/config/temp-paths.js +1 -1
- package/.agents/scripts/lib/config/worktree-isolation.js +18 -3
- package/.agents/scripts/lib/config-resolver.js +4 -1
- package/.agents/scripts/lib/config-settings-schema-delivery.js +1 -1
- package/.agents/scripts/lib/git-branch-lifecycle.js +90 -0
- package/.agents/scripts/lib/loop-units/validate-loop-unit.js +197 -0
- package/.agents/scripts/lib/mandrel-catalog.js +36 -0
- package/.agents/scripts/lib/orchestration/auto-merge-cwd.js +128 -0
- package/.agents/scripts/lib/orchestration/column-sync.js +88 -9
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +183 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +20 -2
- package/.agents/scripts/lib/orchestration/project-meta-cache.js +238 -0
- package/.agents/scripts/lib/orchestration/reassert-status-column.js +3 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +25 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +80 -14
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +74 -25
- package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +10 -1
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +48 -1
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +148 -4
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +8 -1
- package/.agents/scripts/lib/story-body/story-body.js +76 -7
- package/.agents/scripts/lib/story-init/branch-initializer.js +29 -43
- package/.agents/scripts/lib/story-init/hierarchy-tracer.js +25 -4
- package/.agents/scripts/lib/story-init/task-graph-builder.js +22 -12
- package/.agents/scripts/lib/templates/decomposer-prompts.js +23 -0
- package/.agents/scripts/lib/validation-evidence.js +63 -25
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +239 -31
- package/.agents/scripts/providers/github/branch-protection.js +1 -1
- package/.agents/scripts/providers/github/errors.js +53 -2
- package/.agents/scripts/providers/github/labels.js +1 -1
- package/.agents/scripts/providers/github/projects-v2-graphql.js +1 -1
- package/.agents/scripts/resync-status-column.js +5 -0
- package/.agents/scripts/run-coverage.js +85 -45
- package/.agents/scripts/run-lint.js +11 -0
- package/.agents/scripts/single-story-init.js +22 -29
- package/.agents/scripts/story-init.js +38 -63
- package/.agents/scripts/story-phase.js +46 -4
- package/.agents/scripts/sync-claude-commands.js +112 -29
- package/.agents/scripts/update-maintainability-baseline.js +19 -76
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +5 -3
- package/.agents/workflows/helpers/acceptance-self-eval.md +27 -0
- package/.agents/workflows/helpers/deliver-epic.md +19 -2
- package/.agents/workflows/helpers/epic-deliver-story.md +50 -14
- package/.agents/workflows/helpers/single-story-deliver.md +12 -0
- package/.agents/workflows/loops/README.md +65 -0
- package/.agents/workflows/loops/fix-failing-tests.md +74 -0
- package/.agents/workflows/loops/nightly-audit.md +71 -0
- package/.agents/workflows/loops/watch-ci.md +68 -0
- package/docs/CHANGELOG.md +51 -0
- package/package.json +1 -1
- 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.
|
|
26
|
-
*
|
|
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
|
-
|
|
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
|
-
-
|
|
272
|
-
-
|
|
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
|
|
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> --
|
|
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> --
|
|
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> --
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
245
|
-
--story <storyId> --
|
|
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> --
|
|
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.
|