mandrel 1.92.0 → 1.93.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/scripts/lib/orchestration/file-assumptions.js +68 -7
- package/.agents/scripts/lib/orchestration/plan-context.js +189 -3
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +99 -0
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +38 -1
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +16 -1
- package/.agents/scripts/lib/orchestration/ticket-validator.js +18 -1
- package/.agents/scripts/plan-context.js +28 -10
- package/.agents/scripts/plan-critics.js +20 -48
- package/.agents/workflows/helpers/plan-epic-reference.md +19 -8
- package/.agents/workflows/helpers/plan-epic.md +83 -17
- package/.agents/workflows/helpers/scope-triage-gate.md +9 -0
- package/.agents/workflows/plan.md +16 -4
- package/docs/CHANGELOG.md +7 -0
- package/package.json +1 -1
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Rules (one error per mismatched path):
|
|
17
17
|
* - `creates` + path **exists** → error (Story would clobber).
|
|
18
|
-
* - `refactors-existing` + path **absent** →
|
|
18
|
+
* - `refactors-existing` (via `changes`) + path **absent** →
|
|
19
|
+
* auto-normalized to `creates` with a logged warning (#4496 fix 5):
|
|
20
|
+
* a refactor declaration against a base-untracked path is
|
|
21
|
+
* deterministically a create, so rejecting it only forces a
|
|
22
|
+
* reject→amend→re-persist cycle for a mechanical rewrite. Genuine
|
|
23
|
+
* mismatches keep failing — a `references`-sourced `refactors-existing`
|
|
24
|
+
* on an absent path is a missing read dependency and stays an error.
|
|
19
25
|
* - `exists` + path **absent** → error (read dependency missing).
|
|
20
26
|
* - `deletes` + path **absent** → error (nothing to delete).
|
|
21
27
|
*
|
|
@@ -185,6 +191,18 @@ function renderMismatch({
|
|
|
185
191
|
return `"${slug}" → body.${source} declares assumption="${assumption}" for ${path} but the path already exists at the base branch.`;
|
|
186
192
|
}
|
|
187
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Render an auto-normalization (#4496 fix 5) into a stable warning string.
|
|
196
|
+
* Kept pure and exported through the report so callers log a
|
|
197
|
+
* self-explanatory line rather than re-deriving the rationale.
|
|
198
|
+
*
|
|
199
|
+
* @param {{ slug: string, source: string, path: string, assumption: string }} normalization
|
|
200
|
+
* @returns {string}
|
|
201
|
+
*/
|
|
202
|
+
function renderNormalization({ slug, source, path, assumption }) {
|
|
203
|
+
return `"${slug}" → body.${source} declares assumption="${assumption}" for ${path} but the path is untracked at the base branch — auto-normalized to "creates" (a refactor of a base-untracked path is deterministically a create). Declare assumption="creates" in the plan to silence this warning.`;
|
|
204
|
+
}
|
|
205
|
+
|
|
188
206
|
/**
|
|
189
207
|
* Index, across every Story, which Stories declare a `creates` (and which
|
|
190
208
|
* declare a `deletes`) for each `changes`-sourced path. The maps drive the
|
|
@@ -251,8 +269,11 @@ function predecessorMutator(index, path, predecessors) {
|
|
|
251
269
|
*
|
|
252
270
|
* {
|
|
253
271
|
* errors: string[] // one entry per mismatch, batched per Story
|
|
254
|
-
* warnings: string[] // legacy/no-assumption deprecation nudges
|
|
272
|
+
* warnings: string[] // legacy/no-assumption deprecation nudges +
|
|
273
|
+
* // auto-normalization notices (#4496 fix 5)
|
|
255
274
|
* mismatches: object[] // structured payload for downstream tooling
|
|
275
|
+
* normalizations: object[] // `refactors-existing`→`creates`
|
|
276
|
+
* // auto-normalizations on base-untracked paths
|
|
256
277
|
* }
|
|
257
278
|
*
|
|
258
279
|
* Under the 2-tier hierarchy the Story is the implementation unit, so the
|
|
@@ -283,6 +304,7 @@ export function validateStoryFileAssumptions(opts) {
|
|
|
283
304
|
const errors = [];
|
|
284
305
|
const warnings = [];
|
|
285
306
|
const mismatches = [];
|
|
307
|
+
const normalizations = [];
|
|
286
308
|
const probeCache = new Map();
|
|
287
309
|
|
|
288
310
|
// Wave-aware setup (Story #3960): transitive predecessor sets over the
|
|
@@ -350,6 +372,14 @@ export function validateStoryFileAssumptions(opts) {
|
|
|
350
372
|
predecessorCreator,
|
|
351
373
|
});
|
|
352
374
|
if (mismatch !== null) {
|
|
375
|
+
// Auto-normalization (#4496 fix 5): a deterministic
|
|
376
|
+
// `refactors-existing`→`creates` rewrite is a warning, never a
|
|
377
|
+
// rejection — genuine mismatches keep flowing to `errors`.
|
|
378
|
+
if (mismatch.normalizedTo === 'creates') {
|
|
379
|
+
normalizations.push(mismatch);
|
|
380
|
+
warnings.push(renderNormalization(mismatch));
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
353
383
|
mismatches.push(mismatch);
|
|
354
384
|
errors.push(renderMismatch(mismatch));
|
|
355
385
|
continue;
|
|
@@ -384,7 +414,7 @@ export function validateStoryFileAssumptions(opts) {
|
|
|
384
414
|
}
|
|
385
415
|
}
|
|
386
416
|
}
|
|
387
|
-
return { errors, warnings, mismatches };
|
|
417
|
+
return { errors, warnings, mismatches, normalizations };
|
|
388
418
|
}
|
|
389
419
|
|
|
390
420
|
/**
|
|
@@ -468,12 +498,43 @@ function checkAssumption({
|
|
|
468
498
|
}
|
|
469
499
|
return null;
|
|
470
500
|
case 'refactors-existing':
|
|
501
|
+
// Validate against the simulated tree: a predecessor `creates` makes
|
|
502
|
+
// an otherwise-absent base path present, so `refactors-existing`
|
|
503
|
+
// against it is no longer a false-positive mismatch (Story #3960).
|
|
504
|
+
if (!simulatedExists) {
|
|
505
|
+
// Auto-normalization (#4496 fix 5): a `changes`-sourced refactor
|
|
506
|
+
// declaration on a path untracked at the base branch (and not
|
|
507
|
+
// produced by any predecessor) is deterministically a create —
|
|
508
|
+
// downgrade to a normalization warning instead of rejecting. Two
|
|
509
|
+
// genuine mismatches stay hard errors: a `references`-sourced entry
|
|
510
|
+
// (a read dependency this Story does not author cannot be "created"
|
|
511
|
+
// here), and a base-TRACKED path a predecessor deletes (the absence
|
|
512
|
+
// is a plan-shape conflict, not an untracked-path misdeclaration).
|
|
513
|
+
if (source === 'changes' && !baseExists) {
|
|
514
|
+
return {
|
|
515
|
+
slug,
|
|
516
|
+
source,
|
|
517
|
+
path,
|
|
518
|
+
assumption,
|
|
519
|
+
expected: 'creates',
|
|
520
|
+
actual: 'absent',
|
|
521
|
+
normalizedTo: 'creates',
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
slug,
|
|
526
|
+
source,
|
|
527
|
+
path,
|
|
528
|
+
assumption,
|
|
529
|
+
expected: 'present',
|
|
530
|
+
actual: 'absent',
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
471
534
|
case 'exists':
|
|
472
535
|
case 'deletes':
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
// `exists` / `deletes` against it is no longer a false-positive
|
|
476
|
-
// mismatch (Story #3960).
|
|
536
|
+
// Same simulated-tree overlay as above (Story #3960); an absent path
|
|
537
|
+
// remains a genuine mismatch for both assumptions.
|
|
477
538
|
if (!simulatedExists) {
|
|
478
539
|
return {
|
|
479
540
|
slug,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* file instead of shim-scripting library imports (the bench measured
|
|
12
12
|
* ~12–15 turns of shim-writing for the dup search alone).
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Three modes (the design's mode matrix + the #4496 seed entry):
|
|
15
15
|
* - `epic` — the Epic exists. Carries `epic`, `clarity` (the Epic
|
|
16
16
|
* Clarity Gate rubric — free, same body fetch), `replan`
|
|
17
17
|
* (already-planned signals) and `planState`.
|
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
* to the persist half). Carries `onePager` and
|
|
20
20
|
* `duplicates[]` (cross-Epic dup search). Clarity is not
|
|
21
21
|
* scored — the ideation path is definitionally clear.
|
|
22
|
+
* - `seed` — headless ideation entry (#4496 fix 1): the one-pager
|
|
23
|
+
* does not exist yet either. The dup search runs off the
|
|
24
|
+
* raw seed text, and the envelope additively carries
|
|
25
|
+
* `seed`, `scopeTriage` (the scope-triage rubric applied
|
|
26
|
+
* CLI-side — no skill Reads on the headless path) and
|
|
27
|
+
* `onePagerSpec` (the canonical one-pager sections, so
|
|
28
|
+
* the authoring pass writes the one-pager in the SAME
|
|
29
|
+
* batched write as the spec artifacts).
|
|
22
30
|
*
|
|
23
31
|
* All fields are JSON-serialisable; the module performs no GitHub writes.
|
|
24
32
|
* The only I/O surfaces are the injected `provider` (reads) and the
|
|
@@ -79,6 +87,131 @@ export const TICKET_SCHEMA_DESCRIPTOR = Object.freeze({
|
|
|
79
87
|
'validateAndNormalizeTickets (lib/orchestration/ticket-validator.js) at persist time',
|
|
80
88
|
});
|
|
81
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Canonical one-pager authoring descriptor for the `seed` envelope
|
|
92
|
+
* (#4496 fix 1). The section names are the ones `plan-epic.md`'s ideation
|
|
93
|
+
* entry has always named, chosen so the authored headings parse against
|
|
94
|
+
* the `SECTION_RE` map in `lib/epic-plan-ideation.js` (which renders the
|
|
95
|
+
* Epic body from the one-pager at persist time via
|
|
96
|
+
* `.agents/templates/epic-from-idea.md`).
|
|
97
|
+
*/
|
|
98
|
+
export const ONE_PAGER_AUTHORING_SPEC = Object.freeze({
|
|
99
|
+
sections: Object.freeze([
|
|
100
|
+
'Problem Statement',
|
|
101
|
+
'Recommended Direction',
|
|
102
|
+
'Key Assumptions',
|
|
103
|
+
'MVP Scope',
|
|
104
|
+
'Not Doing',
|
|
105
|
+
]),
|
|
106
|
+
instruction:
|
|
107
|
+
'Author the one-pager markdown (the canonical sections above, as `## ` ' +
|
|
108
|
+
'headings) in the SAME batched write as the other planning artifacts — ' +
|
|
109
|
+
'no separate ideation pass and no idea-refinement skill activation on ' +
|
|
110
|
+
'this path. Every unresolved unknown lands in Key Assumptions instead ' +
|
|
111
|
+
'of a question.',
|
|
112
|
+
consumedBy:
|
|
113
|
+
'plan-persist.js --one-pager (ideation Epic creation via ' +
|
|
114
|
+
'.agents/templates/epic-from-idea.md)',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Count top-level enumerated items (`- `, `* `, `1. `) anywhere in a
|
|
119
|
+
* free-form seed text. Unlike {@link countScopeItems} this does not require
|
|
120
|
+
* a scope-shaped heading — a raw `--idea` seed rarely has one.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} text
|
|
123
|
+
* @returns {number}
|
|
124
|
+
*/
|
|
125
|
+
function countEnumeratedItems(text) {
|
|
126
|
+
if (typeof text !== 'string' || text.length === 0) return 0;
|
|
127
|
+
return text
|
|
128
|
+
.split(/\r?\n/)
|
|
129
|
+
.filter((line) => /^\s*(?:[-*]|\d+\.)\s+\S/.test(line)).length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Delta-shaped change-request verbs — the `core/scope-triage` skill's
|
|
134
|
+
* change-request rubric routes these to `story` by default when the
|
|
135
|
+
* footprint stays inside Story width.
|
|
136
|
+
*/
|
|
137
|
+
const DELTA_VERB_RE =
|
|
138
|
+
/\b(fix(?:es)?|tweak(?:s)?|extend(?:s)?|update(?:s)?|adjust(?:s)?|rename(?:s)?|correct(?:s)?|patch(?:es)?|bug|regression|flaky)\b/i;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Deterministic, CLI-applied scope-triage verdict over a raw `--idea` seed
|
|
142
|
+
* (#4496 fix 6). Embedding the verdict in the `--seed` envelope removes the
|
|
143
|
+
* two skill Reads (`core/scope-triage` + the gate fragment's rubric pass)
|
|
144
|
+
* from the headless path; the attended path keeps the skill-based judgment.
|
|
145
|
+
*
|
|
146
|
+
* The heuristics anchor to the same sizing SSOT the skill anchors to —
|
|
147
|
+
* `DELIVERABLE_GRANULARITY_GUIDANCE` / `DEFAULT_TASK_SIZING` in
|
|
148
|
+
* `ticket-validator-sizing.js` (one Story = one coherent capability slice;
|
|
149
|
+
* multiple independent capabilities = an Epic) — and to the skill's
|
|
150
|
+
* change-request delta rubric. Like the skill, the verdict is **advisory**:
|
|
151
|
+
* being wrong in the `epic` direction is cheap (the consolidation critic and
|
|
152
|
+
* the sizing validator catch an over-planned Story later), and `borderline`
|
|
153
|
+
* is a first-class output, not a forced call.
|
|
154
|
+
*
|
|
155
|
+
* @param {{ seedText?: string }} args
|
|
156
|
+
* @returns {{ verdict: 'epic'|'story'|'borderline', reasons: string[], advisory: true, appliedBy: 'cli' }}
|
|
157
|
+
*/
|
|
158
|
+
export function buildScopeTriageSignal({ seedText = '' } = {}) {
|
|
159
|
+
const advisory = /** @type {const} */ (true);
|
|
160
|
+
const appliedBy = /** @type {const} */ ('cli');
|
|
161
|
+
const text = typeof seedText === 'string' ? seedText : '';
|
|
162
|
+
const listItems = countEnumeratedItems(text);
|
|
163
|
+
const wordCount = text.split(/\s+/).filter(Boolean).length;
|
|
164
|
+
|
|
165
|
+
if (listItems >= 3) {
|
|
166
|
+
return {
|
|
167
|
+
verdict: 'epic',
|
|
168
|
+
reasons: [
|
|
169
|
+
`seed enumerates ${listItems} candidate capabilities — a genuine fan-out surface`,
|
|
170
|
+
],
|
|
171
|
+
advisory,
|
|
172
|
+
appliedBy,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (listItems >= 1) {
|
|
176
|
+
return {
|
|
177
|
+
verdict: 'story',
|
|
178
|
+
reasons: [
|
|
179
|
+
`seed enumerates ${listItems} capability item(s) — one coherent change with one reason to exist`,
|
|
180
|
+
],
|
|
181
|
+
advisory,
|
|
182
|
+
appliedBy,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (DELTA_VERB_RE.test(text) && wordCount <= 120) {
|
|
186
|
+
return {
|
|
187
|
+
verdict: 'story',
|
|
188
|
+
reasons: [
|
|
189
|
+
'delta-shaped seed (change-request verb, no capability enumeration) within Story width',
|
|
190
|
+
],
|
|
191
|
+
advisory,
|
|
192
|
+
appliedBy,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (wordCount >= 250) {
|
|
196
|
+
return {
|
|
197
|
+
verdict: 'epic',
|
|
198
|
+
reasons: [
|
|
199
|
+
`broad prose seed (~${wordCount} words) with no enumeration — plausibly multiple independent capabilities`,
|
|
200
|
+
],
|
|
201
|
+
advisory,
|
|
202
|
+
appliedBy,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
verdict: 'borderline',
|
|
207
|
+
reasons: [
|
|
208
|
+
'no capability enumeration and no clear delta signal — could be one ambitious Story or a small Epic; the operator (or the --yes Recommended branch) decides',
|
|
209
|
+
],
|
|
210
|
+
advisory,
|
|
211
|
+
appliedBy,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
82
215
|
/**
|
|
83
216
|
* Resolve the planning risk heuristics list from the canonical config
|
|
84
217
|
* block (same resolution the decompose context uses).
|
|
@@ -449,14 +582,56 @@ async function buildOnePagerModeEnvelope({
|
|
|
449
582
|
};
|
|
450
583
|
}
|
|
451
584
|
|
|
585
|
+
/**
|
|
586
|
+
* Build the seed-mode (headless ideation) envelope — #4496 fix 1. The
|
|
587
|
+
* one-pager does not exist yet: the dup search and the authoring-context
|
|
588
|
+
* fold both run off the raw seed text (the same builders the one-pager mode
|
|
589
|
+
* uses), and the envelope additively carries `seed`, the CLI-applied
|
|
590
|
+
* `scopeTriage` verdict (fix 6 — no skill Reads on the headless path), and
|
|
591
|
+
* `onePagerSpec` so the one-pager sections are authored in the same batched
|
|
592
|
+
* write as the spec artifacts.
|
|
593
|
+
*/
|
|
594
|
+
async function buildSeedModeEnvelope({
|
|
595
|
+
seedText,
|
|
596
|
+
provider,
|
|
597
|
+
config,
|
|
598
|
+
settings,
|
|
599
|
+
fullContext,
|
|
600
|
+
cwd,
|
|
601
|
+
}) {
|
|
602
|
+
if (typeof seedText !== 'string' || seedText.trim().length === 0) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
'[plan-context] --seed requires non-empty seed text — nothing to plan from.',
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const base = await buildOnePagerModeEnvelope({
|
|
608
|
+
onePagerPath: undefined,
|
|
609
|
+
onePagerContent: seedText,
|
|
610
|
+
provider,
|
|
611
|
+
config,
|
|
612
|
+
settings,
|
|
613
|
+
fullContext,
|
|
614
|
+
cwd,
|
|
615
|
+
});
|
|
616
|
+
const { onePager: _onePager, ...rest } = base;
|
|
617
|
+
return {
|
|
618
|
+
...rest,
|
|
619
|
+
mode: 'seed',
|
|
620
|
+
seed: { text: seedText },
|
|
621
|
+
scopeTriage: buildScopeTriageSignal({ seedText }),
|
|
622
|
+
onePagerSpec: ONE_PAGER_AUTHORING_SPEC,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
452
626
|
/**
|
|
453
627
|
* Build the single planner-context envelope.
|
|
454
628
|
*
|
|
455
629
|
* @param {{
|
|
456
|
-
* mode: 'epic'|'one-pager',
|
|
630
|
+
* mode: 'epic'|'one-pager'|'seed',
|
|
457
631
|
* epicId?: number,
|
|
458
632
|
* onePagerPath?: string,
|
|
459
633
|
* onePagerContent?: string,
|
|
634
|
+
* seedText?: string,
|
|
460
635
|
* provider: object,
|
|
461
636
|
* config: object,
|
|
462
637
|
* settings: object,
|
|
@@ -469,6 +644,7 @@ export async function buildPlanContext({
|
|
|
469
644
|
epicId,
|
|
470
645
|
onePagerPath,
|
|
471
646
|
onePagerContent,
|
|
647
|
+
seedText,
|
|
472
648
|
provider,
|
|
473
649
|
config = {},
|
|
474
650
|
settings = {},
|
|
@@ -504,7 +680,17 @@ export async function buildPlanContext({
|
|
|
504
680
|
cwd,
|
|
505
681
|
});
|
|
506
682
|
}
|
|
683
|
+
if (mode === 'seed') {
|
|
684
|
+
return buildSeedModeEnvelope({
|
|
685
|
+
seedText,
|
|
686
|
+
provider,
|
|
687
|
+
config,
|
|
688
|
+
settings,
|
|
689
|
+
fullContext,
|
|
690
|
+
cwd,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
507
693
|
throw new Error(
|
|
508
|
-
`[plan-context] unknown mode "${mode}" — expected "epic"
|
|
694
|
+
`[plan-context] unknown mode "${mode}" — expected "epic", "one-pager" or "seed".`,
|
|
509
695
|
);
|
|
510
696
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plan-critics-evaluate.js — shared critic-dispatch evaluation for the
|
|
3
|
+
* collapsed /plan flow (#4496 fix 6; extracted from the `plan-critics.js`
|
|
4
|
+
* CLI so the persist surface folds the same evaluation in as a pre-write
|
|
5
|
+
* phase).
|
|
6
|
+
*
|
|
7
|
+
* Two consumers:
|
|
8
|
+
* - `plan-persist.js` (via `runPlanPersist`) — evaluates the dispatch
|
|
9
|
+
* conditions as a deterministic pre-write phase, prints the verdicts,
|
|
10
|
+
* and records every skip on the plan-metrics ledger, so the headless
|
|
11
|
+
* path never pays a standalone CLI turn for the same decision.
|
|
12
|
+
* - `plan-critics.js` — the standalone CLI survives one release as a
|
|
13
|
+
* thin shim over this module for the attended pre-gate evaluation
|
|
14
|
+
* (the verdict folds into gate #2's view before the persist runs).
|
|
15
|
+
*
|
|
16
|
+
* Pure evaluation: no file I/O, no GitHub calls, no ledger writes — the
|
|
17
|
+
* callers own artifact loading and skip recording.
|
|
18
|
+
*
|
|
19
|
+
* @module lib/orchestration/plan-critics-evaluate
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { getLimits } from '../config-resolver.js';
|
|
23
|
+
import {
|
|
24
|
+
evaluateConsolidationDispatch,
|
|
25
|
+
evaluatePremortemDispatch,
|
|
26
|
+
} from './plan-critic-conditions.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the planning risk heuristics list from the canonical config
|
|
30
|
+
* block (same resolution `plan-context.js` and the decompose context use).
|
|
31
|
+
*
|
|
32
|
+
* @param {object} config
|
|
33
|
+
* @returns {string[]}
|
|
34
|
+
*/
|
|
35
|
+
function resolveRiskHeuristics(config = {}) {
|
|
36
|
+
if (Array.isArray(config.planning?.riskHeuristics)) {
|
|
37
|
+
return config.planning.riskHeuristics;
|
|
38
|
+
}
|
|
39
|
+
return config.agentSettings?.planning?.riskHeuristics || [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Evaluate the consolidation + pre-mortem critic dispatch conditions over
|
|
44
|
+
* the authored planning artifacts (design §4 / #4474 PR6 conditions,
|
|
45
|
+
* unchanged):
|
|
46
|
+
*
|
|
47
|
+
* - Consolidation: skipped outright when `tickets` is null/absent (the
|
|
48
|
+
* single-delivery shape authors no draft tickets); otherwise the
|
|
49
|
+
* deterministic precondition + size/divergence conditions.
|
|
50
|
+
* - Pre-mortem: risk verdict overall level high, OR ticket count at least
|
|
51
|
+
* half `maxTickets`, OR any `planning.riskHeuristics` phrase matching
|
|
52
|
+
* the plan text.
|
|
53
|
+
*
|
|
54
|
+
* @param {{
|
|
55
|
+
* techSpecContent: string,
|
|
56
|
+
* riskVerdict: { summary?: string },
|
|
57
|
+
* tickets?: Array<object>|null,
|
|
58
|
+
* config?: object,
|
|
59
|
+
* }} args
|
|
60
|
+
* @returns {{
|
|
61
|
+
* consolidation: { critic: string, dispatch: boolean, reasons: string[] },
|
|
62
|
+
* premortem: { critic: string, dispatch: boolean, reasons: string[] },
|
|
63
|
+
* }}
|
|
64
|
+
*/
|
|
65
|
+
export function evaluatePlanCritics({
|
|
66
|
+
techSpecContent,
|
|
67
|
+
riskVerdict,
|
|
68
|
+
tickets = null,
|
|
69
|
+
config = {},
|
|
70
|
+
}) {
|
|
71
|
+
const ticketList = Array.isArray(tickets) ? tickets : null;
|
|
72
|
+
const consolidation =
|
|
73
|
+
ticketList === null
|
|
74
|
+
? {
|
|
75
|
+
critic: 'consolidation',
|
|
76
|
+
dispatch: false,
|
|
77
|
+
reasons: [
|
|
78
|
+
'single-delivery shape — no draft tickets exist to consolidate.',
|
|
79
|
+
],
|
|
80
|
+
}
|
|
81
|
+
: evaluateConsolidationDispatch({
|
|
82
|
+
draftStories: ticketList,
|
|
83
|
+
specText: techSpecContent,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const premortem = evaluatePremortemDispatch({
|
|
87
|
+
riskVerdict,
|
|
88
|
+
ticketCount: ticketList?.length ?? 0,
|
|
89
|
+
maxTickets: getLimits(config).maxTickets,
|
|
90
|
+
riskHeuristics: resolveRiskHeuristics(config),
|
|
91
|
+
planText: [
|
|
92
|
+
techSpecContent ?? '',
|
|
93
|
+
ticketList ? JSON.stringify(ticketList) : '',
|
|
94
|
+
riskVerdict?.summary ?? '',
|
|
95
|
+
].join('\n'),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return { consolidation, premortem };
|
|
99
|
+
}
|
|
@@ -130,6 +130,7 @@ import {
|
|
|
130
130
|
read as readPlanState,
|
|
131
131
|
write as writePlanState,
|
|
132
132
|
} from '../epic-plan-state-store.js';
|
|
133
|
+
import { evaluatePlanCritics } from '../plan-critics-evaluate.js';
|
|
133
134
|
import {
|
|
134
135
|
appendCriticSkip,
|
|
135
136
|
readPlanMetrics,
|
|
@@ -426,9 +427,10 @@ export async function runPlanPersist({
|
|
|
426
427
|
let validated = null;
|
|
427
428
|
let amendPartition = null;
|
|
428
429
|
let reachability = null;
|
|
430
|
+
let gateSet = null;
|
|
429
431
|
if (mode !== 'single') {
|
|
430
432
|
amendPartition = mode === 'amend' ? partitionAmendTickets(tickets) : null;
|
|
431
|
-
|
|
433
|
+
gateSet = mode === 'amend' ? buildMergedTicketSet(tickets) : tickets;
|
|
432
434
|
const maxTickets = getLimits(config).maxTickets;
|
|
433
435
|
if (gateSet.length > maxTickets && !allowOverBudget) {
|
|
434
436
|
throw new Error(
|
|
@@ -533,6 +535,40 @@ export async function runPlanPersist({
|
|
|
533
535
|
);
|
|
534
536
|
}
|
|
535
537
|
|
|
538
|
+
// ---- Step 4.7: folded critic dispatch evaluation (#4496 fix 6 — the
|
|
539
|
+
// former standalone `plan-critics.js` turn). Deterministic and git-local,
|
|
540
|
+
// still zero provider calls: the verdicts are printed (and returned on
|
|
541
|
+
// the result) as part of the pre-write phase, and every skip decision is
|
|
542
|
+
// appended to the plan-metrics ledger exactly as the standalone CLI did,
|
|
543
|
+
// so under-firing stays auditable without a separate invocation on the
|
|
544
|
+
// headless path. Advisory by construction — the deterministic validators
|
|
545
|
+
// above remain the unchanged hard gates. ----
|
|
546
|
+
const critics = evaluatePlanCritics({
|
|
547
|
+
techSpecContent,
|
|
548
|
+
riskVerdict,
|
|
549
|
+
tickets: gateSet,
|
|
550
|
+
config,
|
|
551
|
+
});
|
|
552
|
+
for (const decision of [critics.consolidation, critics.premortem]) {
|
|
553
|
+
Logger.info(
|
|
554
|
+
`[plan-persist] critic ${decision.critic}: ` +
|
|
555
|
+
`${decision.dispatch ? 'dispatch' : 'skip'} — ` +
|
|
556
|
+
decision.reasons.join('; '),
|
|
557
|
+
);
|
|
558
|
+
if (!decision.dispatch) {
|
|
559
|
+
// Best-effort by contract — a failed append never fails the persist.
|
|
560
|
+
await appendCriticSkip(
|
|
561
|
+
{
|
|
562
|
+
critic: decision.critic,
|
|
563
|
+
reasons: decision.reasons,
|
|
564
|
+
cli: 'plan-persist',
|
|
565
|
+
epicId: requestedEpicId,
|
|
566
|
+
},
|
|
567
|
+
config,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
536
572
|
// ---- Step 5: ideation fold / Epic resolution (first provider call). ----
|
|
537
573
|
const { epicId, epic, created } = await resolveTargetEpic({
|
|
538
574
|
epicId: requestedEpicId,
|
|
@@ -902,6 +938,7 @@ export async function runPlanPersist({
|
|
|
902
938
|
freshness,
|
|
903
939
|
healthcheck,
|
|
904
940
|
reachability,
|
|
941
|
+
critics,
|
|
905
942
|
reconcile,
|
|
906
943
|
specPath: specFilePath,
|
|
907
944
|
waveTable,
|
|
@@ -89,10 +89,16 @@ function renderWaveTableLines(waveTable) {
|
|
|
89
89
|
* record `{ deliveryShape, sliceCount, routingReasons }` (Epic #4474 PR4)
|
|
90
90
|
* for the spec-only mode.
|
|
91
91
|
*
|
|
92
|
+
* Every auto-waiver the persist derived is printed WITH its reason
|
|
93
|
+
* (#4496 fix 2) — e.g. the no-BDD-runner acceptance-disposition waiver
|
|
94
|
+
* (`planningRisk.acceptanceWaivedReason`) — so the summary is
|
|
95
|
+
* self-explanatory and a headless reader never has to re-derive a persist
|
|
96
|
+
* outcome from framework source.
|
|
97
|
+
*
|
|
92
98
|
* @param {{
|
|
93
99
|
* epicId: number,
|
|
94
100
|
* ticketCount: number,
|
|
95
|
-
* planningRisk: { overallLevel?: string, gateDecision?: string },
|
|
101
|
+
* planningRisk: { overallLevel?: string, gateDecision?: string, acceptanceDisposition?: string, acceptanceWaivedReason?: string },
|
|
96
102
|
* reviewRouting: { decision?: string },
|
|
97
103
|
* freshness?: { stale?: number, ambiguous?: number },
|
|
98
104
|
* healthcheck?: { ok?: boolean, waived?: boolean, skipped?: boolean },
|
|
@@ -137,6 +143,14 @@ export function buildPlanSummaryCommentBody({
|
|
|
137
143
|
? `- Single-delivery plan (\`delivery::single\`): no Story tree — the Delivery Slicing table is the audit trail.`
|
|
138
144
|
: `- ${ticketCount} Story ticket(s) persisted across ${waveTable.length} wave(s).`;
|
|
139
145
|
|
|
146
|
+
// Auto-waivers always ship with their reason (#4496 fix 2): the summary
|
|
147
|
+
// is authoritative, so the line must be self-explanatory on its own.
|
|
148
|
+
const waiverLines = planningRisk?.acceptanceWaivedReason
|
|
149
|
+
? [
|
|
150
|
+
`- ⚠️ Acceptance disposition auto-waived to \`not-applicable\` — ${planningRisk.acceptanceWaivedReason}`,
|
|
151
|
+
]
|
|
152
|
+
: [];
|
|
153
|
+
|
|
140
154
|
const amendLines = amend
|
|
141
155
|
? [
|
|
142
156
|
`- Amend delta: ${amend.created.length} added, ${amend.recreated.length} modified (closed + recreated), ${amend.closed.length} closed, ${amend.keptCount} kept untouched.`,
|
|
@@ -176,6 +190,7 @@ export function buildPlanSummaryCommentBody({
|
|
|
176
190
|
headLine,
|
|
177
191
|
...amendLines,
|
|
178
192
|
`- Risk: ${planningRisk?.overallLevel ?? 'unknown'} · ${planningRisk?.gateDecision ?? 'unknown'} (review routing: ${reviewRouting?.decision ?? 'unknown'}).`,
|
|
193
|
+
...waiverLines,
|
|
179
194
|
freshnessLine,
|
|
180
195
|
healthcheckLine,
|
|
181
196
|
// G2 measurement receipt (Epic #4474 PR1/PR7): the plan-CLI invocation
|
|
@@ -589,8 +589,25 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
|
|
|
589
589
|
gitRunner: sharedGitRunner,
|
|
590
590
|
cwd: opts.cwd,
|
|
591
591
|
});
|
|
592
|
+
// Auto-normalizations (#4496 fix 5) get their own prefix so the logged
|
|
593
|
+
// warning is self-explanatory; everything else on the warnings channel
|
|
594
|
+
// is a legacy-shape deprecation nudge.
|
|
595
|
+
const normalizationWarnings = new Set(
|
|
596
|
+
(assumptionReport.normalizations ?? []).map((n) => n.path),
|
|
597
|
+
);
|
|
592
598
|
for (const warning of assumptionReport.warnings) {
|
|
593
|
-
|
|
599
|
+
const isNormalization = warning.includes('auto-normalized to "creates"');
|
|
600
|
+
Logger.warn(
|
|
601
|
+
`[ticket-validator] ${isNormalization ? 'assumption-normalized' : 'assumption-deprecation'}: ${warning}`,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
if (normalizationWarnings.size > 0) {
|
|
605
|
+
Logger.warn(
|
|
606
|
+
`[ticket-validator] ${normalizationWarnings.size} refactors-existing ` +
|
|
607
|
+
'declaration(s) on base-untracked path(s) auto-normalized to ' +
|
|
608
|
+
'"creates" — the gate proceeds; update the plan declarations at ' +
|
|
609
|
+
'the next amend.',
|
|
610
|
+
);
|
|
594
611
|
}
|
|
595
612
|
assumptionErrors = assumptionReport.errors;
|
|
596
613
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* stdout-pure JSON envelope. The PR7 cutover retired the delegate CLIs —
|
|
12
12
|
* this is the only emit-context surface.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Three entry forms (exactly one is required):
|
|
15
15
|
*
|
|
16
16
|
* --epic <id> Existing-Epic mode. Envelope carries `epic`,
|
|
17
17
|
* `clarity` (Epic Clarity Gate rubric), `replan`
|
|
@@ -23,6 +23,15 @@
|
|
|
23
23
|
* dup search). No clarity score: the ideation path
|
|
24
24
|
* is definitionally clear.
|
|
25
25
|
*
|
|
26
|
+
* --seed "<text>" Headless ideation entry (#4496 fix 1) — neither
|
|
27
|
+
* the Epic nor the one-pager exists yet. The dup
|
|
28
|
+
* search runs off the raw seed text, and the
|
|
29
|
+
* envelope additively carries `seed`, `scopeTriage`
|
|
30
|
+
* (the scope-triage rubric applied CLI-side — fix 6)
|
|
31
|
+
* and `onePagerSpec`, so the one-pager sections are
|
|
32
|
+
* authored in the same batched write as the spec
|
|
33
|
+
* artifacts.
|
|
34
|
+
*
|
|
26
35
|
* Flags:
|
|
27
36
|
* --pretty Pretty-print the JSON envelope.
|
|
28
37
|
* --full-context Bypass the planning-context budget (unbounded body).
|
|
@@ -58,10 +67,11 @@ import { createProvider } from './lib/provider-factory.js';
|
|
|
58
67
|
* captured output is exactly one `JSON.parse`-able payload.
|
|
59
68
|
*
|
|
60
69
|
* @param {{
|
|
61
|
-
* mode: 'epic'|'one-pager',
|
|
70
|
+
* mode: 'epic'|'one-pager'|'seed',
|
|
62
71
|
* epicId?: number,
|
|
63
72
|
* onePagerPath?: string,
|
|
64
73
|
* onePagerContent?: string,
|
|
74
|
+
* seedText?: string,
|
|
65
75
|
* provider: object,
|
|
66
76
|
* config: object,
|
|
67
77
|
* settings: object,
|
|
@@ -77,6 +87,7 @@ export async function emitPlanContext({
|
|
|
77
87
|
epicId,
|
|
78
88
|
onePagerPath,
|
|
79
89
|
onePagerContent,
|
|
90
|
+
seedText,
|
|
80
91
|
provider,
|
|
81
92
|
config,
|
|
82
93
|
settings,
|
|
@@ -90,6 +101,7 @@ export async function emitPlanContext({
|
|
|
90
101
|
epicId,
|
|
91
102
|
onePagerPath,
|
|
92
103
|
onePagerContent,
|
|
104
|
+
seedText,
|
|
93
105
|
provider,
|
|
94
106
|
config,
|
|
95
107
|
settings,
|
|
@@ -108,6 +120,7 @@ async function main() {
|
|
|
108
120
|
options: {
|
|
109
121
|
epic: { type: 'string' },
|
|
110
122
|
'one-pager': { type: 'string' },
|
|
123
|
+
seed: { type: 'string' },
|
|
111
124
|
pretty: { type: 'boolean', default: false },
|
|
112
125
|
'full-context': { type: 'boolean', default: false },
|
|
113
126
|
},
|
|
@@ -117,12 +130,16 @@ async function main() {
|
|
|
117
130
|
const hasEpic = typeof values.epic === 'string' && values.epic.length > 0;
|
|
118
131
|
const hasOnePager =
|
|
119
132
|
typeof values['one-pager'] === 'string' && values['one-pager'].length > 0;
|
|
120
|
-
|
|
133
|
+
const hasSeed = typeof values.seed === 'string' && values.seed.length > 0;
|
|
134
|
+
const entryForms = [hasEpic, hasOnePager, hasSeed].filter(Boolean).length;
|
|
135
|
+
if (entryForms !== 1) {
|
|
121
136
|
throw new Error(
|
|
122
|
-
'Pass exactly one of --epic <id
|
|
123
|
-
'(--epic: existing-Epic mode; --one-pager: ideation mode
|
|
137
|
+
'Pass exactly one of --epic <id>, --one-pager <path> or --seed "<text>". ' +
|
|
138
|
+
'(--epic: existing-Epic mode; --one-pager: ideation mode; ' +
|
|
139
|
+
'--seed: headless ideation entry.)',
|
|
124
140
|
);
|
|
125
141
|
}
|
|
142
|
+
const mode = hasEpic ? 'epic' : hasOnePager ? 'one-pager' : 'seed';
|
|
126
143
|
|
|
127
144
|
let epicId;
|
|
128
145
|
if (hasEpic) {
|
|
@@ -159,21 +176,22 @@ async function main() {
|
|
|
159
176
|
const provider = createProvider(config);
|
|
160
177
|
|
|
161
178
|
// Plan-metrics ledger (#4474 PR1): stamp entry/exit + mode so the folded
|
|
162
|
-
// emit surface is measured against the 12-phase baseline. One-pager
|
|
163
|
-
//
|
|
164
|
-
// (epicId null) exactly like `story-plan.js`.
|
|
179
|
+
// emit surface is measured against the 12-phase baseline. One-pager and
|
|
180
|
+
// seed modes have no Epic yet, so the record routes to the standalone
|
|
181
|
+
// stream (epicId null) exactly like `story-plan.js`.
|
|
165
182
|
await recordPlanInvocation(
|
|
166
183
|
{
|
|
167
184
|
cli: 'plan-context',
|
|
168
|
-
mode
|
|
185
|
+
mode,
|
|
169
186
|
epicId: hasEpic ? epicId : null,
|
|
170
187
|
config,
|
|
171
188
|
},
|
|
172
189
|
() =>
|
|
173
190
|
emitPlanContext({
|
|
174
|
-
mode
|
|
191
|
+
mode,
|
|
175
192
|
epicId,
|
|
176
193
|
onePagerPath: hasOnePager ? values['one-pager'] : undefined,
|
|
194
|
+
seedText: hasSeed ? values.seed : undefined,
|
|
177
195
|
provider,
|
|
178
196
|
config,
|
|
179
197
|
settings,
|
|
@@ -5,15 +5,24 @@
|
|
|
5
5
|
* author-step critics of the collapsed /plan flow (Epic #4474 PR6,
|
|
6
6
|
* design §4).
|
|
7
7
|
*
|
|
8
|
+
* **Thin shim (#4496 fix 6).** The evaluation itself now lives in
|
|
9
|
+
* `lib/orchestration/plan-critics-evaluate.js` and is folded into
|
|
10
|
+
* `plan-persist.js` as a pre-write phase, so the headless (`--yes`) path
|
|
11
|
+
* never pays a standalone CLI turn for the dispatch decision. This CLI
|
|
12
|
+
* survives one release for the attended pre-gate evaluation (the verdict
|
|
13
|
+
* folds into gate #2's view before the persist runs) and for any external
|
|
14
|
+
* scripting; it delegates to the shared module and keeps its exact output
|
|
15
|
+
* contract.
|
|
16
|
+
*
|
|
8
17
|
* Runs between authoring and gate #2, entirely git-local (zero GitHub
|
|
9
18
|
* calls): reads the authored artifacts, evaluates the risk/size dispatch
|
|
10
19
|
* conditions for the consolidation (8.3) and pre-mortem (8.5) critics via
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
20
|
+
* the shared evaluator, and emits one JSON verdict on stdout. The workflow
|
|
21
|
+
* dispatches a fresh-context sub-agent ONLY for critics with
|
|
22
|
+
* `dispatch: true`; every skip decision is appended to the plan-metrics
|
|
23
|
+
* ledger (`kind: "critic-skip"`, with reasons) so under-firing is
|
|
24
|
+
* auditable — the persist validators remain unchanged hard gates
|
|
25
|
+
* regardless of what this gate decides.
|
|
17
26
|
*
|
|
18
27
|
* Conditions (design §4 / §6 PR6):
|
|
19
28
|
* - Consolidation: the existing deterministic precondition
|
|
@@ -53,16 +62,12 @@ import { parseArgs } from 'node:util';
|
|
|
53
62
|
import { runAsCli } from './lib/cli-utils.js';
|
|
54
63
|
import { epicArtifactPath } from './lib/config/temp-paths.js';
|
|
55
64
|
import {
|
|
56
|
-
getLimits,
|
|
57
65
|
resolveConfig,
|
|
58
66
|
validateOrchestrationConfig,
|
|
59
67
|
} from './lib/config-resolver.js';
|
|
60
68
|
import { routeAllOutputToStderr } from './lib/Logger.js';
|
|
61
69
|
import { loadRiskVerdict } from './lib/orchestration/epic-plan-spec/phases/risk-verdict.js';
|
|
62
|
-
import {
|
|
63
|
-
evaluateConsolidationDispatch,
|
|
64
|
-
evaluatePremortemDispatch,
|
|
65
|
-
} from './lib/orchestration/plan-critic-conditions.js';
|
|
70
|
+
import { evaluatePlanCritics } from './lib/orchestration/plan-critics-evaluate.js';
|
|
66
71
|
import {
|
|
67
72
|
appendCriticSkip,
|
|
68
73
|
recordPlanInvocation,
|
|
@@ -72,20 +77,6 @@ const USAGE =
|
|
|
72
77
|
'Usage: plan-critics.js (--epic <EpicId> | --tech-spec <file> ' +
|
|
73
78
|
'--risk-verdict <file> [--tickets <file>]) [--pretty]';
|
|
74
79
|
|
|
75
|
-
/**
|
|
76
|
-
* Resolve the planning risk heuristics list from the canonical config
|
|
77
|
-
* block (same resolution `plan-context.js` and the decompose context use).
|
|
78
|
-
*
|
|
79
|
-
* @param {object} config
|
|
80
|
-
* @returns {string[]}
|
|
81
|
-
*/
|
|
82
|
-
function resolveRiskHeuristics(config = {}) {
|
|
83
|
-
if (Array.isArray(config.planning?.riskHeuristics)) {
|
|
84
|
-
return config.planning.riskHeuristics;
|
|
85
|
-
}
|
|
86
|
-
return config.agentSettings?.planning?.riskHeuristics || [];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
80
|
async function readOptional(filePath, { required }) {
|
|
90
81
|
try {
|
|
91
82
|
return await readFile(filePath, 'utf8');
|
|
@@ -172,30 +163,11 @@ async function main() {
|
|
|
172
163
|
}
|
|
173
164
|
}
|
|
174
165
|
|
|
175
|
-
const consolidation =
|
|
176
|
-
|
|
177
|
-
? {
|
|
178
|
-
critic: 'consolidation',
|
|
179
|
-
dispatch: false,
|
|
180
|
-
reasons: [
|
|
181
|
-
'single-delivery shape — no draft tickets exist to consolidate.',
|
|
182
|
-
],
|
|
183
|
-
}
|
|
184
|
-
: evaluateConsolidationDispatch({
|
|
185
|
-
draftStories: tickets,
|
|
186
|
-
specText: techSpecContent,
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
const premortem = evaluatePremortemDispatch({
|
|
166
|
+
const { consolidation, premortem } = evaluatePlanCritics({
|
|
167
|
+
techSpecContent,
|
|
190
168
|
riskVerdict,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
riskHeuristics: resolveRiskHeuristics(config),
|
|
194
|
-
planText: [
|
|
195
|
-
techSpecContent,
|
|
196
|
-
ticketsRaw ?? '',
|
|
197
|
-
riskVerdict.summary ?? '',
|
|
198
|
-
].join('\n'),
|
|
169
|
+
tickets,
|
|
170
|
+
config,
|
|
199
171
|
});
|
|
200
172
|
|
|
201
173
|
// Skip-audit trail (#4474 PR6): every non-dispatch is a ledger
|
|
@@ -100,19 +100,30 @@ reseeding from live GitHub state when the file is missing — so only the
|
|
|
100
100
|
genuinely-missing children are created; the existing tree is never
|
|
101
101
|
duplicated.
|
|
102
102
|
|
|
103
|
-
## Measurement — the G2 acceptance gate (Epic #4474)
|
|
103
|
+
## Measurement — the G2 acceptance gate (Epic #4474, restated per #4496)
|
|
104
104
|
|
|
105
|
-
The 3-step collapse is **measured, not asserted**. The
|
|
106
|
-
|
|
105
|
+
The 3-step collapse is **measured, not asserted**. The gate is stated
|
|
106
|
+
**per mode** (#4496 — adopted in the bench roadmap after the first measured
|
|
107
|
+
headless run):
|
|
107
108
|
|
|
108
|
-
- **
|
|
109
|
-
|
|
109
|
+
- **Epic-mode** (`/plan <id> --yes`): **≤ ~12 turns / ≤ ~1.1M tokens**.
|
|
110
|
+
- **Ideation-mode** (`/plan --idea --yes`): **≤ ~15 turns / ≤ ~1.5M
|
|
111
|
+
tokens** once the #4496 fixes (seed-mode ideation, authoritative persist
|
|
112
|
+
summaries, batched artifact writes) are in play; **interim smoke
|
|
113
|
+
threshold ≤ ~20 turns / ≤ ~2.0M tokens** for runs measured before/during
|
|
114
|
+
the rollout.
|
|
115
|
+
- Verification is a cheap direct `/plan --idea --yes` probe reading the
|
|
116
|
+
plan-metrics ledger (~$3–4), not a full bench cell.
|
|
117
|
+
|
|
118
|
+
Shared mechanics behind both rungs (from the 55–72-turn / 7.3–8.9M
|
|
119
|
+
12-phase baseline):
|
|
120
|
+
|
|
121
|
+
- The turns proxy is the plan-metrics invocation ledger
|
|
110
122
|
(`temp/epic-<id>/plan-metrics.json`) plus the host session's turn
|
|
111
123
|
accounting — ledger records count CLI invocations from the parent
|
|
112
124
|
session's perspective, not sub-agent turns.
|
|
113
|
-
-
|
|
114
|
-
|
|
115
|
-
in-repo counter.
|
|
125
|
+
- Plan tokens are owned by the host's session accounting (mandrel-bench
|
|
126
|
+
`modelUsage`), not by any in-repo counter.
|
|
116
127
|
- **Unchanged validator coverage** — every deterministic gate of the
|
|
117
128
|
retired pipeline still fires on the persist path: section gate, ticket
|
|
118
129
|
validator, file-assumption, DAG, budget, draft reachability, inline
|
|
@@ -35,6 +35,9 @@ confirmed (gate #1).
|
|
|
35
35
|
|
|
36
36
|
### Ideation entry (`--idea "<seed>"` or no argument)
|
|
37
37
|
|
|
38
|
+
**Attended (no `--yes`)** — the interactive grill loop is HITL by
|
|
39
|
+
definition and stays:
|
|
40
|
+
|
|
38
41
|
1. Activate the [`core/idea-refinement`](../../skills/core/idea-refinement/SKILL.md)
|
|
39
42
|
skill with the seed. It returns a one-pager with the canonical sections
|
|
40
43
|
(Problem Statement, Recommended Direction, Key Assumptions, MVP Scope,
|
|
@@ -64,6 +67,26 @@ confirmed (gate #1).
|
|
|
64
67
|
operator either confirms the new Epic is distinct or folds the idea into
|
|
65
68
|
an existing Epic (in which case `/plan` exits).
|
|
66
69
|
|
|
70
|
+
**Headless (`--yes`) — seed entry (#4496 fix 1).** There is no one to
|
|
71
|
+
grill, so the one-pager prelude is pure ceremony: do **not** activate the
|
|
72
|
+
`idea-refinement` skill and do **not** write a separate one-pager file
|
|
73
|
+
before authoring. Instead, emit the envelope directly off the seed:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
node .agents/scripts/plan-context.js --seed "<seed text>" \
|
|
77
|
+
> temp/plan-ideation/<slug>/plan-context.json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
(slug from the seed's leading phrase; the tree is gitignored). The seed envelope is the one-pager envelope plus three additive fields:
|
|
81
|
+
`seed` (the raw text), `scopeTriage` (the scope-triage rubric applied
|
|
82
|
+
CLI-side — no skill Reads on this path; a `story` / `borderline` verdict
|
|
83
|
+
resolves to its Recommended handoff per
|
|
84
|
+
[`scope-triage-gate.md`](scope-triage-gate.md)), and `onePagerSpec` (the
|
|
85
|
+
canonical one-pager sections). The one-pager markdown is authored **in
|
|
86
|
+
step 2's single batched write**, alongside the other artifacts — every
|
|
87
|
+
unresolved unknown lands in its Key Assumptions section. `duplicates[]`
|
|
88
|
+
resolves headlessly per gate #1's `--yes` note.
|
|
89
|
+
|
|
67
90
|
### Existing-Epic entry (`/plan <epicId>`)
|
|
68
91
|
|
|
69
92
|
1. Emit the authoring envelope:
|
|
@@ -126,25 +149,38 @@ step 2 until the operator explicitly confirms.
|
|
|
126
149
|
Read the envelope with the `Read` tool and write the planning artifacts to
|
|
127
150
|
`temp/epic-[Epic_ID]/` (ideation: the `temp/plan-ideation/<slug>/` tree).
|
|
128
151
|
The single `plan-context.json` envelope supersedes the per-phase
|
|
129
|
-
`planner-context.json` / `decomposer-context.json` files
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
`planner-context.json` / `decomposer-context.json` files.
|
|
153
|
+
|
|
154
|
+
**The envelope's `systemPrompts` ARE the authoring instructions on this
|
|
155
|
+
path** (#4496 fix 4): `systemPrompts.spec` / `systemPrompts.acceptance`
|
|
156
|
+
govern the Tech Spec, risk verdict, and Acceptance Spec;
|
|
157
|
+
`systemPrompts.decompose` (with `ticketSchema` and `maxTickets`) governs
|
|
158
|
+
the tickets. Do **not** read the
|
|
159
|
+
[`epic-plan-spec-author`](../../skills/core/epic-plan-spec-author/SKILL.md)
|
|
160
|
+
or
|
|
161
|
+
[`epic-plan-decompose-author`](../../skills/core/epic-plan-decompose-author/SKILL.md)
|
|
162
|
+
SKILL.md files here — they remain the reference source the prompts render
|
|
163
|
+
from, but re-reading them double-pays instructions the envelope already
|
|
164
|
+
carries.
|
|
165
|
+
|
|
166
|
+
**Batched writes (#4496 fix 3).** Emit the artifact files as **parallel
|
|
167
|
+
`Write` calls in ONE message** — `techspec.md`, `risk-verdict.json`,
|
|
168
|
+
`acceptance-spec.md`, and `tickets.json` together (ideation: the one-pager
|
|
169
|
+
markdown joins the same batch). Never write them one-per-turn.
|
|
170
|
+
|
|
171
|
+
1. **`techspec.md`** — per `systemPrompts.spec`. The Tech Spec opens with
|
|
172
|
+
`## Delivery Slicing` and never restates the Epic's Context/Goal/Scope.
|
|
173
|
+
2. **`risk-verdict.json`** — the schema-conformant verdict
|
|
137
174
|
**plus `deliveryShape: "fan-out"|"single"`** and a one-line rationale.
|
|
138
175
|
Seed the shape from the envelope's `deliveryShapeSignal` (advisory —
|
|
139
176
|
the operator vetoes it at gate #2). `"single"` means one-pass-sized or a
|
|
140
177
|
pure dependent chain: the plan ships as spec-only, with **no tickets**.
|
|
141
|
-
3. **`acceptance-spec.md`** —
|
|
142
|
-
the `acceptance::n-a` waiver label.
|
|
143
|
-
4. **`tickets.json`** — fan-out shape only:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
**no** tickets file — the Delivery Slicing table is the plan.
|
|
178
|
+
3. **`acceptance-spec.md`** — per `systemPrompts.acceptance`; omit only
|
|
179
|
+
when the Epic carries the `acceptance::n-a` waiver label.
|
|
180
|
+
4. **`tickets.json`** — fan-out shape only: author against the same
|
|
181
|
+
envelope (its `systemPrompts.decompose`, `ticketSchema`, and
|
|
182
|
+
`maxTickets` fields). In single-delivery shape author **no** tickets
|
|
183
|
+
file — the Delivery Slicing table is the plan.
|
|
148
184
|
|
|
149
185
|
> **One-pass refinement contract (amend, don't regenerate).** When a critic
|
|
150
186
|
> flags Stories or the step 3 persist rejects an artifact, apply **targeted
|
|
@@ -153,8 +189,12 @@ skills name — read the envelope wherever a skill asks for either.
|
|
|
153
189
|
|
|
154
190
|
### Conditional critics (between authoring and gate #2)
|
|
155
191
|
|
|
156
|
-
|
|
157
|
-
|
|
192
|
+
The evaluation is folded into `plan-persist.js` as a deterministic
|
|
193
|
+
pre-write phase (#4496 fix 6) — the persist prints both verdicts, returns
|
|
194
|
+
them on its result envelope, and ledger-logs every skip. **Attended runs**
|
|
195
|
+
evaluate the dispatch conditions before gate #2 so the verdicts fold into
|
|
196
|
+
its view — one git-local CLI call, zero GitHub reads, via the thin shim
|
|
197
|
+
(kept one release over the shared evaluator):
|
|
158
198
|
|
|
159
199
|
```bash
|
|
160
200
|
node .agents/scripts/plan-critics.js --epic [Epic_ID]
|
|
@@ -167,6 +207,14 @@ one-line note. Every skip decision is appended to the plan-metrics ledger
|
|
|
167
207
|
(`kind: "critic-skip"`, with reasons) so under-firing is auditable — the
|
|
168
208
|
persist validators remain unchanged hard gates either way.
|
|
169
209
|
|
|
210
|
+
> **`--yes` (headless).** Do **not** run the standalone CLI and do **not**
|
|
211
|
+
> dispatch critic sub-agents: the critics' findings would fold into a gate
|
|
212
|
+
> that auto-proceeds, so a report nobody reviews is pure spend. The
|
|
213
|
+
> persist's folded pre-write evaluation is the audit record — its verdicts
|
|
214
|
+
> print in the persist output, a `dispatch: true` verdict surfaces as a
|
|
215
|
+
> one-line advisory note in the run summary, and every skip still lands on
|
|
216
|
+
> the plan-metrics ledger.
|
|
217
|
+
|
|
170
218
|
Both critics are **fresh-context sub-agents** (`Agent` tool,
|
|
171
219
|
`subagent_type: general-purpose`) — never inline skill activations, so they
|
|
172
220
|
cannot grade their own homework. Both are report-only: they never write to
|
|
@@ -253,6 +301,24 @@ comment carrying the dry-run wave table → temp cleanup **only at terminal
|
|
|
253
301
|
success**, so a failed run leaves the artifacts in place for `--force` /
|
|
254
302
|
`--resume` reuse.
|
|
255
303
|
|
|
304
|
+
Two deterministic softenings ride the gate list:
|
|
305
|
+
|
|
306
|
+
- The file-assumption gate **auto-normalizes** a `refactors-existing`
|
|
307
|
+
declaration on a base-untracked path to `creates` with a logged warning
|
|
308
|
+
(#4496 fix 5) — a refactor of a path that does not exist is
|
|
309
|
+
deterministically a create, so no amend cycle is forced. Genuine
|
|
310
|
+
mismatches (an absent read dependency, a clobbering `creates`, a missing
|
|
311
|
+
`deletes` target) still reject.
|
|
312
|
+
- Every auto-waiver the persist derives is printed **with its reason** in
|
|
313
|
+
the `plan-summary` comment and the result JSON (#4496 fix 2) — e.g. the
|
|
314
|
+
no-BDD-runner acceptance-disposition waiver.
|
|
315
|
+
|
|
316
|
+
> **`--yes` (headless): persist outcomes are authoritative.** Read the
|
|
317
|
+
> persist's result JSON and `plan-summary` receipts as the final word — do
|
|
318
|
+
> **not** re-derive a waiver, disposition, or routing decision from
|
|
319
|
+
> framework source. If a summary line seems surprising, its reason is on
|
|
320
|
+
> the line itself; spend zero turns re-verifying it.
|
|
321
|
+
|
|
256
322
|
### Persist rejections and soft failures
|
|
257
323
|
|
|
258
324
|
Each rejection names the artifact and the gap; apply the one-pass amend
|
|
@@ -87,6 +87,15 @@ meanings; it only forces the Recommended resolution where the gate would
|
|
|
87
87
|
otherwise STOP. See
|
|
88
88
|
[`plan.md` § Headless / non-interactive mode](../plan.md#headless--non-interactive-mode---yes).
|
|
89
89
|
|
|
90
|
+
**Headless seed entry — the verdict is already in the envelope (#4496).**
|
|
91
|
+
On the `--idea` `--yes` path the rubric is applied **CLI-side** by
|
|
92
|
+
`plan-context.js --seed` and shipped as the envelope's `scopeTriage` field
|
|
93
|
+
(`{ verdict, reasons, advisory: true, appliedBy: "cli" }`), anchored to the
|
|
94
|
+
same sizing SSOT this skill anchors to. Do **not** Read the
|
|
95
|
+
`core/scope-triage` skill headless — use the envelope verdict and resolve it
|
|
96
|
+
per this section. The attended paths keep the skill-based judgment
|
|
97
|
+
unchanged.
|
|
98
|
+
|
|
90
99
|
## No-re-triage rule
|
|
91
100
|
|
|
92
101
|
A **scope-triage handoff** is a triage decision *already made*. When `/plan` is
|
|
@@ -88,7 +88,10 @@ exactly **two** HITL STOP gates, and `--yes` deterministically auto-proceeds
|
|
|
88
88
|
exactly one bounded pass**: no operator questions are asked — facts come
|
|
89
89
|
from the codebase, and every unresolved unknown lands in the one-pager's
|
|
90
90
|
**Key Assumptions** section instead of a question, so a headless driver
|
|
91
|
-
can never hang inside a free-form interrogation.
|
|
91
|
+
can never hang inside a free-form interrogation. On the `--idea` Epic
|
|
92
|
+
path this bounded pass IS the seed entry (#4496): `plan-context.js
|
|
93
|
+
--seed` replaces the idea-refinement prelude, and the one-pager is
|
|
94
|
+
authored in the same batched write as the spec artifacts. The verdict / clarity
|
|
92
95
|
scoring is still recorded in chat (one line); only the *wait* is
|
|
93
96
|
suppressed — the deterministic clarity *scoring* inside the
|
|
94
97
|
`plan-context.js` envelope always runs.
|
|
@@ -126,15 +129,24 @@ advisory critic diffs — auto-proceed for the same headless reason.
|
|
|
126
129
|
1. **Parse args.** Exactly one of `<epicId>`, `--idea`, `--from-notes`, or
|
|
127
130
|
`--body` must be present; anything else is a usage error naming the four
|
|
128
131
|
forms. A `--body` invocation routes to the story path (no triage).
|
|
129
|
-
2. **Triage (idea path only).**
|
|
132
|
+
2. **Triage (idea path only).** Attended: run the
|
|
130
133
|
[`core/scope-triage`](../skills/core/scope-triage/SKILL.md) skill on the
|
|
131
|
-
seed. Record the verdict in chat (one line).
|
|
134
|
+
seed. Record the verdict in chat (one line). **Under `--yes`, do not
|
|
135
|
+
Read the skill**: delegate straight to
|
|
136
|
+
[`helpers/plan-epic.md`](helpers/plan-epic.md)'s ideation entry — its
|
|
137
|
+
`plan-context.js --seed` envelope carries the rubric's verdict applied
|
|
138
|
+
CLI-side (`scopeTriage`), and a `story` / `borderline` verdict resolves
|
|
139
|
+
to the Recommended handoff from inside the helper (see
|
|
140
|
+
[`helpers/scope-triage-gate.md`](helpers/scope-triage-gate.md)).
|
|
132
141
|
3. **Delegate.** Read the selected path helper **in full** and execute it
|
|
133
142
|
from its entry, forwarding the absorbed flags (including `--yes`). The
|
|
134
143
|
helper's steps, HITL gates, and scripts are the procedure — this router
|
|
135
144
|
adds no step content. When `--yes` is present, the two HITL STOP gates
|
|
136
145
|
auto-proceed per [Headless / non-interactive mode](#headless--non-interactive-mode---yes)
|
|
137
|
-
above; every deterministic gate still runs.
|
|
146
|
+
above; every deterministic gate still runs. **Under `--yes`, when the
|
|
147
|
+
helper content is already injected or present in context, execute it
|
|
148
|
+
directly — do not spend a separate read-in-full turn re-reading content
|
|
149
|
+
you already hold.**
|
|
138
150
|
4. **Internal returns.** When a path helper would historically have handed
|
|
139
151
|
off to the other planning command, switch helpers in-place and continue;
|
|
140
152
|
surface the switch to the operator as a one-line note.
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.93.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.92.0...mandrel-v1.93.0) (2026-07-12)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
* **plan:** close the measured turn gap — seed-mode ideation, authoritative summaries, batched writes (refs [#4496](https://github.com/dsj1984/mandrel/issues/4496)) ([#4497](https://github.com/dsj1984/mandrel/issues/4497)) ([ae98701](https://github.com/dsj1984/mandrel/commit/ae987014d68669eb0032a68000ed328c7a3c702d))
|
|
11
|
+
|
|
5
12
|
## [1.92.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.91.0...mandrel-v1.92.0) (2026-07-12)
|
|
6
13
|
|
|
7
14
|
|
package/package.json
CHANGED