mandrel 2.10.0 → 2.12.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/rules/orchestration-error-handling.md +9 -1
- package/.agents/schemas/agentrc.schema.json +13 -8
- package/.agents/scripts/acceptance-eval.js +9 -5
- package/.agents/scripts/lib/audit-suite/audit-rules-reader.js +48 -0
- package/.agents/scripts/lib/audit-suite/selector.js +1 -26
- 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 +561 -184
- package/.agents/scripts/lib/orchestration/plan-context.js +69 -10
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +117 -60
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +21 -15
- package/.agents/scripts/lib/orchestration/resolve-stories.js +28 -7
- package/.agents/scripts/lib/orchestration/review-depth.js +9 -4
- package/.agents/scripts/lib/orchestration/single-story-close/gate-log.js +186 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +21 -3
- 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 +28 -28
- package/.agents/workflows/helpers/acceptance-self-eval.md +16 -5
- package/.agents/workflows/helpers/deliver-digest.md +126 -0
- package/.agents/workflows/helpers/deliver-reference.md +30 -5
- package/.agents/workflows/helpers/deliver-story-reference.md +38 -12
- package/.agents/workflows/helpers/deliver-story.md +34 -37
- package/.agents/workflows/helpers/plan-reference.md +79 -44
- package/.agents/workflows/plan.md +11 -10
- package/docs/CHANGELOG.md +31 -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
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
renderTechSpecSystemPrompt,
|
|
27
27
|
} from '../templates/spec-author-prompts.js';
|
|
28
28
|
import { concurrentMap } from '../util/concurrent-map.js';
|
|
29
|
-
import {
|
|
29
|
+
import { buildComplexitySignals } from './complexity-gate.js';
|
|
30
30
|
import { parseDeliverySlicingTable } from './consolidation-precondition.js';
|
|
31
31
|
import { buildDocsDigest } from './docs-digest.js';
|
|
32
32
|
import { buildAuthoringContext } from './planning/authoring-context.js';
|
|
@@ -144,6 +144,41 @@ export const TICKET_SCHEMA_DESCRIPTOR = Object.freeze({
|
|
|
144
144
|
*/
|
|
145
145
|
export const STORIES_TEMPLATE_FILENAME = 'stories.template.json';
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Build the template's `changes[]` entries from the envelope's advisory
|
|
149
|
+
* complexity signals (Story #4723). Each seed-predicted path is
|
|
150
|
+
* pre-resolved to its creates-vs-refactors assumption against the repo
|
|
151
|
+
* snapshot the signals already probed: a path present in the repo is a
|
|
152
|
+
* `refactors-existing`, a missing one is a `creates`. Order follows
|
|
153
|
+
* `predictedPaths` (first appearance in the seed). Falls back to the
|
|
154
|
+
* single instructive placeholder entry when the seed predicted no paths.
|
|
155
|
+
*
|
|
156
|
+
* @param {{
|
|
157
|
+
* predictedPaths?: string[],
|
|
158
|
+
* repoState?: { existingPaths?: string[], missingPaths?: string[] },
|
|
159
|
+
* }|null|undefined} complexitySignals
|
|
160
|
+
* @returns {Array<{ path: string, assumption: string }>}
|
|
161
|
+
*/
|
|
162
|
+
function buildTemplateChanges(complexitySignals) {
|
|
163
|
+
const predicted = Array.isArray(complexitySignals?.predictedPaths)
|
|
164
|
+
? complexitySignals.predictedPaths.filter(
|
|
165
|
+
(p) => typeof p === 'string' && p.length > 0,
|
|
166
|
+
)
|
|
167
|
+
: [];
|
|
168
|
+
if (predicted.length === 0) {
|
|
169
|
+
return [{ path: 'path/to/file.ext', assumption: 'refactors-existing' }];
|
|
170
|
+
}
|
|
171
|
+
const existing = new Set(
|
|
172
|
+
Array.isArray(complexitySignals?.repoState?.existingPaths)
|
|
173
|
+
? complexitySignals.repoState.existingPaths
|
|
174
|
+
: [],
|
|
175
|
+
);
|
|
176
|
+
return predicted.map((path) => ({
|
|
177
|
+
path,
|
|
178
|
+
assumption: existing.has(path) ? 'refactors-existing' : 'creates',
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
|
|
147
182
|
/**
|
|
148
183
|
* Render the ready-to-fill `stories.json` authoring template (Story #4707).
|
|
149
184
|
*
|
|
@@ -158,12 +193,23 @@ export const STORIES_TEMPLATE_FILENAME = 'stories.template.json';
|
|
|
158
193
|
* / `verify[]` live at the ticket's top level — the machine contract persist
|
|
159
194
|
* syncs into the body.
|
|
160
195
|
*
|
|
196
|
+
* Correct-by-construction skeleton (Story #4723): the emitted `verify[]`
|
|
197
|
+
* placeholder already ends with a valid `(tier)` tag (swap `(unit)` for
|
|
198
|
+
* `(contract)` / `(e2e)` / `(validate)` where appropriate), and when the
|
|
199
|
+
* envelope's `complexitySignals` predicted a footprint the `changes[]`
|
|
200
|
+
* entries arrive pre-resolved to creates-vs-refactors against the repo
|
|
201
|
+
* snapshot — a faithfully-filled skeleton passes the persist ticket
|
|
202
|
+
* validators without a mechanical round-trip. The persist gates stay
|
|
203
|
+
* authoritative (they probe the base branch ref, not the working tree).
|
|
204
|
+
*
|
|
161
205
|
* Pure and deterministic; the output is valid JSON (parseable as-is), with
|
|
162
206
|
* instructive placeholder values rather than comments.
|
|
163
207
|
*
|
|
208
|
+
* @param {{ complexitySignals?: object|null }} [opts] Envelope signals to
|
|
209
|
+
* pre-resolve the skeleton against; omit for the bare placeholder shape.
|
|
164
210
|
* @returns {string} Pretty-printed JSON template content.
|
|
165
211
|
*/
|
|
166
|
-
export function renderStoriesTemplate() {
|
|
212
|
+
export function renderStoriesTemplate({ complexitySignals = null } = {}) {
|
|
167
213
|
const template = [
|
|
168
214
|
{
|
|
169
215
|
slug: 'fill-hyphen-case-slug',
|
|
@@ -176,11 +222,9 @@ export function renderStoriesTemplate() {
|
|
|
176
222
|
'codes, security invariants, and load-bearing constraints with ' +
|
|
177
223
|
'their why. Implementation choices belong to the deliverer unless ' +
|
|
178
224
|
'load-bearing. No per-file behavior paragraphs, no current-state ' +
|
|
179
|
-
'narration.
|
|
180
|
-
'contract.',
|
|
181
|
-
changes:
|
|
182
|
-
{ path: 'path/to/file.ext', assumption: 'refactors-existing' },
|
|
183
|
-
],
|
|
225
|
+
'narration. Keep it under ~250 words (soft advisory budget). ' +
|
|
226
|
+
'Delete this field when acceptance[] carries the whole contract.',
|
|
227
|
+
changes: buildTemplateChanges(complexitySignals),
|
|
184
228
|
non_goals: [],
|
|
185
229
|
reason_to_exist:
|
|
186
230
|
'Fill: the single coherent reason this Story exists (one sentence).',
|
|
@@ -188,7 +232,9 @@ export function renderStoriesTemplate() {
|
|
|
188
232
|
acceptance: [
|
|
189
233
|
'Fill: a testable, observable criterion (a command exits 0, a file exists, a test matches)',
|
|
190
234
|
],
|
|
191
|
-
verify: [
|
|
235
|
+
verify: [
|
|
236
|
+
'Fill: exact command or test path — keep the trailing tier tag valid: unit, contract, e2e, or validate (unit)',
|
|
237
|
+
],
|
|
192
238
|
depends_on: [],
|
|
193
239
|
},
|
|
194
240
|
];
|
|
@@ -540,7 +586,15 @@ async function buildSeedFileModeEnvelope({
|
|
|
540
586
|
return {
|
|
541
587
|
mode: modeLabel,
|
|
542
588
|
seed: { path: seedFilePath ?? null, content },
|
|
543
|
-
|
|
589
|
+
// Advisory complexity signals only (Story #4722): no route, no routing
|
|
590
|
+
// authority. The planner authors the trivial-vs-standard verdict; persist
|
|
591
|
+
// validates a lite claim against the authored Story's shape.
|
|
592
|
+
complexitySignals: buildComplexitySignals({
|
|
593
|
+
seedText: content,
|
|
594
|
+
config,
|
|
595
|
+
riskHeuristics: heuristics,
|
|
596
|
+
cwd,
|
|
597
|
+
}),
|
|
544
598
|
duplicates,
|
|
545
599
|
docsContext,
|
|
546
600
|
codebaseSnapshot: authoring.codebaseSnapshot,
|
|
@@ -694,7 +748,12 @@ async function buildTicketsModeEnvelope({
|
|
|
694
748
|
mode: 'tickets',
|
|
695
749
|
sourceTickets,
|
|
696
750
|
seed: { text: seed, path: null },
|
|
697
|
-
|
|
751
|
+
complexitySignals: buildComplexitySignals({
|
|
752
|
+
seedText: seed,
|
|
753
|
+
config,
|
|
754
|
+
riskHeuristics: heuristics,
|
|
755
|
+
cwd,
|
|
756
|
+
}),
|
|
698
757
|
duplicates,
|
|
699
758
|
docsContext,
|
|
700
759
|
codebaseSnapshot: authoring.codebaseSnapshot,
|
|
@@ -44,7 +44,12 @@ import { anchorTempRoot, tempRootFrom } from '../../config/temp-paths.js';
|
|
|
44
44
|
import { getLimits, PROJECT_ROOT } from '../../config-resolver.js';
|
|
45
45
|
import { gitSpawn } from '../../git-utils.js';
|
|
46
46
|
import { Logger } from '../../Logger.js';
|
|
47
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
deriveStoryShape,
|
|
49
|
+
LITE_ROUTE_LABEL,
|
|
50
|
+
resolveComplexityGate,
|
|
51
|
+
resolvePlannerRouteVerdict,
|
|
52
|
+
} from '../complexity-gate.js';
|
|
48
53
|
import {
|
|
49
54
|
appendCriticSkip,
|
|
50
55
|
readPlanMetrics,
|
|
@@ -264,65 +269,108 @@ async function renderRunScopedPlanMetricsLine({
|
|
|
264
269
|
|
|
265
270
|
/**
|
|
266
271
|
* Resolve the plan's **effective** complexity route for persist
|
|
267
|
-
* (Story #4707).
|
|
272
|
+
* (Story #4722, superseding the envelope-verdict model of Story #4707).
|
|
273
|
+
*
|
|
274
|
+
* Two staged inputs, no word count anywhere:
|
|
268
275
|
*
|
|
269
|
-
* The
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
276
|
+
* 1. **The planner's authored verdict** — `--route-downgrade-reason` is the
|
|
277
|
+
* lite claim's recorded reason (`resolvePlannerRouteVerdict`). No
|
|
278
|
+
* recorded reason means no claim: the plan persists as standard `full`
|
|
279
|
+
* and nothing is ledgered (`null`).
|
|
280
|
+
* 2. **The deterministic shape backstop** — a lite claim is validated
|
|
281
|
+
* against every assembled Story's own shape (`deriveStoryShape` over its
|
|
282
|
+
* `changes[]`, acceptance count, creates-vs-refactors mix, and
|
|
283
|
+
* sensitive-path classes). Any Story exceeding the ceilings **fails the
|
|
284
|
+
* claim closed to `full`** — the honest gate: after authoring, the work
|
|
285
|
+
* has measurable shape, so complexity is read from the work, not guessed
|
|
286
|
+
* from the seed.
|
|
275
287
|
*
|
|
276
288
|
* The resolved route decides whether the created Stories carry the
|
|
277
|
-
* {@link LITE_ROUTE_LABEL}
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
289
|
+
* {@link LITE_ROUTE_LABEL} **hint** (never the control signal — `/deliver`
|
|
290
|
+
* re-derives the route from the Story body's shape) and the `route` block
|
|
291
|
+
* ledgered on their `story-plan-state` checkpoint, including the authored
|
|
292
|
+
* verdict, its recorded reason, and the per-Story shape evidence. A refused
|
|
293
|
+
* claim is ledgered too (route `full` with the refusal reasons), so the
|
|
294
|
+
* judgment stays auditable either way.
|
|
281
295
|
*
|
|
282
296
|
* Module-private: reachable end to end through {@link runPlanPersist}
|
|
283
297
|
* (whose result reports the resolved route), so there is no test-only
|
|
284
298
|
* export to leave production-dead.
|
|
285
299
|
*
|
|
286
|
-
* @param {{
|
|
287
|
-
*
|
|
288
|
-
*
|
|
300
|
+
* @param {{
|
|
301
|
+
* stories: ReturnType<typeof assemblePlanStories>['stories'],
|
|
302
|
+
* routeDowngradeReason?: string|null,
|
|
303
|
+
* config?: object,
|
|
304
|
+
* injectedRules?: object,
|
|
305
|
+
* }} args
|
|
306
|
+
* @returns {{
|
|
307
|
+
* route: 'lite'|'full',
|
|
308
|
+
* reasons: string[],
|
|
309
|
+
* authored: { route: 'lite', reason: string },
|
|
310
|
+
* shape: Array<{ slug: string, route: string, reasons: string[], shape: object|null }>,
|
|
311
|
+
* }|null} `null` when the planner authored no verdict (nothing to persist).
|
|
289
312
|
*/
|
|
290
313
|
function resolveEffectiveRoute({
|
|
291
|
-
|
|
314
|
+
stories,
|
|
292
315
|
routeDowngradeReason = null,
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
316
|
+
config = {},
|
|
317
|
+
injectedRules,
|
|
318
|
+
}) {
|
|
319
|
+
const verdict = resolvePlannerRouteVerdict({ reason: routeDowngradeReason });
|
|
320
|
+
if (verdict.route !== 'lite') return null;
|
|
321
|
+
|
|
322
|
+
// The schema's documented contract: with the gate disabled
|
|
323
|
+
// (`planning.complexityGate.enabled=false`), persist refuses lite claims —
|
|
324
|
+
// the same switch dispatch reads (`resolveStoryDispatchMode` falls back to
|
|
325
|
+
// sub-agent), so neither read point can honor a lite claim the operator
|
|
326
|
+
// has switched off. The refusal is ledgered like any other, keeping the
|
|
327
|
+
// judgment auditable.
|
|
328
|
+
if (!resolveComplexityGate(config).enabled) {
|
|
329
|
+
return {
|
|
330
|
+
route: 'full',
|
|
331
|
+
reasons: [
|
|
332
|
+
'planner lite verdict refused: complexity routing is disabled ' +
|
|
333
|
+
'(planning.complexityGate.enabled=false)',
|
|
334
|
+
],
|
|
335
|
+
authored: verdict.authored,
|
|
336
|
+
shape: [],
|
|
337
|
+
};
|
|
308
338
|
}
|
|
309
|
-
|
|
310
|
-
|
|
339
|
+
|
|
340
|
+
const perStory = (Array.isArray(stories) ? stories : []).map((story) => {
|
|
341
|
+
const derived = deriveStoryShape({
|
|
342
|
+
changes: story.bodyObject?.changes,
|
|
343
|
+
acceptance: story.acceptance,
|
|
344
|
+
injectedRules,
|
|
345
|
+
});
|
|
346
|
+
return {
|
|
347
|
+
slug: story.slug,
|
|
348
|
+
route: derived.route,
|
|
349
|
+
reasons: derived.reasons,
|
|
350
|
+
shape: derived.shape,
|
|
351
|
+
};
|
|
311
352
|
});
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
353
|
+
const offenders = perStory.filter((entry) => entry.route !== 'lite');
|
|
354
|
+
if (offenders.length > 0) {
|
|
355
|
+
return {
|
|
356
|
+
route: 'full',
|
|
357
|
+
reasons: [
|
|
358
|
+
`planner lite verdict refused: ${offenders.length} of ${perStory.length} ` +
|
|
359
|
+
'Story(ies) exceed the lite shape ceilings — failing closed to full',
|
|
360
|
+
...offenders.map((entry) => `${entry.slug}: ${entry.reasons[0]}`),
|
|
361
|
+
],
|
|
362
|
+
authored: verdict.authored,
|
|
363
|
+
shape: perStory,
|
|
364
|
+
};
|
|
321
365
|
}
|
|
322
366
|
return {
|
|
323
|
-
route:
|
|
324
|
-
reasons:
|
|
325
|
-
|
|
367
|
+
route: 'lite',
|
|
368
|
+
reasons: [
|
|
369
|
+
...verdict.reasons,
|
|
370
|
+
'shape backstop: every authored Story fits the lite shape ceilings',
|
|
371
|
+
],
|
|
372
|
+
authored: verdict.authored,
|
|
373
|
+
shape: perStory,
|
|
326
374
|
};
|
|
327
375
|
}
|
|
328
376
|
|
|
@@ -410,6 +458,7 @@ export async function reapStalePlanDirs({
|
|
|
410
458
|
* sourceTicketOrigin?: 'flag'|'envelope'|'none',
|
|
411
459
|
* closeSuperseded?: boolean,
|
|
412
460
|
* routeDowngradeReason?: string|null,
|
|
461
|
+
* injectedRules?: object,
|
|
413
462
|
* },
|
|
414
463
|
* }} input
|
|
415
464
|
*/
|
|
@@ -424,7 +473,6 @@ export async function runPlanPersist({
|
|
|
424
473
|
stories: rawStories = null,
|
|
425
474
|
techSpecContent = null,
|
|
426
475
|
planAcceptance = null,
|
|
427
|
-
planContextEnvelope = null,
|
|
428
476
|
} = artifacts ?? {};
|
|
429
477
|
const {
|
|
430
478
|
forceReview = false,
|
|
@@ -439,6 +487,7 @@ export async function runPlanPersist({
|
|
|
439
487
|
sourceTicketOrigin = 'none',
|
|
440
488
|
closeSuperseded = true,
|
|
441
489
|
routeDowngradeReason = null,
|
|
490
|
+
injectedRules = undefined,
|
|
442
491
|
} = opts;
|
|
443
492
|
|
|
444
493
|
// Boundary for the plan-metrics summary below: everything this invocation
|
|
@@ -513,22 +562,29 @@ export async function runPlanPersist({
|
|
|
513
562
|
sourceTicketIds,
|
|
514
563
|
});
|
|
515
564
|
|
|
516
|
-
// Effective complexity route (Story #
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
// persists
|
|
565
|
+
// Effective complexity route (Story #4722): the planner's authored lite
|
|
566
|
+
// verdict (recorded reason), validated against every assembled Story's own
|
|
567
|
+
// shape — a claim exceeding the shape ceilings fails closed to full. Lite
|
|
568
|
+
// persists the `route::lite` HINT label + a checkpoint route block; a
|
|
569
|
+
// refused claim ledgers the refusal (no label); no claim persists nothing.
|
|
520
570
|
const route = resolveEffectiveRoute({
|
|
521
|
-
|
|
571
|
+
stories,
|
|
522
572
|
routeDowngradeReason,
|
|
573
|
+
config,
|
|
574
|
+
injectedRules,
|
|
523
575
|
});
|
|
524
576
|
const isLiteRoute = route?.route === 'lite';
|
|
525
577
|
if (isLiteRoute) {
|
|
526
578
|
Logger.info(
|
|
527
|
-
`[plan-persist] ceremony-lite route
|
|
528
|
-
|
|
529
|
-
(route.
|
|
530
|
-
|
|
531
|
-
|
|
579
|
+
`[plan-persist] ceremony-lite route upheld by the shape backstop: ` +
|
|
580
|
+
`created Stories carry the ${LITE_ROUTE_LABEL} hint ` +
|
|
581
|
+
`(recorded reason: ${route.authored.reason}). /deliver re-derives ` +
|
|
582
|
+
'the route from each Story body — the label is never the control signal.',
|
|
583
|
+
);
|
|
584
|
+
} else if (route) {
|
|
585
|
+
Logger.warn(
|
|
586
|
+
`[plan-persist] ${route.reasons.join('; ')} — persisting as full ` +
|
|
587
|
+
'(no route hint label).',
|
|
532
588
|
);
|
|
533
589
|
}
|
|
534
590
|
|
|
@@ -586,10 +642,11 @@ export async function runPlanPersist({
|
|
|
586
642
|
id: createdStory.id,
|
|
587
643
|
})),
|
|
588
644
|
},
|
|
589
|
-
// Ledger the
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
|
|
645
|
+
// Ledger the authored route verdict — the recorded reason and the
|
|
646
|
+
// per-Story shape evidence, including a shape-refused claim — on plan
|
|
647
|
+
// state (Story #4722). No authored verdict writes no block: absence
|
|
648
|
+
// is the standard full path.
|
|
649
|
+
...(route ? { route } : {}),
|
|
593
650
|
});
|
|
594
651
|
}
|
|
595
652
|
await upsertStructuredComment(
|
|
@@ -46,7 +46,11 @@ export const PLAN_RUN_LABEL_PREFIX = 'plan-run::';
|
|
|
46
46
|
/** Stable color for the cohort grouping label (`ensureLabels`). */
|
|
47
47
|
const PLAN_RUN_LABEL_COLOR = '#C5DEF5';
|
|
48
48
|
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Stable color for the `route::lite` ceremony-route hint (Story #4707;
|
|
51
|
+
* hint-only since Story #4722 — `/deliver` re-derives the route from the
|
|
52
|
+
* Story body's shape).
|
|
53
|
+
*/
|
|
50
54
|
const LITE_ROUTE_LABEL_COLOR = '#D4C5F9';
|
|
51
55
|
|
|
52
56
|
/** Length of the derived plan-run id (hex chars). */
|
|
@@ -694,9 +698,10 @@ async function mirrorNativeDependencyEdges({ provider, stories, idBySlug }) {
|
|
|
694
698
|
* route, and an opaque derived label never exists yet.
|
|
695
699
|
*
|
|
696
700
|
* **Non-fatal by design**, matching the native-blocked_by mirroring posture:
|
|
697
|
-
* neither label is load-bearing for correctness (grouping is cosmetic
|
|
698
|
-
*
|
|
699
|
-
*
|
|
701
|
+
* neither label is load-bearing for correctness (grouping is cosmetic, and
|
|
702
|
+
* the route label is a human-visible hint only — `/deliver` re-derives the
|
|
703
|
+
* route from the Story body's shape, Story #4722), so it is never a reason
|
|
704
|
+
* to fail persist. On an ensure
|
|
700
705
|
* failure (throw, or the label reported `missing` by the post-loop
|
|
701
706
|
* reconcile) the create loop proceeds **without** the label — applying an
|
|
702
707
|
* unensured label could fail the issue create itself, and the Stories matter
|
|
@@ -782,15 +787,16 @@ async function ensurePersistLabel({
|
|
|
782
787
|
* every id is known (Story #4544), so plan-created order stops depending on
|
|
783
788
|
* prose. That pass is non-fatal — see `mirrorNativeDependencyEdges`.
|
|
784
789
|
*
|
|
785
|
-
* **A lite-routed cohort carries the `route::lite`
|
|
786
|
-
* When the caller resolves the plan's
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
790
|
+
* **A lite-routed cohort carries the `route::lite` hint** (Story #4707,
|
|
791
|
+
* hint-only since Story #4722). When the caller resolves the plan's
|
|
792
|
+
* effective complexity route to `lite` (the planner's recorded verdict,
|
|
793
|
+
* upheld by the shape backstop), it passes the label via `opts.routeLabel`
|
|
794
|
+
* and every created Story carries it — a **human-visible hint only**, never
|
|
795
|
+
* the control signal: `/deliver` re-derives the route from each Story body's
|
|
796
|
+
* own shape (`resolveStoryDispatchMode`), so a lost or failed label write
|
|
797
|
+
* cannot misroute delivery. A full-routed plan passes nothing and its
|
|
798
|
+
* Stories carry **no** route label. Like the cohort label, the ensure is
|
|
799
|
+
* non-fatal — the label is cosmetic either way.
|
|
794
800
|
*
|
|
795
801
|
* @param {object} args
|
|
796
802
|
* @param {object} args.provider
|
|
@@ -857,8 +863,8 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
857
863
|
label: routeLabel,
|
|
858
864
|
color: LITE_ROUTE_LABEL_COLOR,
|
|
859
865
|
description:
|
|
860
|
-
'Ceremony-lite
|
|
861
|
-
'
|
|
866
|
+
'Ceremony-lite hint (human-visible only): /deliver re-derives the ' +
|
|
867
|
+
'route from the Story body shape; every close gate runs unchanged.',
|
|
862
868
|
role: 'route-marker',
|
|
863
869
|
}));
|
|
864
870
|
|
|
@@ -300,6 +300,9 @@ export async function readNativeBlockedBy({
|
|
|
300
300
|
* @param {Map<number, number[]>} nativeEdges
|
|
301
301
|
* @param {number[]} foreignDone Ids outside the set already satisfied.
|
|
302
302
|
* @param {(msg: string) => void} [warn]
|
|
303
|
+
* @param {object} [injectedRules] Test seam forwarded to the shape
|
|
304
|
+
* derivation — skips the `audit-rules.json` disk read. Production callers
|
|
305
|
+
* omit it (the real manifest, memoized per process, is the default).
|
|
303
306
|
* @returns {{ kind: string, stories: object[], dag: object[], done: number[] }}
|
|
304
307
|
*/
|
|
305
308
|
export function buildStoriesEnvelope({
|
|
@@ -307,23 +310,41 @@ export function buildStoriesEnvelope({
|
|
|
307
310
|
nativeEdges = new Map(),
|
|
308
311
|
foreignDone = [],
|
|
309
312
|
warn,
|
|
313
|
+
config,
|
|
314
|
+
injectedRules,
|
|
310
315
|
}) {
|
|
311
316
|
const sorted = [...stories].sort((a, b) => a.id - b.id);
|
|
312
317
|
const inSetDone = sorted.filter(isSatisfiedBlocker).map((s) => s.id);
|
|
313
318
|
return {
|
|
314
319
|
kind: 'stories',
|
|
315
|
-
// `dispatchMode` (Story #
|
|
316
|
-
// execution mode from the
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
320
|
+
// `dispatchMode` (Story #4722): the resolver derives the per-Story
|
|
321
|
+
// execution mode from the fetched Story BODY's own shape (the shared
|
|
322
|
+
// shape function in `complexity-gate.js`) so `/deliver` reads one field —
|
|
323
|
+
// `inline` (lite-shaped: no story-worker / acceptance-critic sub-agent
|
|
324
|
+
// boots) or `subagent` (everything else, the conservative default). The
|
|
325
|
+
// `route::lite` label is a human-visible hint only, never the control
|
|
326
|
+
// signal: a lost label cannot misroute delivery. Model-side fan-out
|
|
327
|
+
// only; close gates are untouched.
|
|
328
|
+
//
|
|
329
|
+
// `storyCount` (Story #4736) carries the run's topology into that same
|
|
330
|
+
// decision: a run resolving exactly ONE Story is inline whatever its
|
|
331
|
+
// shape, because the isolation a sub-agent buys only matters against a
|
|
332
|
+
// concurrently-dispatched sibling. It is the resolved set size — not the
|
|
333
|
+
// undelivered remainder — so the mode a caller reads for a given `--ids`
|
|
334
|
+
// list never changes as siblings land mid-run.
|
|
335
|
+
stories: sorted.map(({ id, title, body, url, labels, state }) => ({
|
|
321
336
|
id,
|
|
322
337
|
title,
|
|
323
338
|
url,
|
|
324
339
|
labels,
|
|
325
340
|
state,
|
|
326
|
-
dispatchMode: resolveStoryDispatchMode({
|
|
341
|
+
dispatchMode: resolveStoryDispatchMode({
|
|
342
|
+
body,
|
|
343
|
+
labels,
|
|
344
|
+
config,
|
|
345
|
+
storyCount: sorted.length,
|
|
346
|
+
injectedRules,
|
|
347
|
+
}).mode,
|
|
327
348
|
})),
|
|
328
349
|
dag: storiesToDag(sorted, nativeEdges, warn),
|
|
329
350
|
done: [...new Set([...inSetDone, ...foreignDone])].sort((a, b) => a - b),
|
|
@@ -74,10 +74,15 @@ export const DEFAULT_DIFF_WIDTH = Object.freeze({
|
|
|
74
74
|
* changed-file set intersect any sensitive-path class registered in
|
|
75
75
|
* `audit-rules.json`?
|
|
76
76
|
*
|
|
77
|
-
* This is the **single source** of the derived level —
|
|
78
|
-
* ({@link resolveDepth})
|
|
79
|
-
* (`ceremony-routing.js#resolveCeremonyForRisk`)
|
|
80
|
-
*
|
|
77
|
+
* This is the **single source** of the derived level — the review depth
|
|
78
|
+
* ({@link resolveDepth}), the acceptance-critic fresh-vs-inline routing
|
|
79
|
+
* (`ceremony-routing.js#resolveCeremonyForRisk`), and the dispatch-side
|
|
80
|
+
* complexity routing (`complexity-gate.js#deriveStoryShape`, Story #4722)
|
|
81
|
+
* all consume what this returns, so no ceremony decision can disagree about
|
|
82
|
+
* how risky a change is. Dispatch reads the **predicted** shape (the Story's
|
|
83
|
+
* declared `changes[]` footprint) and close reads the **actual** diff — one
|
|
84
|
+
* taxonomy, two read points, which is what keeps a lite-shaped Story whose
|
|
85
|
+
* footprint touches a sensitive path on the full route with its fresh critic.
|
|
81
86
|
*
|
|
82
87
|
* Returns `null` — the fail-safe "no derivable signal" level — when the change
|
|
83
88
|
* set is empty/unknown or the manifest cannot be read. Both downstream
|