mandrel 2.10.0 → 2.11.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 +35 -33
- package/.agents/schemas/agentrc.schema.json +13 -8
- package/.agents/scripts/acceptance-eval.js +9 -5
- package/.agents/scripts/lib/baselines/env-overrides.js +33 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/preview-gates.js +5 -0
- package/.agents/scripts/lib/config/gates/maintainability.schema.js +10 -1
- package/.agents/scripts/lib/config/quality.js +13 -0
- package/.agents/scripts/lib/config-settings-schema.js +12 -16
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +45 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +97 -4
- package/.agents/scripts/lib/orchestration/check-baselines/phases/parse-args.js +7 -0
- package/.agents/scripts/lib/orchestration/complexity-gate.js +509 -180
- package/.agents/scripts/lib/orchestration/plan-context.js +69 -10
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +111 -60
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +21 -15
- package/.agents/scripts/lib/orchestration/resolve-stories.js +11 -7
- package/.agents/scripts/lib/orchestration/review-depth.js +9 -4
- package/.agents/scripts/lib/orchestration/spec-budget.js +78 -0
- package/.agents/scripts/lib/orchestration/story-body-gate.js +72 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +6 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +18 -62
- package/.agents/scripts/plan-context.js +23 -5
- package/.agents/scripts/resolve-stories.js +2 -0
- package/.agents/workflows/deliver.md +2 -0
- package/.agents/workflows/helpers/acceptance-self-eval.md +16 -5
- package/.agents/workflows/helpers/deliver-reference.md +9 -5
- package/.agents/workflows/helpers/deliver-story-reference.md +32 -12
- package/.agents/workflows/helpers/deliver-story.md +4 -3
- package/.agents/workflows/helpers/plan-reference.md +79 -44
- package/.agents/workflows/plan.md +11 -10
- package/docs/CHANGELOG.md +19 -0
- package/lib/cli/registry.js +31 -14
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +92 -0
- package/package.json +1 -1
|
@@ -3,11 +3,12 @@ import { detectCycle } from '../Graph.js';
|
|
|
3
3
|
import { gitSpawn } from '../git-utils.js';
|
|
4
4
|
|
|
5
5
|
import { Logger } from '../Logger.js';
|
|
6
|
-
import {
|
|
7
|
-
parse as parseStoryBody,
|
|
8
|
-
StoryBodyParseError,
|
|
9
|
-
} from '../story-body/story-body.js';
|
|
10
6
|
import { validateStoryFileAssumptions } from './file-assumptions.js';
|
|
7
|
+
import { computeSpecBudgetFindings } from './spec-budget.js';
|
|
8
|
+
import {
|
|
9
|
+
assertStoryBodiesParse,
|
|
10
|
+
parseStoryBodyOrThrow,
|
|
11
|
+
} from './story-body-gate.js';
|
|
11
12
|
import {
|
|
12
13
|
computeConflictFindings,
|
|
13
14
|
renderHardConflictError,
|
|
@@ -46,63 +47,6 @@ function collectPathsFromText(text, paths) {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
/**
|
|
50
|
-
* Parse a Story's serialized markdown body, translating a
|
|
51
|
-
* `StoryBodyParseError` into a `ValidationError` that names the offending
|
|
52
|
-
* **section** and **entry** (Story #4541).
|
|
53
|
-
*
|
|
54
|
-
* `StoryBodyParseError` already carries `field` (the section the parser was
|
|
55
|
-
* reading) and `raw` (the entry text that failed); this lifts both into an
|
|
56
|
-
* operator-legible message and a structured `violation` payload so an
|
|
57
|
-
* authoring loop can point at the exact bullet instead of re-deriving it
|
|
58
|
-
* from a downstream freshness miss.
|
|
59
|
-
*
|
|
60
|
-
* @param {object} story Story whose `body` is a non-empty markdown string.
|
|
61
|
-
* @returns {object} The structured body.
|
|
62
|
-
* @throws {ValidationError} `code: 'story-body-unparseable'`.
|
|
63
|
-
*/
|
|
64
|
-
function parseStoryBodyOrThrow(story) {
|
|
65
|
-
try {
|
|
66
|
-
return parseStoryBody(story.body).body;
|
|
67
|
-
} catch (err) {
|
|
68
|
-
if (!(err instanceof StoryBodyParseError)) throw err;
|
|
69
|
-
const slug = story.slug ?? '<unknown>';
|
|
70
|
-
const section = err.field ?? 'body';
|
|
71
|
-
const entry = err.raw ?? null;
|
|
72
|
-
const entryLine = entry === null ? '' : `\n entry: ${entry}`;
|
|
73
|
-
const violation = { slug, section, entry, reason: err.message };
|
|
74
|
-
const error = new ValidationError(
|
|
75
|
-
`Cross-Validation Failed: Story "${slug}" has an unparseable body — ` +
|
|
76
|
-
`the ## ${section} section could not be read: ${err.message}` +
|
|
77
|
-
`${entryLine}\n\nFix the offending entry; this is a malformed body, ` +
|
|
78
|
-
'not a stale path reference.',
|
|
79
|
-
{ violations: [violation] },
|
|
80
|
-
);
|
|
81
|
-
error.code = 'story-body-unparseable';
|
|
82
|
-
error.violations = [violation];
|
|
83
|
-
throw error;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Refuse the plan when any Story's serialized body cannot be parsed, before
|
|
89
|
-
* either git-probe gate runs (Story #4541). Ordering matters: the freshness
|
|
90
|
-
* gate consults `body.changes` for its net-new whitelist, so an unparseable
|
|
91
|
-
* body used to reach the operator as a freshness miss naming declared paths.
|
|
92
|
-
*
|
|
93
|
-
* @param {{ tickets: object[] }} opts
|
|
94
|
-
* @throws {ValidationError} `code: 'story-body-unparseable'` on the first
|
|
95
|
-
* offending Story.
|
|
96
|
-
*/
|
|
97
|
-
function assertStoryBodiesParse({ tickets }) {
|
|
98
|
-
for (const story of (tickets ?? []).filter((t) => t.type === 'story')) {
|
|
99
|
-
if (typeof story.body !== 'string' || story.body.trim().length === 0) {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
parseStoryBodyOrThrow(story);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
50
|
/**
|
|
107
51
|
* Resolve every acceptance line a Story declares, across both authoring
|
|
108
52
|
* shapes (Story #4541).
|
|
@@ -721,7 +665,19 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
|
|
|
721
665
|
stories,
|
|
722
666
|
policy: opts.conflictPolicy,
|
|
723
667
|
});
|
|
724
|
-
|
|
668
|
+
// Advisory `## Spec` word-budget pass (Story #4723) — soft findings only,
|
|
669
|
+
// surfaced as warnings here and via the persist soft-finding channel;
|
|
670
|
+
// never promoted to `errors[]`, so an over-budget Spec cannot fail the
|
|
671
|
+
// persist. Runs after `assertStoryBodiesParse`, so string bodies parse.
|
|
672
|
+
const specBudgetFindings = computeSpecBudgetFindings({ stories });
|
|
673
|
+
for (const finding of specBudgetFindings) {
|
|
674
|
+
Logger.warn(`[ticket-validator] spec-word-budget: ${finding.message}`);
|
|
675
|
+
}
|
|
676
|
+
const findings = [
|
|
677
|
+
...sizingFindings,
|
|
678
|
+
...conflictFindings,
|
|
679
|
+
...specBudgetFindings,
|
|
680
|
+
];
|
|
725
681
|
const CONFLICT_KINDS = new Set([
|
|
726
682
|
'shared-editor',
|
|
727
683
|
'implicit-cross-story-dep',
|
|
@@ -124,7 +124,7 @@ export async function emitPlanContext({
|
|
|
124
124
|
// later turn. When it is captured to disk anyway, stdout carries a
|
|
125
125
|
// compact digest naming the artifact instead of the payload itself.
|
|
126
126
|
await writeEnvelopeFile(outPath, json);
|
|
127
|
-
await writeStoriesTemplateFile(outPath);
|
|
127
|
+
await writeStoriesTemplateFile(outPath, envelope);
|
|
128
128
|
const resolved = path.resolve(outPath);
|
|
129
129
|
const digest = {
|
|
130
130
|
digest: 'plan-context',
|
|
@@ -137,7 +137,16 @@ export async function emitPlanContext({
|
|
|
137
137
|
bytes: Buffer.byteLength(json, 'utf8'),
|
|
138
138
|
sourceTickets: (envelope.sourceTickets ?? []).map((t) => t.id),
|
|
139
139
|
duplicates: (envelope.duplicates ?? []).length,
|
|
140
|
-
|
|
140
|
+
// Advisory only (Story #4722): signals, no route — the planner owns
|
|
141
|
+
// the trivial-vs-standard verdict and persist validates it by shape.
|
|
142
|
+
complexitySignals: envelope.complexitySignals
|
|
143
|
+
? {
|
|
144
|
+
artifactCount: envelope.complexitySignals.artifactCount,
|
|
145
|
+
riskHeuristicHits: envelope.complexitySignals.riskHeuristicHits,
|
|
146
|
+
sensitivePathClasses:
|
|
147
|
+
envelope.complexitySignals.sensitivePathClasses,
|
|
148
|
+
}
|
|
149
|
+
: null,
|
|
141
150
|
};
|
|
142
151
|
stdout.write(`${JSON.stringify(digest)}\n`);
|
|
143
152
|
} else {
|
|
@@ -177,18 +186,27 @@ async function writeEnvelopeFile(outPath, json) {
|
|
|
177
186
|
* requires reading `story-body.js` source. Written whenever `--out` is
|
|
178
187
|
* passed, and throwing on failure for the same reason the envelope write
|
|
179
188
|
* does: a silently missing template re-opens the format-discovery loop it
|
|
180
|
-
* exists to close.
|
|
189
|
+
* exists to close. The envelope's advisory `complexitySignals` are threaded
|
|
190
|
+
* through so the skeleton's `changes[]` arrive pre-resolved to
|
|
191
|
+
* creates-vs-refactors against the repo snapshot (Story #4723).
|
|
181
192
|
*
|
|
182
193
|
* @param {string} outPath The envelope `--out` path; the template lands in
|
|
183
194
|
* the same directory as {@link STORIES_TEMPLATE_FILENAME}.
|
|
195
|
+
* @param {object} [envelope] The emitted plan-context envelope.
|
|
184
196
|
*/
|
|
185
|
-
async function writeStoriesTemplateFile(outPath) {
|
|
197
|
+
async function writeStoriesTemplateFile(outPath, envelope = {}) {
|
|
186
198
|
const resolved = path.resolve(
|
|
187
199
|
path.dirname(path.resolve(outPath)),
|
|
188
200
|
STORIES_TEMPLATE_FILENAME,
|
|
189
201
|
);
|
|
190
202
|
try {
|
|
191
|
-
await writeFile(
|
|
203
|
+
await writeFile(
|
|
204
|
+
resolved,
|
|
205
|
+
renderStoriesTemplate({
|
|
206
|
+
complexitySignals: envelope?.complexitySignals ?? null,
|
|
207
|
+
}),
|
|
208
|
+
'utf8',
|
|
209
|
+
);
|
|
192
210
|
} catch (err) {
|
|
193
211
|
throw new Error(
|
|
194
212
|
`[plan-context] cannot write stories template to ${resolved}: ${err.message}`,
|
|
@@ -209,6 +209,7 @@ async function main() {
|
|
|
209
209
|
stories,
|
|
210
210
|
nativeEdges,
|
|
211
211
|
warn: (m) => Logger.warn(m),
|
|
212
|
+
config,
|
|
212
213
|
});
|
|
213
214
|
const foreignDone = await resolveForeignDone({
|
|
214
215
|
provider,
|
|
@@ -220,6 +221,7 @@ async function main() {
|
|
|
220
221
|
nativeEdges,
|
|
221
222
|
foreignDone,
|
|
222
223
|
warn: () => {},
|
|
224
|
+
config,
|
|
223
225
|
});
|
|
224
226
|
|
|
225
227
|
process.stdout.write(
|
|
@@ -25,6 +25,8 @@ blocker resolved against its real issue state). You never hand it a graph, and
|
|
|
25
25
|
there is no batch label — which is what lets you deliver Stories **across plan
|
|
26
26
|
runs and over time**. The `plan-run::<id>` grouping label is filter metadata
|
|
27
27
|
only — never a resolution input (there is no `--run` or `--dep` axis).
|
|
28
|
+
Per-Story routes are **body-derived** too (#4722); `route::lite` is a hint
|
|
29
|
+
only.
|
|
28
30
|
|
|
29
31
|
## Inputs
|
|
30
32
|
|
|
@@ -30,10 +30,19 @@ mid-delivery, and evaluates the actual work product.
|
|
|
30
30
|
|
|
31
31
|
## Per round
|
|
32
32
|
|
|
33
|
-
1. **Eval pass
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
1. **Eval pass — one verdict-owner per cluster (Story #4723).** Exactly
|
|
34
|
+
**one** pass authors each cluster's verdict: the **fresh-context critic**
|
|
35
|
+
when the ceremony routing below resolves `fresh` (a sub-agent via the
|
|
36
|
+
`Agent` tool, *not* a continuation of your implementing turn — the
|
|
37
|
+
evaluator does not grade its own homework), or the **inline self-eval**
|
|
38
|
+
when it resolves `inline`. The resolved decision names the owner
|
|
39
|
+
explicitly (`verdictOwner: 'fresh-critic' | 'inline-self-eval'` from
|
|
40
|
+
`resolveCeremonyForRisk`). **Never run both**, and never run a
|
|
41
|
+
preliminary self-assessment pass before dispatching the fresh critic —
|
|
42
|
+
the redundant pre-pass buys no measurable quality and roughly triples
|
|
43
|
+
the acceptance-block cost. Step 2's gate is the deterministic **scorer**
|
|
44
|
+
of the one authored verdict, not a second (or third) pass over the
|
|
45
|
+
criteria.
|
|
37
46
|
|
|
38
47
|
> **Sub-agent type + derived-level ceremony (Epic #4478, M7-B).** When
|
|
39
48
|
> `delivery.routing.roleScopedAgents` is enabled (the **default**), dispatch
|
|
@@ -129,7 +138,9 @@ mid-delivery, and evaluates the actual work product.
|
|
|
129
138
|
one `{ index, criterion, verdict: met|partial|unmet, evidence,
|
|
130
139
|
verifyEvidence[] }` record per acceptance item.
|
|
131
140
|
2. **Decide.** Run the gate against the verdict (the caller's Step 1a names the
|
|
132
|
-
exact invocation — omit `--epic`)
|
|
141
|
+
exact invocation — omit `--epic`). The gate **scores the single verdict
|
|
142
|
+
the round's owner authored** — schema validation, round cap, decision —
|
|
143
|
+
and never re-scores the criteria itself (Story #4723):
|
|
133
144
|
|
|
134
145
|
```bash
|
|
135
146
|
node <main-repo>/.agents/scripts/acceptance-eval.js \
|
|
@@ -51,19 +51,23 @@ probe logs a warning and leans on init's lease refusal alone.
|
|
|
51
51
|
|
|
52
52
|
## Dispatch mechanics (role-scoped by default)
|
|
53
53
|
|
|
54
|
-
**Lite-
|
|
54
|
+
**Lite-shaped Stories execute inline (Story #4722).** Before spawning anything,
|
|
55
55
|
read the Story's `dispatchMode` from the resolver envelope
|
|
56
56
|
(`stories[].dispatchMode`, derived by `resolveStoryDispatchMode` in
|
|
57
|
-
`lib/orchestration/complexity-gate.js` from the
|
|
58
|
-
|
|
57
|
+
`lib/orchestration/complexity-gate.js` **from the fetched Story body's own
|
|
58
|
+
shape** — `changes[]` count, acceptance count, creates-vs-refactors mix, and
|
|
59
|
+
sensitive-path classes; the `route::lite` label is a human-visible hint only,
|
|
60
|
+
never the control signal, so a lost or never-written label cannot misroute
|
|
61
|
+
delivery): a Story with `dispatchMode: "inline"` executes
|
|
59
62
|
[`deliver-story.md`](deliver-story.md) **inline in this session** — no
|
|
60
63
|
`story-worker` sub-agent boot and no fresh acceptance-critic sub-agents
|
|
61
64
|
(sub-agent boots are the dominant deliver-phase token cost at trivial scope) —
|
|
62
65
|
threading the same `docsDigestPath` / `checklistPath` / change-set discipline
|
|
63
66
|
as a spawned worker. Inline removes model-side fan-out only: every
|
|
64
67
|
`single-story-close.js` gate, the PR to `main`, and the terminal envelope are
|
|
65
|
-
identical.
|
|
66
|
-
|
|
68
|
+
identical. Everything else — a full-shaped body, a missing/unparseable body,
|
|
69
|
+
or a footprint intersecting a sensitive-path class (sensitivity wins and
|
|
70
|
+
keeps the fresh acceptance critic) — takes the standard sub-agent path.
|
|
67
71
|
|
|
68
72
|
**Dispatch each `ready` Story (role-scoped by default).** When
|
|
69
73
|
`delivery.routing.roleScopedAgents` is enabled (the **default**) and the host
|
|
@@ -125,8 +125,7 @@ The v2 engine's trait table:
|
|
|
125
125
|
| Spec / slices | Folded `## Spec` + optional `## Slicing` checkpoints in-session |
|
|
126
126
|
| Ceremony | Per-Story, routed off the derived change level via `ceremony-routing.js` |
|
|
127
127
|
|
|
128
|
-
**Ceremony-lite Stories still land through this engine unchanged (Story #4683).** A Story
|
|
129
|
-
`complexityRoute.route === "lite"`) collapses only the *advisory* plan/deliver
|
|
128
|
+
**Ceremony-lite Stories still land through this engine unchanged (Story #4683).** A lite-routed Story collapses only the *advisory* plan/deliver
|
|
130
129
|
ceremony — the fresh-critic / Tech-Spec authoring a one-artifact scope does
|
|
131
130
|
not earn. It does **not** get a cheaper landing: the close-validation gates
|
|
132
131
|
(lint / test / format / coverage / CRAP / maintainability), the PR to `main`,
|
|
@@ -134,15 +133,22 @@ and the `rules/security-baseline.md` MUSTs all run exactly as for a
|
|
|
134
133
|
full-ceremony Story. The lite route's `preserves` field is the machine-readable
|
|
135
134
|
record of those non-negotiables; there is no lite-specific gate bypass.
|
|
136
135
|
|
|
137
|
-
**
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
**Deliver derives the route from the Story body's shape (Story #4722).**
|
|
137
|
+
Persist stamps a lite cohort's Stories with the `route::lite` label as a
|
|
138
|
+
*human-visible hint only* (and ledgers the authored verdict — recorded
|
|
139
|
+
reason plus per-Story shape evidence — on the `story-plan-state`
|
|
140
|
+
checkpoint); the label is never the control signal. `/deliver` computes the
|
|
141
|
+
route from the fetched Story body via `resolveStoryDispatchMode`
|
|
142
|
+
(`lib/orchestration/complexity-gate.js`) — the same shape taxonomy
|
|
143
|
+
`deriveChangeLevel` applies to the landed diff at close: `changes[]` count,
|
|
144
|
+
acceptance count, creates-vs-refactors mix, sensitive-path classes. A
|
|
145
|
+
lite-shaped Story executes **inline in the deliver session** — even when the
|
|
146
|
+
label is absent or its write failed — with no `story-worker` sub-agent boot,
|
|
147
|
+
and the Step 1a acceptance self-eval runs its critics **inline** (no
|
|
144
148
|
fresh-context acceptance-critic sub-agent dispatch; sub-agent boots are the
|
|
145
|
-
dominant deliver-phase token cost at trivial scope).
|
|
149
|
+
dominant deliver-phase token cost at trivial scope). A footprint
|
|
150
|
+
intersecting a sensitive-path class derives `full` — sensitivity wins, and
|
|
151
|
+
the Story keeps its fresh acceptance critic. Inline execution
|
|
146
152
|
changes the isolation only: the engine, every script gate, and the
|
|
147
153
|
terminal envelope are byte-identical either way.
|
|
148
154
|
|
|
@@ -175,6 +181,20 @@ directly.
|
|
|
175
181
|
|
|
176
182
|
### Step 1a — self-eval mechanics
|
|
177
183
|
|
|
184
|
+
**One verdict-owner per cluster (Story #4723).** The ceremony routing's
|
|
185
|
+
resolved decision names each cluster's single verdict owner
|
|
186
|
+
(`verdictOwner: 'fresh-critic' | 'inline-self-eval'` from
|
|
187
|
+
`resolveCeremonyForRisk`): the fresh maker-blind critic when sensitivity
|
|
188
|
+
routes the cluster `fresh`, the contract-identical inline self-eval when it
|
|
189
|
+
routes `inline`. Exactly one pass authors the verdict — never both, and
|
|
190
|
+
never a preliminary self-assessment pass before dispatching the fresh
|
|
191
|
+
critic (the redundant pre-pass buys no measurable quality and roughly
|
|
192
|
+
triples the acceptance-block cost). `acceptance-eval.js` is the
|
|
193
|
+
deterministic **scorer** of that one authored verdict — schema validation,
|
|
194
|
+
round cap, proceed / redraft / block — not an independent additional pass
|
|
195
|
+
over the criteria. The M4-B floor holds: one verdict per cluster, the
|
|
196
|
+
cluster count owned by `acceptance-clusters.js` alone.
|
|
197
|
+
|
|
178
198
|
**Critic evidence-share (Story #4250).** When the critic runs a `verify[]`
|
|
179
199
|
command that is byte-identical to a close gate (`lint` / `typecheck`), it
|
|
180
200
|
records the pass into the Story evidence keyspace via `--standalone` so
|
|
@@ -227,8 +247,8 @@ Resolve fresh-vs-inline acceptance critics per AC-cluster with
|
|
|
227
247
|
floor forces `fresh`). Review depth reads the same derived level via
|
|
228
248
|
`review-depth.js` inside close, so the two decisions cannot disagree.
|
|
229
249
|
|
|
230
|
-
**Lite-route override (Story #
|
|
231
|
-
|
|
250
|
+
**Lite-route override (Story #4722).** When the Story's body derives the
|
|
251
|
+
lite shape (`resolveStoryDispatchMode` → `inline`), run every
|
|
232
252
|
acceptance critic **inline** — do not spawn fresh-context critic sub-agents
|
|
233
253
|
regardless of what the profile would otherwise resolve. The self-eval rigor
|
|
234
254
|
(scoring each `acceptance[]` item against the one computed change set, with
|
|
@@ -14,8 +14,9 @@ description:
|
|
|
14
14
|
|
|
15
15
|
## Overview
|
|
16
16
|
|
|
17
|
-
The **one** delivery engine in v2 — every Story (
|
|
18
|
-
inline with inline critics; engine, gates, envelope
|
|
17
|
+
The **one** delivery engine in v2 — every Story (a lite-**shaped** Story
|
|
18
|
+
runs inline with inline critics, #4722; engine, gates, envelope
|
|
19
|
+
byte-identical):
|
|
19
20
|
|
|
20
21
|
```text
|
|
21
22
|
single-story-init.js → implement + commits → derived-level ceremony
|
|
@@ -74,7 +75,7 @@ One branch, one PR to `main`, commits against the inline `acceptance[]` /
|
|
|
74
75
|
### Step 1a — Bounded acceptance self-eval loop (**required**)
|
|
75
76
|
|
|
76
77
|
Follow the single-homed include
|
|
77
|
-
[`acceptance-self-eval.md`](acceptance-self-eval.md) (
|
|
78
|
+
[`acceptance-self-eval.md`](acceptance-self-eval.md) (single verdict-owner,
|
|
78
79
|
`verify[]`-as-evidence, proceed / redraft / block). Gate invocation (omit
|
|
79
80
|
`--epic`):
|
|
80
81
|
|
|
@@ -1,52 +1,87 @@
|
|
|
1
1
|
# /plan — on-demand reference appendix
|
|
2
2
|
|
|
3
3
|
> **Applies when:** you are executing [`/plan`](../plan.md) and hit one of the
|
|
4
|
-
> situations below —
|
|
5
|
-
> critic dispatch detail, a failed persist, or source-id
|
|
6
|
-
> stays resident; this file is read on demand.
|
|
7
|
-
|
|
8
|
-
##
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
`
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
the
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
Story
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
`
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
4
|
+
> situations below — shape-derived complexity routing, `--tickets` supersede
|
|
5
|
+
> authoring, critic dispatch detail, a failed persist, or source-id
|
|
6
|
+
> resolution. The spine stays resident; this file is read on demand.
|
|
7
|
+
|
|
8
|
+
## Shape-derived complexity routing (`complexitySignals`)
|
|
9
|
+
|
|
10
|
+
Complexity routes on the **objective shape of the authored work**, never on
|
|
11
|
+
seed word count (Story #4722 — a detailed prompt can describe trivial work, a
|
|
12
|
+
terse one complex work; `maxSeedWords` is removed). The pipeline stages the
|
|
13
|
+
decision:
|
|
14
|
+
|
|
15
|
+
- **Signals, not routing.** The envelope's `complexitySignals` field is
|
|
16
|
+
advisory only (`routingAuthority: false`): enumerated-artifact count (with
|
|
17
|
+
the configured `maxArtifacts` threshold beside it as one input),
|
|
18
|
+
`planning.riskHeuristics` phrases present in the seed, the repo state of
|
|
19
|
+
predicted paths (existing paths predict refactors; missing predict
|
|
20
|
+
creates), and the `audit-rules.json` sensitive-path classes the predicted
|
|
21
|
+
footprint intersects.
|
|
22
|
+
- **You author the verdict.** Judge the signals: a genuinely trivial scope
|
|
23
|
+
(small additive footprint, no risk hits, no sensitive class) earns a `lite`
|
|
24
|
+
claim via `plan-persist.js --route-downgrade-reason "<why>"`. The reason is
|
|
25
|
+
recorded on every created Story's `story-plan-state` checkpoint, making the
|
|
26
|
+
judgment auditable; without a recorded reason the conservative default
|
|
27
|
+
(`full`) stands.
|
|
28
|
+
- **Persist backstops the claim deterministically.** After authoring, the
|
|
29
|
+
work has measurable shape, so persist validates the `lite` claim against
|
|
30
|
+
each Story's own shape — `changes[]` count, acceptance-criteria count,
|
|
31
|
+
creates-vs-refactors mix, glob-free footprint, and sensitive-path classes,
|
|
32
|
+
against the framework `STORY_SHAPE_CEILINGS` — and **fails closed to
|
|
33
|
+
`full`** when any Story exceeds them (the refusal is ledgered on the
|
|
34
|
+
checkpoint too). The lite route is **not** licence to drop a
|
|
35
|
+
non-negotiable — every decision's `preserves` field enumerates what still
|
|
36
|
+
holds: the Story ticket, the PR-to-`main` landing, every repo quality gate,
|
|
37
|
+
and the security baseline. Those gates run in `single-story-close.js`
|
|
38
|
+
regardless of route.
|
|
39
|
+
|
|
40
|
+
**The label is a hint; deliver re-derives (Story #4722).** Persist labels a
|
|
41
|
+
lite cohort's Stories with **`route::lite`** as a *human-visible hint only* —
|
|
42
|
+
`/deliver` computes the route from each fetched Story body via the same shape
|
|
43
|
+
function at dispatch, so neither a lost label nor an unread marker can
|
|
44
|
+
misroute delivery: a lite-shaped Story executes **inline** (no story-worker
|
|
45
|
+
or acceptance-critic sub-agent boots) even with the label absent, and a
|
|
46
|
+
sensitive-footprint Story routes `full` and keeps its fresh critic even with
|
|
47
|
+
the label present. The `route::*` axis stays runtime-derived: hand-authored
|
|
48
|
+
`route::*` entries in `labels[]` are dropped by persist.
|
|
49
|
+
|
|
50
|
+
The knobs (`planning.complexityGate.{enabled, maxArtifacts}`) are documented
|
|
51
|
+
in [`.agents/docs/configuration.md`](../../docs/configuration.md) under
|
|
52
|
+
`### planning`; the defaults live on `DEFAULT_COMPLEXITY_GATE` and the shape
|
|
53
|
+
ceilings on `STORY_SHAPE_CEILINGS` in
|
|
48
54
|
[`lib/orchestration/complexity-gate.js`](../../scripts/lib/orchestration/complexity-gate.js).
|
|
49
55
|
|
|
56
|
+
## Correct-by-construction authoring template (Story #4723)
|
|
57
|
+
|
|
58
|
+
`plan-context.js --out` writes `stories.template.json` as a
|
|
59
|
+
**correct-by-construction** skeleton, built from the same repo snapshot the
|
|
60
|
+
`complexitySignals` probed:
|
|
61
|
+
|
|
62
|
+
- **`verify[]` placeholders already end with a valid `(tier)` tag.** Keep
|
|
63
|
+
every filled entry's trailing tag one of `(unit)` / `(contract)` /
|
|
64
|
+
`(e2e)` / `(validate)` (or use the `manual:<reason>` escape) — a tierless
|
|
65
|
+
entry is exactly the mechanical persist round-trip the template exists to
|
|
66
|
+
prevent.
|
|
67
|
+
- **`changes[]` arrive pre-resolved to creates-vs-refactors.** Every path
|
|
68
|
+
the seed predicted is probed against the repo: an existing path is
|
|
69
|
+
emitted with `assumption: "refactors-existing"`, a missing one with
|
|
70
|
+
`assumption: "creates"`. Trust the pre-resolved assumption — verify
|
|
71
|
+
against the repo before overriding one (authoring `creates` for a file
|
|
72
|
+
that exists at base is a validator rejection). The persist gates stay
|
|
73
|
+
authoritative: they probe the base branch ref, not the working tree.
|
|
74
|
+
- **Keep `## Spec` near contract-level prose.** Persist emits an
|
|
75
|
+
**advisory** warning past ~250 words (`SPEC_SOFT_WORD_BUDGET`) — it never
|
|
76
|
+
fails the persist, but it is the nudge toward the #4707 contract-level
|
|
77
|
+
Spec (interfaces, invariants, load-bearing constraints; no per-file
|
|
78
|
+
behavior narration). The hard fail-closed ceiling (~1500 tokens,
|
|
79
|
+
`spec-spill.js`) is unchanged.
|
|
80
|
+
|
|
81
|
+
A faithfully-filled skeleton — placeholders replaced, pre-resolved entries
|
|
82
|
+
kept, tags valid — passes the persist ticket validators with no
|
|
83
|
+
round-trip.
|
|
84
|
+
|
|
50
85
|
## Tickets mode — authoring `supersedes[]`
|
|
51
86
|
|
|
52
87
|
In `--tickets` mode each Story carries a top-level `supersedes` array claiming
|
|
@@ -32,7 +32,7 @@ Epic/Story router, no scope-triage `epic|story` verdict:
|
|
|
32
32
|
| `--tickets <ids>` | Issue ids to analyze; closed as superseded at persist. |
|
|
33
33
|
| `--no-close-superseded` | Keep the source issues open — no supersede comment, no close. |
|
|
34
34
|
| `--force-review` | STOP at gate #2 for operator review — the only review gate (Story #4542). |
|
|
35
|
-
| `--route-downgrade-reason "<text>"` |
|
|
35
|
+
| `--route-downgrade-reason "<text>"` | Authored `lite` verdict + reason (Story #4722), ledgered per Story; shape-validated, fails closed to `full`. |
|
|
36
36
|
| `--allow-over-budget` | Permit a plan exceeding `maxTickets`. |
|
|
37
37
|
| `--yes` | Non-interactive: auto-proceed gate #1 and gate #2 HITL waits. |
|
|
38
38
|
| `--dry-run` | Author + validate without GitHub writes; run as a pre-pass. |
|
|
@@ -66,11 +66,11 @@ authoring skeleton step 2 starts from.
|
|
|
66
66
|
|
|
67
67
|
The envelope carries docs context, codebase snapshot, the story-author
|
|
68
68
|
prompt, `sourceTickets[]`, `duplicates[]` (open **Stories** overlapping the
|
|
69
|
-
seed — never Epics), and
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
[`helpers/plan-reference.md` §
|
|
69
|
+
seed — never Epics), and advisory `complexitySignals` (**no routing
|
|
70
|
+
authority**, Story #4722). A genuinely trivial scope earns
|
|
71
|
+
`--route-downgrade-reason "<why>"` at persist — shape-validated, failing
|
|
72
|
+
closed to `full`. Detail:
|
|
73
|
+
[`helpers/plan-reference.md` § Shape-derived routing](helpers/plan-reference.md).
|
|
74
74
|
Under `--yes`, do not ask free-form operator questions — unresolved
|
|
75
75
|
unknowns land in Key Assumptions.
|
|
76
76
|
|
|
@@ -79,8 +79,9 @@ duplicate-candidate review. Under `--yes`, auto-proceed.
|
|
|
79
79
|
|
|
80
80
|
### 2. Author
|
|
81
81
|
|
|
82
|
-
**One-shot authoring (Story #4707).** Start from `stories.template.json
|
|
83
|
-
|
|
82
|
+
**One-shot authoring (Story #4707).** Start from `stories.template.json`;
|
|
83
|
+
author `stories.json` in one pass. Entries are pre-resolved (#4723); keep
|
|
84
|
+
tiers/assumptions valid. `body` is a
|
|
84
85
|
markdown string **or** a structured object; persist parses either,
|
|
85
86
|
serializes the canonical markdown, and syncs the top-level `acceptance[]` /
|
|
86
87
|
`verify[]` into the body — never dual-author those lists.
|
|
@@ -182,7 +183,7 @@ persist, re-run the same command; never hand-delete issues.
|
|
|
182
183
|
|
|
183
184
|
- [`/deliver`](deliver.md) — delivery entry point.
|
|
184
185
|
- [`/audit-to-stories`](audit-to-stories.md) — audit findings → plan seed.
|
|
185
|
-
- [`helpers/plan-reference.md`](helpers/plan-reference.md) —
|
|
186
|
-
|
|
186
|
+
- [`helpers/plan-reference.md`](helpers/plan-reference.md) — on-demand
|
|
187
|
+
detail.
|
|
187
188
|
- [`core/scope-triage`](../skills/core/scope-triage/SKILL.md) — optional
|
|
188
189
|
split-advisory notes only (no routing verdict).
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [2.11.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.10.0...mandrel-v2.11.0) (2026-07-23)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
* **migration:** strip retired planning.complexityGate.maxSeedWords on consumer upgrade ([#4729](https://github.com/dsj1984/mandrel/issues/4729)) ([fd75b54](https://github.com/dsj1984/mandrel/commit/fd75b54b0f7390a3d562a25e90cbe6c5911b4f66))
|
|
11
|
+
* route on the work, not the words — shape-derived complexity routing honored end-to-end (refs [#4722](https://github.com/dsj1984/mandrel/issues/4722)) ([#4725](https://github.com/dsj1984/mandrel/issues/4725)) ([f166ca1](https://github.com/dsj1984/mandrel/commit/f166ca14785bad97657fdda0f41e282e47db2ea3))
|
|
12
|
+
* single-owner acceptance verification + plan-template hardening ([#4723](https://github.com/dsj1984/mandrel/issues/4723)) ([#4728](https://github.com/dsj1984/mandrel/issues/4728)) ([954f55a](https://github.com/dsj1984/mandrel/commit/954f55aedda3199d1fdd48b11884a9513012e2c4))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
* **doctor:** include .agents/local/workflows/ in the commands-in-sync expected set (refs [#4724](https://github.com/dsj1984/mandrel/issues/4724)) ([#4726](https://github.com/dsj1984/mandrel/issues/4726)) ([b96dcd4](https://github.com/dsj1984/mandrel/commit/b96dcd446879a9da28e8d86c1a493c2bae62d2c8))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
* **orchestration:** extract spec-budget and story-body parse gate from ticket-validator ([#4730](https://github.com/dsj1984/mandrel/issues/4730)) ([fb0c3c3](https://github.com/dsj1984/mandrel/commit/fb0c3c31795f6178b2ba740b2fb00760921e1178))
|
|
23
|
+
|
|
5
24
|
## [2.10.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.9.0...mandrel-v2.10.0) (2026-07-23)
|
|
6
25
|
|
|
7
26
|
|
package/lib/cli/registry.js
CHANGED
|
@@ -225,10 +225,20 @@ function runGhAuth({ runner = spawn, env = process.env } = {}) {
|
|
|
225
225
|
// ---------------------------------------------------------------------------
|
|
226
226
|
|
|
227
227
|
/**
|
|
228
|
-
* Dry-run the sync-claude-commands logic: compare
|
|
229
|
-
* sources
|
|
230
|
-
*
|
|
231
|
-
*
|
|
228
|
+
* Dry-run the sync-claude-commands logic: compare the union of the two
|
|
229
|
+
* projection sources — `.agents/workflows/*.md` (the installed payload) and
|
|
230
|
+
* `.agents/local/workflows/*.md` (consumer-authored, prune-exempt, projected
|
|
231
|
+
* since 1.75.0 / #4244) — to the generated flat command tree
|
|
232
|
+
* `.claude/commands/*.md` destinations and report parity (the projection is
|
|
233
|
+
* a flat `/<name>` command surface; the #3576 plugin projection was
|
|
234
|
+
* reverted). A dest command backed only by a local workflow is in sync, not
|
|
235
|
+
* stale (#4721).
|
|
236
|
+
*
|
|
237
|
+
* Union semantics mirror the sync script's payload-wins shadowing (`byRel`):
|
|
238
|
+
* a basename projects iff at least one source's copy is non-excluded — an
|
|
239
|
+
* excluded payload copy never enters `byRel`, so it does not shadow a
|
|
240
|
+
* projectable local copy. Filtering each source dir independently and
|
|
241
|
+
* unioning the surviving basenames reproduces that exactly.
|
|
232
242
|
*
|
|
233
243
|
* Resolution anchor (Story #3588): the root defaults to `process.cwd()` —
|
|
234
244
|
* the consumer project directory where `mandrel sync` materializes both
|
|
@@ -272,28 +282,35 @@ function runCommandsInSync({ projectRoot, cwd, readDir, readFile } = {}) {
|
|
|
272
282
|
}
|
|
273
283
|
});
|
|
274
284
|
|
|
275
|
-
const
|
|
285
|
+
const srcDirs = [
|
|
286
|
+
path.join(root, '.agents', 'workflows'),
|
|
287
|
+
path.join(root, '.agents', 'local', 'workflows'),
|
|
288
|
+
];
|
|
276
289
|
const destDir = path.join(root, '.claude', 'commands');
|
|
277
290
|
|
|
278
291
|
// Only top-level .md files are synced (helpers/ subdirectory excluded by
|
|
279
292
|
// the sync script — they are path-included modules, not slash commands).
|
|
280
293
|
// Workflows whose frontmatter carries `command: false` (#4482) opt out of
|
|
281
|
-
// projection and must not count toward the expected command set.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
294
|
+
// projection and must not count toward the expected command set. An absent
|
|
295
|
+
// local dir degrades to [] via the listDir catch, so payload-only
|
|
296
|
+
// consumers are unchanged.
|
|
297
|
+
const expected = new Set();
|
|
298
|
+
for (const srcDir of srcDirs) {
|
|
299
|
+
for (const f of listDir(srcDir)) {
|
|
300
|
+
if (f.startsWith('.')) continue;
|
|
285
301
|
const content = readSource(path.join(srcDir, f));
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
302
|
+
if (content != null && isCommandExcluded(content)) continue;
|
|
303
|
+
expected.add(f);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const sources = [...expected].sort();
|
|
289
307
|
const dests = listDir(destDir)
|
|
290
308
|
.filter((f) => !f.startsWith('.'))
|
|
291
309
|
.sort();
|
|
292
310
|
|
|
293
|
-
const srcSet = new Set(sources);
|
|
294
311
|
const dstSet = new Set(dests);
|
|
295
312
|
const missing = sources.filter((f) => !dstSet.has(f));
|
|
296
|
-
const extra = dests.filter((f) => !
|
|
313
|
+
const extra = dests.filter((f) => !expected.has(f));
|
|
297
314
|
|
|
298
315
|
if (missing.length === 0 && extra.length === 0) {
|
|
299
316
|
return { ok: true, detail: `${sources.length} commands up to date` };
|