mandrel 2.56.0 → 2.57.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/agents/plan-critic.md +13 -18
- package/.agents/agents/story-worker.md +25 -34
- package/.agents/docs/agentrc-reference.json +0 -30
- package/.agents/docs/configuration.md +8 -28
- package/.agents/docs/execution-reference.md +5 -5
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/instructions.md +9 -10
- package/.agents/schemas/agentrc.schema.json +9 -185
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +107 -17
- package/.agents/scripts/ceremony-derive.js +191 -0
- package/.agents/scripts/check-context-budget.js +28 -33
- package/.agents/scripts/check-cyclomatic.js +4 -3
- package/.agents/scripts/deliver-light.js +31 -94
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
- package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
- package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
- package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
- package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
- package/.agents/scripts/lib/close-validation/gates.js +52 -1
- package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
- package/.agents/scripts/lib/config/delivery-routing.js +7 -33
- package/.agents/scripts/lib/config/explain.js +0 -19
- package/.agents/scripts/lib/config/limits.js +18 -78
- package/.agents/scripts/lib/config/quality.js +6 -3
- package/.agents/scripts/lib/config/runners.js +3 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
- package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
- package/.agents/scripts/lib/config-settings-schema.js +16 -143
- package/.agents/scripts/lib/crap-engine.js +35 -4
- package/.agents/scripts/lib/crap-utils.js +17 -1
- package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
- package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
- package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
- package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
- package/.agents/scripts/lib/orchestration/plan-context.js +181 -387
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +300 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +118 -297
- package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +30 -139
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
- package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
- package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
- package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
- package/.agents/scripts/lib/story-body/story-body.js +17 -237
- package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -121
- package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
- package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
- package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
- package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
- package/.agents/scripts/lib/test-run-credit.js +266 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
- package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
- package/.agents/scripts/lib/workers/crap-worker.js +32 -41
- package/.agents/scripts/plan-context.js +7 -9
- package/.agents/scripts/plan-critics.js +28 -54
- package/.agents/scripts/plan-persist.js +25 -68
- package/.agents/scripts/quality-preview.js +51 -0
- package/.agents/scripts/run-tests.js +12 -0
- package/.agents/scripts/stories-wave-tick.js +23 -45
- package/.agents/scripts/test-isolate.js +13 -180
- package/.agents/scripts/update-coverage-baseline.js +25 -70
- package/.agents/scripts/update-crap-baseline.js +19 -123
- package/.agents/skills/core/scope-triage/SKILL.md +3 -3
- package/.agents/workflows/audit-clean-code.md +4 -3
- package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
- package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
- package/.agents/workflows/helpers/code-review.md +2 -3
- package/.agents/workflows/helpers/deliver-digest.md +41 -57
- package/.agents/workflows/helpers/deliver-light.md +40 -105
- package/.agents/workflows/helpers/deliver-reference.md +1 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +37 -58
- package/.agents/workflows/helpers/deliver-story.md +9 -13
- package/.agents/workflows/helpers/plan-reference.md +132 -219
- package/.agents/workflows/mandrel-plan.md +27 -40
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +23 -0
- package/lib/migrations/index.js +4 -0
- package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
- package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/framework-version.js +0 -39
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
- package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
- package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* run-plan-persist.js — flat Story persist for the v2 `/mandrel-plan` collapse
|
|
3
3
|
* (Stage 3 — `docs/roadmap.md`).
|
|
4
4
|
*
|
|
5
|
-
* Ordered,
|
|
5
|
+
* Ordered pipeline — hard gates refuse, everything else is a **warning** the
|
|
6
|
+
* dry-run lists (Story #5312):
|
|
6
7
|
*
|
|
7
|
-
* 1.
|
|
8
|
+
* 1. `changes[]` repair + ticket validator + file-assumption + DAG
|
|
8
9
|
* 2. Draft reachability (named soft failure, exit 3)
|
|
9
|
-
* 3. Split-policy partition (`assertAcceptancePartition`) + spec fold
|
|
10
|
+
* 3. Split-policy partition (`assertAcceptancePartition`) + spec fold
|
|
10
11
|
* 4. Create Story issues (`type::story` + sanitized authored labels —
|
|
11
12
|
* deliberately NOT `agent::ready`), resumably via a plan fingerprint
|
|
12
13
|
* 5. Upsert `story-plan-state` on every created Story; upsert `plan-summary`
|
|
@@ -38,20 +39,13 @@
|
|
|
38
39
|
*/
|
|
39
40
|
|
|
40
41
|
import { rm } from 'node:fs/promises';
|
|
41
|
-
import {
|
|
42
|
-
import { gitSpawn } from '../../git-utils.js';
|
|
42
|
+
import { getPaths, PROJECT_ROOT } from '../../config-resolver.js';
|
|
43
43
|
import { Logger } from '../../Logger.js';
|
|
44
44
|
import { sweepTempRetention } from '../../temp-retention.js';
|
|
45
45
|
import {
|
|
46
46
|
concurrentMap,
|
|
47
47
|
FANOUT_CONCURRENCY,
|
|
48
48
|
} from '../../util/concurrent-map.js';
|
|
49
|
-
import {
|
|
50
|
-
deriveStoryShape,
|
|
51
|
-
LITE_ROUTE_LABEL,
|
|
52
|
-
resolveComplexityGate,
|
|
53
|
-
resolvePlannerRouteVerdict,
|
|
54
|
-
} from '../complexity-gate.js';
|
|
55
49
|
import {
|
|
56
50
|
appendCriticSkip,
|
|
57
51
|
readPlanMetrics,
|
|
@@ -62,22 +56,20 @@ import {
|
|
|
62
56
|
evaluateDraftReachability,
|
|
63
57
|
renderReachabilityOrphans,
|
|
64
58
|
} from '../plan-reachability.js';
|
|
59
|
+
import { evaluateTextHygiene } from '../plan-text-hygiene.js';
|
|
65
60
|
import {
|
|
66
61
|
computeAssembledConflictFindings,
|
|
67
62
|
conflictFindingKey,
|
|
68
|
-
renderHardConflictError,
|
|
69
63
|
} from '../ticket-validator-conflicts.js';
|
|
70
64
|
import { upsertStructuredComment } from '../ticketing.js';
|
|
71
65
|
import { recordAuditFilings, withAuditLabels } from './audit-provenance.js';
|
|
66
|
+
import { renderChangeRepair } from './changes-repair.js';
|
|
72
67
|
import {
|
|
73
68
|
resolveContainerEpic,
|
|
74
69
|
resolveCrossPlanLinks,
|
|
75
70
|
} from './cross-plan-links.js';
|
|
76
|
-
import {
|
|
77
|
-
|
|
78
|
-
surfaceSoftConflictFindings,
|
|
79
|
-
} from './fan-out-gate.js';
|
|
80
|
-
import { resolveBaseBranchRef, validateTickets } from './persist-helpers.js';
|
|
71
|
+
import { validateTickets } from './persist-helpers.js';
|
|
72
|
+
import { surfaceSoftConflictFindings } from './soft-findings.js';
|
|
81
73
|
import {
|
|
82
74
|
assemblePlanStories,
|
|
83
75
|
createStoryIssues,
|
|
@@ -128,66 +120,87 @@ export async function writeCheckpointV2(provider, storyId, state) {
|
|
|
128
120
|
}
|
|
129
121
|
|
|
130
122
|
/**
|
|
131
|
-
* Enforce the ticket validator's
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* @returns {{ stale: number, ambiguous: number }}
|
|
147
|
-
* posted summary.
|
|
123
|
+
* Enforce the ticket validator's hard errors and collect its warnings.
|
|
124
|
+
*
|
|
125
|
+
* Since Story #5312 the only hard refusal the validator batches into
|
|
126
|
+
* `errors[]` is a `deletes` naming a path absent at base; every other
|
|
127
|
+
* footprint probe — a `creates` / `refactors-existing` mismatch, a goal,
|
|
128
|
+
* acceptance or verify path absent at base — lands on `warnings[]`, which
|
|
129
|
+
* the dry-run prints and the persist proceeds past. The `changes[]` repairs
|
|
130
|
+
* the helper applied are reported alongside so the operator sees what was
|
|
131
|
+
* rewritten.
|
|
132
|
+
*
|
|
133
|
+
* The returned freshness counts feed the posted `plan-summary`'s freshness
|
|
134
|
+
* line: every warning is a reference the base branch disagreed with, so it
|
|
135
|
+
* counts as `stale` there rather than the comment reading "clean" on a run
|
|
136
|
+
* that had something to say.
|
|
137
|
+
*
|
|
138
|
+
* @returns {{ warnings: string[], freshness: { stale: number, ambiguous: number } }}
|
|
148
139
|
*/
|
|
149
|
-
function enforceTicketValidation(validated
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
error.startsWith('File assumption mismatch:'),
|
|
153
|
-
);
|
|
154
|
-
const blockingErrors = validationErrors.filter(
|
|
155
|
-
(error) => !error.startsWith('File assumption mismatch:'),
|
|
156
|
-
);
|
|
157
|
-
if (blockingErrors.length > 0) {
|
|
158
|
-
throw new Error(
|
|
159
|
-
`[plan-persist] ticket validation failed with ${blockingErrors.length} ` +
|
|
160
|
-
`hard error(s):\n${blockingErrors.map((error) => ` - ${error}`).join('\n')}`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
if (assumptionFailures.length === 0) return { stale: 0, ambiguous: 0 };
|
|
164
|
-
// Story #4541: resolve through the canonical `project.baseBranch` (the
|
|
165
|
-
// shape `config-resolver` actually emits) with the legacy settings bag as
|
|
166
|
-
// a fallback — reading a bare `config.baseBranch` meant this probe always
|
|
167
|
-
// targeted the literal `main`.
|
|
168
|
-
const gateBaseRef =
|
|
169
|
-
config?.project?.baseBranch ??
|
|
170
|
-
settings?.baseBranch ??
|
|
171
|
-
resolveBaseBranchRef(config);
|
|
172
|
-
const refResolves =
|
|
173
|
-
gitSpawn(
|
|
174
|
-
cwd ?? process.cwd(),
|
|
175
|
-
'rev-parse',
|
|
176
|
-
'--verify',
|
|
177
|
-
'--quiet',
|
|
178
|
-
`${gateBaseRef}^{commit}`,
|
|
179
|
-
).status === 0;
|
|
180
|
-
if (refResolves) {
|
|
140
|
+
function enforceTicketValidation(validated) {
|
|
141
|
+
const errors = validated.errors ?? [];
|
|
142
|
+
if (errors.length > 0) {
|
|
181
143
|
throw new Error(
|
|
182
|
-
`[plan-persist]
|
|
183
|
-
`
|
|
144
|
+
`[plan-persist] ticket validation failed with ${errors.length} ` +
|
|
145
|
+
`hard error(s):\n${errors.map((error) => ` - ${error}`).join('\n')}`,
|
|
184
146
|
);
|
|
185
147
|
}
|
|
148
|
+
const warnings = [
|
|
149
|
+
...(validated.repairs ?? []).map((repair) => renderChangeRepair(repair)),
|
|
150
|
+
...(validated.warnings ?? []),
|
|
151
|
+
];
|
|
152
|
+
return {
|
|
153
|
+
warnings,
|
|
154
|
+
freshness: freshnessCounts(validated.probeRef, validated.warnings ?? []),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Freshness counts for the posted plan summary. Every validator warning is
|
|
160
|
+
* a reference the base branch disagreed with — `stale` when a base ref was
|
|
161
|
+
* actually read, `ambiguous` when none resolved in this checkout and the
|
|
162
|
+
* probes were skipped, since nothing was verified either way.
|
|
163
|
+
*
|
|
164
|
+
* @param {string|null} probeRef
|
|
165
|
+
* @param {string[]} warnings
|
|
166
|
+
* @returns {{ stale: number, ambiguous: number }}
|
|
167
|
+
*/
|
|
168
|
+
function freshnessCounts(probeRef, warnings) {
|
|
169
|
+
if (probeRef === null) return { stale: 0, ambiguous: warnings.length };
|
|
170
|
+
return { stale: warnings.length, ambiguous: 0 };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The `open-question` lint over the draft bodies (Story #5312) — an
|
|
175
|
+
* operator-directed question persisted into a Story a non-interactive
|
|
176
|
+
* sub-agent executes. A warning the dry-run lists, never a refusal.
|
|
177
|
+
*
|
|
178
|
+
* @param {object[]} rawStories
|
|
179
|
+
* @returns {string[]}
|
|
180
|
+
*/
|
|
181
|
+
function collectOpenQuestionWarnings(rawStories) {
|
|
182
|
+
return evaluateTextHygiene({ draftStories: rawStories }).findings.map(
|
|
183
|
+
(finding) =>
|
|
184
|
+
`Story "${finding.slug}": open question in body — "${finding.evidence}". ${finding.message}`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Print every warning the run collected under one heading. The dry-run is
|
|
190
|
+
* where an operator reads these; the persist prints the same list so a
|
|
191
|
+
* `--chain-on-clean` run loses nothing.
|
|
192
|
+
*
|
|
193
|
+
* @param {string[]} warnings
|
|
194
|
+
* @returns {void}
|
|
195
|
+
*/
|
|
196
|
+
function logWarnings(warnings) {
|
|
197
|
+
if (warnings.length === 0) return;
|
|
186
198
|
Logger.warn(
|
|
187
|
-
`[plan-persist]
|
|
188
|
-
`does not resolve — ${assumptionFailures.length} finding(s) downgraded.`,
|
|
199
|
+
`[plan-persist] ${warnings.length} warning(s) — the persist proceeds; review before delivering:`,
|
|
189
200
|
);
|
|
190
|
-
|
|
201
|
+
for (const warning of warnings) {
|
|
202
|
+
Logger.warn(`[plan-persist] warning: ${warning}`);
|
|
203
|
+
}
|
|
191
204
|
}
|
|
192
205
|
|
|
193
206
|
/**
|
|
@@ -282,144 +295,23 @@ async function renderRunScopedPlanMetricsLine({
|
|
|
282
295
|
}
|
|
283
296
|
|
|
284
297
|
/**
|
|
285
|
-
*
|
|
286
|
-
* (Story #
|
|
287
|
-
*
|
|
288
|
-
* Two staged inputs, no word count anywhere:
|
|
289
|
-
*
|
|
290
|
-
* 1. **The planner's authored verdict** — `--route-downgrade-reason` is the
|
|
291
|
-
* lite claim's recorded reason (`resolvePlannerRouteVerdict`). No
|
|
292
|
-
* recorded reason means no claim: the plan persists as standard `full`
|
|
293
|
-
* and nothing is ledgered (`null`).
|
|
294
|
-
* 2. **The deterministic shape backstop** — a lite claim is validated
|
|
295
|
-
* against every assembled Story's own shape (`deriveStoryShape` over its
|
|
296
|
-
* `changes[]`, acceptance count, creates-vs-refactors mix, and
|
|
297
|
-
* sensitive-path classes). Any Story exceeding the ceilings **fails the
|
|
298
|
-
* claim closed to `full`** — the honest gate: after authoring, the work
|
|
299
|
-
* has measurable shape, so complexity is read from the work, not guessed
|
|
300
|
-
* from the seed.
|
|
301
|
-
*
|
|
302
|
-
* The resolved route decides whether the created Stories carry the
|
|
303
|
-
* {@link LITE_ROUTE_LABEL} **hint** (never the control signal — `/mandrel-deliver`
|
|
304
|
-
* re-derives the route from the Story body's shape) and the `route` block
|
|
305
|
-
* ledgered on their `story-plan-state` checkpoint, including the authored
|
|
306
|
-
* verdict, its recorded reason, and the per-Story shape evidence. A refused
|
|
307
|
-
* claim is ledgered too (route `full` with the refusal reasons), so the
|
|
308
|
-
* judgment stays auditable either way.
|
|
309
|
-
*
|
|
310
|
-
* Module-private: reachable end to end through {@link runPlanPersist}
|
|
311
|
-
* (whose result reports the resolved route), so there is no test-only
|
|
312
|
-
* export to leave production-dead.
|
|
313
|
-
*
|
|
314
|
-
* @param {{
|
|
315
|
-
* stories: ReturnType<typeof assemblePlanStories>['stories'],
|
|
316
|
-
* routeDowngradeReason?: string|null,
|
|
317
|
-
* config?: object,
|
|
318
|
-
* injectedRules?: object,
|
|
319
|
-
* }} args
|
|
320
|
-
* @returns {{
|
|
321
|
-
* route: 'lite'|'full',
|
|
322
|
-
* reasons: string[],
|
|
323
|
-
* authored: { route: 'lite', reason: string },
|
|
324
|
-
* shape: Array<{ slug: string, route: string, reasons: string[], shape: object|null }>,
|
|
325
|
-
* }|null} `null` when the planner authored no verdict (nothing to persist).
|
|
326
|
-
*/
|
|
327
|
-
function resolveEffectiveRoute({
|
|
328
|
-
stories,
|
|
329
|
-
routeDowngradeReason = null,
|
|
330
|
-
config = {},
|
|
331
|
-
injectedRules,
|
|
332
|
-
}) {
|
|
333
|
-
const verdict = resolvePlannerRouteVerdict({ reason: routeDowngradeReason });
|
|
334
|
-
if (verdict.route !== 'lite') return null;
|
|
335
|
-
|
|
336
|
-
// The schema's documented contract: with the gate disabled
|
|
337
|
-
// (`planning.complexityGate.enabled=false`), persist refuses lite claims —
|
|
338
|
-
// the same switch dispatch reads (`resolveStoryDispatchMode` falls back to
|
|
339
|
-
// sub-agent), so neither read point can honor a lite claim the operator
|
|
340
|
-
// has switched off. The refusal is ledgered like any other, keeping the
|
|
341
|
-
// judgment auditable.
|
|
342
|
-
if (!resolveComplexityGate(config).enabled) {
|
|
343
|
-
return {
|
|
344
|
-
route: 'full',
|
|
345
|
-
reasons: [
|
|
346
|
-
'planner lite verdict refused: complexity routing is disabled ' +
|
|
347
|
-
'(planning.complexityGate.enabled=false)',
|
|
348
|
-
],
|
|
349
|
-
authored: verdict.authored,
|
|
350
|
-
shape: [],
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const perStory = (Array.isArray(stories) ? stories : []).map((story) => {
|
|
355
|
-
const derived = deriveStoryShape({
|
|
356
|
-
changes: story.bodyObject?.changes,
|
|
357
|
-
acceptance: story.acceptance,
|
|
358
|
-
injectedRules,
|
|
359
|
-
});
|
|
360
|
-
return {
|
|
361
|
-
slug: story.slug,
|
|
362
|
-
route: derived.route,
|
|
363
|
-
reasons: derived.reasons,
|
|
364
|
-
shape: derived.shape,
|
|
365
|
-
};
|
|
366
|
-
});
|
|
367
|
-
const offenders = perStory.filter((entry) => entry.route !== 'lite');
|
|
368
|
-
if (offenders.length > 0) {
|
|
369
|
-
return {
|
|
370
|
-
route: 'full',
|
|
371
|
-
reasons: [
|
|
372
|
-
`planner lite verdict refused: ${offenders.length} of ${perStory.length} ` +
|
|
373
|
-
'Story(ies) exceed the lite shape ceilings — failing closed to full',
|
|
374
|
-
...offenders.map((entry) => `${entry.slug}: ${entry.reasons[0]}`),
|
|
375
|
-
],
|
|
376
|
-
authored: verdict.authored,
|
|
377
|
-
shape: perStory,
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
return {
|
|
381
|
-
route: 'lite',
|
|
382
|
-
reasons: [
|
|
383
|
-
...verdict.reasons,
|
|
384
|
-
'shape backstop: every authored Story fits the lite shape ceilings',
|
|
385
|
-
],
|
|
386
|
-
authored: verdict.authored,
|
|
387
|
-
shape: perStory,
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Re-run the cross-Story conflict passes over the assembled bodies and route
|
|
393
|
-
* the result (Story #5045).
|
|
298
|
+
* Re-run the cross-Story conflict passes over the assembled bodies
|
|
299
|
+
* (Story #5045).
|
|
394
300
|
*
|
|
395
301
|
* `validateTickets` runs before assembly, over the raw payload, so until now
|
|
396
302
|
* plan-time conflict analysis judged an artifact that is not the one persist
|
|
397
303
|
* writes — and the passes that scan `body.acceptance` / `body.verify` were
|
|
398
|
-
* inert on the canonical top-level authoring shape as a result.
|
|
399
|
-
*
|
|
400
|
-
*
|
|
304
|
+
* inert on the canonical top-level authoring shape as a result. Findings the
|
|
305
|
+
* raw pass already reported are dropped so the same collision is not
|
|
306
|
+
* announced twice per run; the rest are returned for the plan-summary
|
|
307
|
+
* comment, which is where these findings stop being a stderr line nobody
|
|
308
|
+
* keeps. Every finding is advisory (Story #5312).
|
|
401
309
|
*
|
|
402
|
-
*
|
|
403
|
-
* `planning.requireExplicitCrossStoryDeps`) are off by default; when an
|
|
404
|
-
* operator turns one on it must bite on the persisted artifact too, and it
|
|
405
|
-
* must bite **before** the first `createIssue`.
|
|
406
|
-
* 2. **Soft findings the raw pass already reported are dropped**, so the same
|
|
407
|
-
* collision is not announced twice per run.
|
|
408
|
-
* 3. **Everything else is returned** for the plan-summary comment, which is
|
|
409
|
-
* where these findings stop being a stderr line nobody keeps.
|
|
410
|
-
*
|
|
411
|
-
* @param {{ stories: object[], config: object, rawFindings: object[] }} args
|
|
310
|
+
* @param {{ stories: object[], rawFindings: object[] }} args
|
|
412
311
|
* @returns {object[]} The assembled-pass findings, for the summary comment.
|
|
413
312
|
*/
|
|
414
|
-
function analyzeAssembledStories({ stories,
|
|
415
|
-
const findings = computeAssembledConflictFindings({ stories
|
|
416
|
-
const hard = findings.filter((finding) => finding.severity === 'hard');
|
|
417
|
-
if (hard.length > 0) {
|
|
418
|
-
throw new Error(
|
|
419
|
-
`[plan-persist] ${hard.length} cross-Story conflict(s) in the assembled ` +
|
|
420
|
-
`Story bodies:\n${hard.map((f) => ` - ${renderHardConflictError(f)}`).join('\n')}`,
|
|
421
|
-
);
|
|
422
|
-
}
|
|
313
|
+
function analyzeAssembledStories({ stories, rawFindings }) {
|
|
314
|
+
const findings = computeAssembledConflictFindings({ stories });
|
|
423
315
|
const alreadyReported = new Set(
|
|
424
316
|
(rawFindings ?? []).map((finding) => conflictFindingKey(finding)),
|
|
425
317
|
);
|
|
@@ -470,33 +362,22 @@ export async function reapStalePlanDirs({
|
|
|
470
362
|
}
|
|
471
363
|
|
|
472
364
|
/**
|
|
473
|
-
* Fail closed on a payload that cannot be persisted
|
|
474
|
-
*
|
|
475
|
-
*
|
|
365
|
+
* Fail closed on a payload that cannot be persisted. Extracted from
|
|
366
|
+
* `runPlanPersist` (Story #4926) so the entry point carries the flow, not the
|
|
367
|
+
* guards. The reviewability budget that used to sit beside this check went
|
|
368
|
+
* with Story #5312 — a plan is as many Stories as the split policy yields.
|
|
476
369
|
*
|
|
477
370
|
* @param {unknown} rawStories
|
|
478
|
-
* @param {{ maxTickets: number, allowOverBudget: boolean }} limits
|
|
479
371
|
* @returns {void}
|
|
480
|
-
* @throws {Error} On an empty payload
|
|
372
|
+
* @throws {Error} On an empty payload.
|
|
481
373
|
*/
|
|
482
|
-
function assertPersistablePlan(rawStories
|
|
374
|
+
function assertPersistablePlan(rawStories) {
|
|
483
375
|
if (!Array.isArray(rawStories) || rawStories.length === 0) {
|
|
484
376
|
throw new Error(
|
|
485
377
|
'[plan-persist] stories payload must be a non-empty array ' +
|
|
486
378
|
'(--stories <file>). Default is one Story.',
|
|
487
379
|
);
|
|
488
380
|
}
|
|
489
|
-
if (rawStories.length <= maxTickets) return;
|
|
490
|
-
if (!allowOverBudget) {
|
|
491
|
-
throw new Error(
|
|
492
|
-
`[plan-persist] Stories (${rawStories.length}) exceed the reviewability ` +
|
|
493
|
-
`budget (${maxTickets}). Re-scope, or rerun with --allow-over-budget.`,
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
Logger.warn(
|
|
497
|
-
`[plan-persist] Persisting an over-budget plan: ${rawStories.length} ` +
|
|
498
|
-
`Stories vs. budget ${maxTickets} (--allow-over-budget).`,
|
|
499
|
-
);
|
|
500
381
|
}
|
|
501
382
|
|
|
502
383
|
/**
|
|
@@ -549,7 +430,6 @@ async function persistStoryArtifacts({
|
|
|
549
430
|
provider,
|
|
550
431
|
created,
|
|
551
432
|
primary,
|
|
552
|
-
route,
|
|
553
433
|
summaryBody,
|
|
554
434
|
}) {
|
|
555
435
|
const cohort = created.map((createdStory) => ({
|
|
@@ -566,11 +446,6 @@ async function persistStoryArtifacts({
|
|
|
566
446
|
primaryStoryId: primary.id,
|
|
567
447
|
stories: cohort,
|
|
568
448
|
},
|
|
569
|
-
// Ledger the authored route verdict — the recorded reason and the
|
|
570
|
-
// per-Story shape evidence, including a shape-refused claim — on plan
|
|
571
|
-
// state (Story #4722). No authored verdict writes no block: absence
|
|
572
|
-
// is the standard full path.
|
|
573
|
-
...(route ? { route } : {}),
|
|
574
449
|
}),
|
|
575
450
|
// The per-Story checkpoint upserts (Story #4952): each targets a
|
|
576
451
|
// different issue and reads nothing another writes, so this loop was
|
|
@@ -605,32 +480,6 @@ async function cleanupPlanDirs({ config, planDir, skipCleanup }) {
|
|
|
605
480
|
await reapStalePlanDirs({ config, keepDir: skipCleanup ? planDir : null });
|
|
606
481
|
}
|
|
607
482
|
|
|
608
|
-
/**
|
|
609
|
-
* Log the effective complexity route (Story #4722). Lite is upheld by the
|
|
610
|
-
* shape backstop; anything else reports why it fell back to full.
|
|
611
|
-
*
|
|
612
|
-
* @param {object|null} route
|
|
613
|
-
* @param {boolean} isLiteRoute
|
|
614
|
-
* @returns {void}
|
|
615
|
-
*/
|
|
616
|
-
function logEffectiveRoute(route, isLiteRoute) {
|
|
617
|
-
if (isLiteRoute) {
|
|
618
|
-
Logger.info(
|
|
619
|
-
`[plan-persist] ceremony-lite route upheld by the shape backstop: ` +
|
|
620
|
-
`created Stories carry the ${LITE_ROUTE_LABEL} hint ` +
|
|
621
|
-
`(recorded reason: ${route.authored.reason}). /mandrel-deliver re-derives ` +
|
|
622
|
-
'the route from each Story body — the label is never the control signal.',
|
|
623
|
-
);
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
if (route) {
|
|
627
|
-
Logger.warn(
|
|
628
|
-
`[plan-persist] ${route.reasons.join('; ')} — persisting as full ` +
|
|
629
|
-
'(no route hint label).',
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
483
|
/**
|
|
635
484
|
* Log the operator-facing persist epilogue: adoption, the ready primary, the
|
|
636
485
|
* deliver command, and the cohort grouping label.
|
|
@@ -688,21 +537,16 @@ function logPersistEpilogue({
|
|
|
688
537
|
* planContextEnvelope?: object|null,
|
|
689
538
|
* },
|
|
690
539
|
* config?: object,
|
|
691
|
-
* settings?: object,
|
|
692
540
|
* opts?: {
|
|
693
541
|
* forceReview?: boolean,
|
|
694
|
-
* allowOverBudget?: boolean,
|
|
695
|
-
* allowLargeFanOut?: boolean,
|
|
696
542
|
* skipCleanup?: boolean,
|
|
697
543
|
* dryRun?: boolean,
|
|
698
544
|
* planDir?: string,
|
|
699
|
-
*
|
|
545
|
+
* gitRunner?: Function,
|
|
700
546
|
* cwd?: string,
|
|
701
547
|
* sourceTicketIds?: number[],
|
|
702
548
|
* sourceTicketOrigin?: 'flag'|'envelope'|'none',
|
|
703
549
|
* closeSuperseded?: boolean,
|
|
704
|
-
* routeDowngradeReason?: string|null,
|
|
705
|
-
* injectedRules?: object,
|
|
706
550
|
* },
|
|
707
551
|
* }} input
|
|
708
552
|
*/
|
|
@@ -710,7 +554,6 @@ export async function runPlanPersist({
|
|
|
710
554
|
provider,
|
|
711
555
|
artifacts,
|
|
712
556
|
config = {},
|
|
713
|
-
settings = {},
|
|
714
557
|
opts = {},
|
|
715
558
|
}) {
|
|
716
559
|
const {
|
|
@@ -721,18 +564,14 @@ export async function runPlanPersist({
|
|
|
721
564
|
} = artifacts ?? {};
|
|
722
565
|
const {
|
|
723
566
|
forceReview = false,
|
|
724
|
-
allowOverBudget = false,
|
|
725
|
-
allowLargeFanOut = false,
|
|
726
567
|
skipCleanup = false,
|
|
727
568
|
dryRun = false,
|
|
728
569
|
planDir = null,
|
|
729
|
-
|
|
570
|
+
gitRunner = undefined,
|
|
730
571
|
cwd = PROJECT_ROOT,
|
|
731
572
|
sourceTicketIds = [],
|
|
732
573
|
sourceTicketOrigin = 'none',
|
|
733
574
|
closeSuperseded = true,
|
|
734
|
-
routeDowngradeReason = null,
|
|
735
|
-
injectedRules = undefined,
|
|
736
575
|
// Story #5139 — the optional container Epic. `null` (the default) is the
|
|
737
576
|
// ordinary shape: no Epic is created unless `/mandrel-plan` offered one
|
|
738
577
|
// above the threshold and the operator confirmed it.
|
|
@@ -745,26 +584,20 @@ export async function runPlanPersist({
|
|
|
745
584
|
// through the shared standalone ledger (Story #4541).
|
|
746
585
|
const runStartedAt = opts.metricsSince ?? new Date().toISOString();
|
|
747
586
|
|
|
748
|
-
assertPersistablePlan(rawStories
|
|
749
|
-
maxTickets: getLimits(config).maxTickets,
|
|
750
|
-
allowOverBudget,
|
|
751
|
-
});
|
|
587
|
+
assertPersistablePlan(rawStories);
|
|
752
588
|
|
|
753
589
|
Logger.info(
|
|
754
590
|
`[plan-persist] Running cross-validation on ${rawStories.length} Story ticket(s)...`,
|
|
755
591
|
);
|
|
756
|
-
const validated = validateTickets(rawStories, config, {
|
|
757
|
-
fanOutCounter,
|
|
758
|
-
cwd,
|
|
759
|
-
modelCapacity: opts.modelCapacity,
|
|
760
|
-
});
|
|
761
|
-
enforceFanOutGate(validated.findings, allowLargeFanOut, 'plan-persist');
|
|
592
|
+
const validated = validateTickets(rawStories, config, { cwd, gitRunner });
|
|
762
593
|
surfaceSoftConflictFindings(validated.findings, 'plan-persist');
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
594
|
+
const { warnings: validationWarnings, freshness } =
|
|
595
|
+
enforceTicketValidation(validated);
|
|
596
|
+
const warnings = [
|
|
597
|
+
...validationWarnings,
|
|
598
|
+
...collectOpenQuestionWarnings(rawStories),
|
|
599
|
+
];
|
|
600
|
+
logWarnings(warnings);
|
|
768
601
|
|
|
769
602
|
const reachability = evaluateDraftReachability({
|
|
770
603
|
tickets: rawStories,
|
|
@@ -780,7 +613,7 @@ export async function runPlanPersist({
|
|
|
780
613
|
epicId: opts.adoptEpicId ?? null,
|
|
781
614
|
});
|
|
782
615
|
|
|
783
|
-
// Split policy + inline Spec fold (
|
|
616
|
+
// Split policy + inline Spec fold (Specs stay inline, never under docs/).
|
|
784
617
|
const seedContent = planContextEnvelope?.seed?.content ?? '';
|
|
785
618
|
const { stories: assembled } = assemblePlanStories(rawStories, {
|
|
786
619
|
sharedSpec: techSpecContent,
|
|
@@ -808,29 +641,14 @@ export async function runPlanPersist({
|
|
|
808
641
|
// GitHub call, so a policy upgrade still refuses the plan pre-creation.
|
|
809
642
|
const assembledConflicts = analyzeAssembledStories({
|
|
810
643
|
stories,
|
|
811
|
-
config,
|
|
812
644
|
rawFindings: validated.findings,
|
|
813
645
|
});
|
|
814
646
|
|
|
815
|
-
// Effective complexity route (Story #4722): the planner's authored lite
|
|
816
|
-
// verdict (recorded reason), validated against every assembled Story's own
|
|
817
|
-
// shape — a claim exceeding the shape ceilings fails closed to full. Lite
|
|
818
|
-
// persists the `route::lite` HINT label + a checkpoint route block; a
|
|
819
|
-
// refused claim ledgers the refusal (no label); no claim persists nothing.
|
|
820
|
-
const route = resolveEffectiveRoute({
|
|
821
|
-
stories,
|
|
822
|
-
routeDowngradeReason,
|
|
823
|
-
config,
|
|
824
|
-
injectedRules,
|
|
825
|
-
});
|
|
826
|
-
const isLiteRoute = route?.route === 'lite';
|
|
827
|
-
logEffectiveRoute(route, isLiteRoute);
|
|
828
|
-
|
|
829
647
|
const { created, planRunLabel, planRunLabelApplied } =
|
|
830
648
|
await createStoryIssues({
|
|
831
649
|
provider,
|
|
832
650
|
stories,
|
|
833
|
-
opts: { dryRun
|
|
651
|
+
opts: { dryRun },
|
|
834
652
|
});
|
|
835
653
|
|
|
836
654
|
// What this run filed, recorded where the next audit sweep reads it
|
|
@@ -894,7 +712,6 @@ export async function runPlanPersist({
|
|
|
894
712
|
provider,
|
|
895
713
|
created,
|
|
896
714
|
primary,
|
|
897
|
-
route,
|
|
898
715
|
summaryBody,
|
|
899
716
|
});
|
|
900
717
|
}
|
|
@@ -941,10 +758,14 @@ export async function runPlanPersist({
|
|
|
941
758
|
stories: created,
|
|
942
759
|
primaryStoryId: primary.id,
|
|
943
760
|
planRunLabel,
|
|
944
|
-
route,
|
|
945
761
|
forceReview,
|
|
946
762
|
reachability,
|
|
947
763
|
freshness,
|
|
764
|
+
// Story #5312: what the run rewrote and what it noticed. The dry-run is
|
|
765
|
+
// the review surface now that the footprint probes warn instead of
|
|
766
|
+
// refusing, so the list rides the result envelope, not just stderr.
|
|
767
|
+
warnings,
|
|
768
|
+
repairs: validated.repairs ?? [],
|
|
948
769
|
waveTable,
|
|
949
770
|
// Story #5265 AC-2/AC-4: both halves of what persist concluded but used
|
|
950
771
|
// to keep to itself — the `refactors-existing` declarations it rewrote,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soft-finding reporting for plan-persist.
|
|
3
|
+
*
|
|
4
|
+
* Story #5312 retired the fan-out gate that shared this file: the delete
|
|
5
|
+
* blast-radius probe never refused a real plan, and its `--allow-large-fan-out`
|
|
6
|
+
* override was a flag nobody typed. What remains is the one surface that
|
|
7
|
+
* announces every advisory finding the validator produced, each under its
|
|
8
|
+
* own kind, so the operator reads a conflict as a conflict and a nudge as a
|
|
9
|
+
* nudge (Story #4907).
|
|
10
|
+
*
|
|
11
|
+
* @module lib/orchestration/plan-persist/soft-findings
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Logger } from '../../Logger.js';
|
|
15
|
+
import {
|
|
16
|
+
CONFLICT_KINDS,
|
|
17
|
+
renderHardConflictError,
|
|
18
|
+
} from '../ticket-validator-conflicts.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Report every soft finding the validator produced, each under its own kind.
|
|
22
|
+
*
|
|
23
|
+
* Only the {@link CONFLICT_KINDS} are cross-Story conflicts. Any other soft
|
|
24
|
+
* kind is a single-Story nudge, and announcing it as a conflict overstated it
|
|
25
|
+
* and taught readers to discount the whole channel.
|
|
26
|
+
*
|
|
27
|
+
* @param {object[]} findings
|
|
28
|
+
* @param {string} [tag]
|
|
29
|
+
*/
|
|
30
|
+
export function surfaceSoftConflictFindings(findings, tag = 'plan-persist') {
|
|
31
|
+
const soft = (findings ?? []).filter((f) => f?.severity === 'soft');
|
|
32
|
+
if (soft.length === 0) return;
|
|
33
|
+
const conflicts = soft.filter((f) => CONFLICT_KINDS.has(f?.kind));
|
|
34
|
+
const advisories = soft.filter((f) => !CONFLICT_KINDS.has(f?.kind));
|
|
35
|
+
if (conflicts.length > 0) {
|
|
36
|
+
Logger.warn(
|
|
37
|
+
`[${tag}] ${conflicts.length} soft cross-Story conflict finding(s) — review before approving the plan:`,
|
|
38
|
+
);
|
|
39
|
+
for (const finding of conflicts) {
|
|
40
|
+
Logger.warn(
|
|
41
|
+
`[${tag}] soft conflict: ${renderHardConflictError(finding)}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (advisories.length > 0) {
|
|
46
|
+
Logger.warn(
|
|
47
|
+
`[${tag}] ${advisories.length} advisory finding(s) — the persist proceeds:`,
|
|
48
|
+
);
|
|
49
|
+
for (const finding of advisories) {
|
|
50
|
+
Logger.warn(
|
|
51
|
+
`[${tag}] advisory (${finding.kind}): ${renderHardConflictError(finding)}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|