mandrel 2.56.0 → 2.58.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 -33
- 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/evidence-gate.js +17 -1
- 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/code-review.js +7 -3
- 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/pinned-identifier-lint.js +137 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +189 -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/acceptance-handle-repair.js +107 -0
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +305 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +138 -170
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +128 -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 +36 -135
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/review-base-ref.js +138 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +37 -5
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +6 -1
- 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 +54 -240
- package/.agents/scripts/lib/templates/decomposer-prompts.js +133 -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 +277 -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 +46 -55
- 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 +54 -55
- package/.agents/workflows/helpers/deliver-story.md +10 -13
- package/.agents/workflows/helpers/plan-reference.md +163 -221
- package/.agents/workflows/mandrel-plan.md +31 -40
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +36 -0
- package/lib/cli/registry.js +98 -2
- 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
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
* maintainability rows this branch improved on files it touched, as a
|
|
19
19
|
* `baseline-refresh:` commit, so the committed baseline stops falling
|
|
20
20
|
* behind the tree in the upward direction.
|
|
21
|
+
* 3. **Context-budget write-back** (Story #5313) — persist the lower
|
|
22
|
+
* documentation-tier totals this branch measured, so a trimmed tier's
|
|
23
|
+
* gain locks in without `check-context-budget.js` ever going red on it.
|
|
21
24
|
*
|
|
22
25
|
* Extracted from `close-validation.js` when the second step landed. The phase
|
|
23
26
|
* module's job is the gate chain, the evidence keyspace and the gate-log sink;
|
|
@@ -29,6 +32,7 @@
|
|
|
29
32
|
|
|
30
33
|
import { Logger } from '../../../Logger.js';
|
|
31
34
|
import { runBaselineUpwardWriteback as defaultRunBaselineUpwardWriteback } from '../../story-close/baseline-upward-writeback.js';
|
|
35
|
+
import { runContextBudgetWriteback as defaultRunContextBudgetWriteback } from '../../story-close/context-budget-writeback.js';
|
|
32
36
|
import { runScopedFormatAutofix as defaultRunScopedFormatAutofix } from '../../story-close/format-autofix.js';
|
|
33
37
|
|
|
34
38
|
/**
|
|
@@ -69,22 +73,22 @@ function formatAutofixStep({
|
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
/**
|
|
72
|
-
* Run the
|
|
76
|
+
* Run one of the two baseline write-backs and report it on one progress line.
|
|
77
|
+
*
|
|
78
|
+
* Both steps (Story #5224's maintainability rows, Story #5313's context-budget
|
|
79
|
+
* totals) take the same context and answer in the same shape — `committed`
|
|
80
|
+
* with a `sha`, or a named `reason` — so one wrapper serves both; `describe`
|
|
81
|
+
* renders the committed outcome in the step's own words.
|
|
73
82
|
*
|
|
74
83
|
* @param {object} ctx the shared step context (see {@link runPreGateSteps})
|
|
84
|
+
* @param {{ tag: string, run: Function, describe: (w: object) => string, noun: string }} step
|
|
75
85
|
* @returns {Promise<void>}
|
|
76
86
|
*/
|
|
77
|
-
async function
|
|
78
|
-
cwd,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
storyBranch,
|
|
83
|
-
config,
|
|
84
|
-
progress,
|
|
85
|
-
runBaselineUpwardWriteback,
|
|
86
|
-
}) {
|
|
87
|
-
const writeback = await runBaselineUpwardWriteback({
|
|
87
|
+
async function writebackStep(
|
|
88
|
+
{ cwd, worktreePath, storyId, baseBranch, storyBranch, config, progress },
|
|
89
|
+
{ tag, run, describe, noun },
|
|
90
|
+
) {
|
|
91
|
+
const writeback = await run({
|
|
88
92
|
cwd,
|
|
89
93
|
worktreePath,
|
|
90
94
|
storyId,
|
|
@@ -94,10 +98,10 @@ async function baselineWritebackStep({
|
|
|
94
98
|
logger: Logger,
|
|
95
99
|
});
|
|
96
100
|
progress(
|
|
97
|
-
|
|
101
|
+
tag,
|
|
98
102
|
writeback?.committed
|
|
99
|
-
? `✅
|
|
100
|
-
: `⏭ No
|
|
103
|
+
? `✅ ${describe(writeback)} as ${writeback.sha} on ${storyBranch}.`
|
|
104
|
+
: `⏭ No ${noun} (${writeback?.reason ?? 'nothing to write'}).`,
|
|
101
105
|
);
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -142,18 +146,24 @@ async function bestEffort({ tag, label, progress, run }) {
|
|
|
142
146
|
* progress: (tag: string, msg: string) => void,
|
|
143
147
|
* runScopedFormatAutofix?: typeof defaultRunScopedFormatAutofix,
|
|
144
148
|
* runBaselineUpwardWriteback?: typeof defaultRunBaselineUpwardWriteback,
|
|
149
|
+
* runContextBudgetWriteback?: typeof defaultRunContextBudgetWriteback,
|
|
145
150
|
* }} args
|
|
146
151
|
* @returns {Promise<void>}
|
|
147
152
|
*/
|
|
148
153
|
export async function runPreGateSteps({
|
|
149
154
|
runScopedFormatAutofix = defaultRunScopedFormatAutofix,
|
|
150
155
|
runBaselineUpwardWriteback = defaultRunBaselineUpwardWriteback,
|
|
156
|
+
runContextBudgetWriteback = defaultRunContextBudgetWriteback,
|
|
151
157
|
...ctx
|
|
152
158
|
}) {
|
|
153
159
|
const { storyBranch, progress } = ctx;
|
|
154
160
|
if (!storyBranch) {
|
|
155
161
|
progress('FORMAT', '⏭ Skipped scoped format-autofix (no story branch).');
|
|
156
162
|
progress('BASELINE', '⏭ Skipped baseline write-back (no story branch).');
|
|
163
|
+
progress(
|
|
164
|
+
'BUDGET',
|
|
165
|
+
'⏭ Skipped context-budget write-back (no story branch).',
|
|
166
|
+
);
|
|
157
167
|
return;
|
|
158
168
|
}
|
|
159
169
|
await bestEffort({
|
|
@@ -166,6 +176,26 @@ export async function runPreGateSteps({
|
|
|
166
176
|
tag: 'BASELINE',
|
|
167
177
|
label: 'baseline write-back',
|
|
168
178
|
progress,
|
|
169
|
-
run: () =>
|
|
179
|
+
run: () =>
|
|
180
|
+
writebackStep(ctx, {
|
|
181
|
+
tag: 'BASELINE',
|
|
182
|
+
run: runBaselineUpwardWriteback,
|
|
183
|
+
noun: 'baseline write-back',
|
|
184
|
+
describe: (w) =>
|
|
185
|
+
`Wrote back ${w.improvedPaths?.length ?? 0} improved maintainability row(s)`,
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
await bestEffort({
|
|
189
|
+
tag: 'BUDGET',
|
|
190
|
+
label: 'context-budget write-back',
|
|
191
|
+
progress,
|
|
192
|
+
run: () =>
|
|
193
|
+
writebackStep(ctx, {
|
|
194
|
+
tag: 'BUDGET',
|
|
195
|
+
run: runContextBudgetWriteback,
|
|
196
|
+
noun: 'context-budget write-back',
|
|
197
|
+
describe: (w) =>
|
|
198
|
+
`Wrote back lower context-budget totals (${w.tiers?.join(', ')})`,
|
|
199
|
+
}),
|
|
170
200
|
});
|
|
171
201
|
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { runCloseValidation } from '../../close-validation/runner.js';
|
|
8
8
|
import { getCiDelivery } from '../../config/ci.js';
|
|
9
9
|
import { resolveConfig } from '../../config-resolver.js';
|
|
10
|
-
import { getStoryBranch, gitSync } from '../../git-utils.js';
|
|
10
|
+
import { getStoryBranch, gitSpawn, gitSync } from '../../git-utils.js';
|
|
11
11
|
import { Logger } from '../../Logger.js';
|
|
12
12
|
import { emitTerminalFriction } from '../../observability/runtime-friction.js';
|
|
13
13
|
import { emitTerseResult } from '../../observability/terse-result.js';
|
|
@@ -318,6 +318,11 @@ async function openAndReviewPr({
|
|
|
318
318
|
prNumber,
|
|
319
319
|
provider,
|
|
320
320
|
runCodeReviewFn: injectedRunCodeReview ?? runCodeReviewDefault,
|
|
321
|
+
// The runner owns the git seam it hands its phases (same as `gitSync` to
|
|
322
|
+
// `pushStoryBranch`). The review needs it to resolve `origin/<base>` —
|
|
323
|
+
// the ref base-sync merged from — before it will score anything
|
|
324
|
+
// (Story #5325).
|
|
325
|
+
gitSpawnFn: gitSpawn,
|
|
321
326
|
progress,
|
|
322
327
|
});
|
|
323
328
|
if (reviewOutcome.halted) {
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-budget-writeback.js — lock a context-budget gain in on the branch
|
|
3
|
+
* that earned it (Story #5313).
|
|
4
|
+
*
|
|
5
|
+
* `check-context-budget.js` used to fail a gated documentation tier that came
|
|
6
|
+
* in **under** its recorded total (Story #4872), so a Story that trimmed prose
|
|
7
|
+
* paid for the trim twice: once in the edit and once in the red gate whose
|
|
8
|
+
* only remedy was a hand-run `--update`. Story #5313 reverses that rule —
|
|
9
|
+
* shrinkage exits 0 and is reported — and this module answers the concern
|
|
10
|
+
* the rule existed for: a stale recorded total silently absorbs the next
|
|
11
|
+
* growth. When the tree the close is scoring measures under its recorded
|
|
12
|
+
* totals, the lower totals are written back into
|
|
13
|
+
* `baselines/context-budget.json` and folded into one `baseline-refresh:`
|
|
14
|
+
* commit on the Story branch, so the gain lands in the branch's own PR.
|
|
15
|
+
*
|
|
16
|
+
* ## Where it runs, and why not post-land
|
|
17
|
+
*
|
|
18
|
+
* The Story names the post-land tail. This step runs from the close's
|
|
19
|
+
* **pre-gate write-back seam** (`phases/pre-gate-steps.js`, beside the
|
|
20
|
+
* maintainability write-back of Story #5224) instead: the only sanctioned
|
|
21
|
+
* landing is the Story branch → PR → `main`, so a post-land write would have
|
|
22
|
+
* to commit straight to the base branch. Writing on the branch ahead of the
|
|
23
|
+
* gates keeps the row inside the PR the gates score and lets
|
|
24
|
+
* `refresh-ack.js` vouch for it through the same `baseline-refresh:` marker.
|
|
25
|
+
*
|
|
26
|
+
* Constraints, each a "must not":
|
|
27
|
+
*
|
|
28
|
+
* 1. **Only a downward move is written.** Growth past tolerance and an
|
|
29
|
+
* unbacked recorded row must still fail `check-context-budget.js` exactly
|
|
30
|
+
* as before; when either is present this step skips and the gate speaks.
|
|
31
|
+
* 2. **Idempotent and silent.** No shrink, no write, no commit.
|
|
32
|
+
* 3. **Never fails the close.** Every failure is a named skip.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import fs from 'node:fs';
|
|
36
|
+
import path from 'node:path';
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
DEFAULT_TOLERANCE_BYTES,
|
|
40
|
+
buildBaseline as defaultBuildBaseline,
|
|
41
|
+
diffBudget as defaultDiffBudget,
|
|
42
|
+
loadBaseline as defaultLoadBaseline,
|
|
43
|
+
} from '../../../check-context-budget.js';
|
|
44
|
+
import { resolveDocTiers as defaultResolveDocTiers } from '../../doc-tiers.js';
|
|
45
|
+
import { gitSync as defaultGitSync } from '../../git-utils.js';
|
|
46
|
+
import { Logger as DefaultLogger } from '../../Logger.js';
|
|
47
|
+
|
|
48
|
+
const TAG = '[context-budget-writeback]';
|
|
49
|
+
|
|
50
|
+
/** Repo-relative location of the committed budget. */
|
|
51
|
+
const BASELINE_REL_PATH = 'baselines/context-budget.json';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build the commit subject — conventional, carrying the `baseline-refresh:`
|
|
55
|
+
* marker `refresh-ack.js` recognises, fixed-length but for the Story id.
|
|
56
|
+
*
|
|
57
|
+
* @param {number|string} storyId
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
function buildCommitSubject(storyId) {
|
|
61
|
+
return `chore(baselines): baseline-refresh: lower context-budget totals (story #${storyId})`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* One line per tier that moved down, before → after.
|
|
66
|
+
*
|
|
67
|
+
* @param {Array<{ tier: string, current: number, baseline: number }>} shrunk
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
function buildCommitBody(shrunk) {
|
|
71
|
+
return [
|
|
72
|
+
'Documentation tiers this branch trimmed under their recorded totals,',
|
|
73
|
+
'written back so the budget stops holding slack the tree no longer',
|
|
74
|
+
'spends. Growth and unbacked rows are never rewritten here.',
|
|
75
|
+
'',
|
|
76
|
+
...shrunk.map((s) => `- ${s.tier}: ${s.baseline} -> ${s.current} bytes`),
|
|
77
|
+
].join('\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Report a no-op by name.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} logger
|
|
84
|
+
* @param {string} reason
|
|
85
|
+
*/
|
|
86
|
+
function skip(logger, reason) {
|
|
87
|
+
logger.info?.(`${TAG} no write-back (${reason}).`);
|
|
88
|
+
return { ran: false, committed: false, reason };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Everything that must hold before a write. Returns a skip reason or `null`.
|
|
93
|
+
*
|
|
94
|
+
* @param {{ workTree: string, storyBranch: string, git: Function, relPath: string }} ctx
|
|
95
|
+
* @returns {string|null}
|
|
96
|
+
*/
|
|
97
|
+
function precheck({ workTree, storyBranch, git, relPath }) {
|
|
98
|
+
const onBranch = git(['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
99
|
+
cwd: workTree,
|
|
100
|
+
});
|
|
101
|
+
if (onBranch !== storyBranch) return 'wrong-branch';
|
|
102
|
+
const status = git(['status', '--porcelain', '--', relPath], {
|
|
103
|
+
cwd: workTree,
|
|
104
|
+
});
|
|
105
|
+
if (status.length > 0) return 'dirty-tree';
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Write the envelope and fold it into one commit. On a rejected commit the
|
|
111
|
+
* file is restored so the gates score the tree the close found.
|
|
112
|
+
*
|
|
113
|
+
* @param {{ workTree: string, git: Function, relPath: string, envelope: object, storyId: number|string, shrunk: object[] }} args
|
|
114
|
+
* @returns {{ sha: string }}
|
|
115
|
+
*/
|
|
116
|
+
function commitBaseline({ workTree, git, relPath, envelope, storyId, shrunk }) {
|
|
117
|
+
fs.writeFileSync(
|
|
118
|
+
path.join(workTree, relPath),
|
|
119
|
+
`${JSON.stringify(envelope, null, 2)}\n`,
|
|
120
|
+
);
|
|
121
|
+
git(['add', '--', relPath], { cwd: workTree });
|
|
122
|
+
try {
|
|
123
|
+
git(
|
|
124
|
+
[
|
|
125
|
+
'commit',
|
|
126
|
+
'-m',
|
|
127
|
+
buildCommitSubject(storyId),
|
|
128
|
+
'-m',
|
|
129
|
+
buildCommitBody(shrunk),
|
|
130
|
+
],
|
|
131
|
+
{ cwd: workTree },
|
|
132
|
+
);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
git(['restore', '--staged', '--worktree', '--', relPath], {
|
|
135
|
+
cwd: workTree,
|
|
136
|
+
});
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
return { sha: git(['rev-parse', '--short', 'HEAD'], { cwd: workTree }) };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Write the lower context-budget totals back on the Story branch.
|
|
144
|
+
*
|
|
145
|
+
* Total: a throw anywhere is a named skip, never a failed close.
|
|
146
|
+
*
|
|
147
|
+
* @param {{
|
|
148
|
+
* cwd: string,
|
|
149
|
+
* worktreePath?: string|null,
|
|
150
|
+
* storyId: number|string,
|
|
151
|
+
* storyBranch?: string,
|
|
152
|
+
* config: object,
|
|
153
|
+
* logger?: object,
|
|
154
|
+
* gitSync?: (cwd: string, ...args: string[]) => string,
|
|
155
|
+
* resolveDocTiersImpl?: typeof defaultResolveDocTiers,
|
|
156
|
+
* loadBaselineImpl?: typeof defaultLoadBaseline,
|
|
157
|
+
* diffBudgetImpl?: typeof defaultDiffBudget,
|
|
158
|
+
* buildBaselineImpl?: typeof defaultBuildBaseline,
|
|
159
|
+
* }} opts
|
|
160
|
+
* @returns {{ ran: boolean, committed: boolean, sha?: string, tiers?: string[], reason?: string }}
|
|
161
|
+
*/
|
|
162
|
+
export function runContextBudgetWriteback({
|
|
163
|
+
cwd,
|
|
164
|
+
worktreePath,
|
|
165
|
+
storyId,
|
|
166
|
+
storyBranch,
|
|
167
|
+
config,
|
|
168
|
+
logger = DefaultLogger,
|
|
169
|
+
gitSync = defaultGitSync,
|
|
170
|
+
resolveDocTiersImpl = defaultResolveDocTiers,
|
|
171
|
+
loadBaselineImpl = defaultLoadBaseline,
|
|
172
|
+
diffBudgetImpl = defaultDiffBudget,
|
|
173
|
+
buildBaselineImpl = defaultBuildBaseline,
|
|
174
|
+
} = {}) {
|
|
175
|
+
if (!cwd || !storyBranch) return skip(logger, 'missing-context');
|
|
176
|
+
const workTree = worktreePath || cwd;
|
|
177
|
+
const git = (args, opts = {}) => gitSync(opts.cwd ?? workTree, ...args);
|
|
178
|
+
try {
|
|
179
|
+
const blocked = precheck({
|
|
180
|
+
workTree,
|
|
181
|
+
storyBranch,
|
|
182
|
+
git,
|
|
183
|
+
relPath: BASELINE_REL_PATH,
|
|
184
|
+
});
|
|
185
|
+
if (blocked) return skip(logger, blocked);
|
|
186
|
+
const baseline = loadBaselineImpl(path.join(workTree, BASELINE_REL_PATH));
|
|
187
|
+
if (!baseline) return skip(logger, 'no-baseline');
|
|
188
|
+
const tierMap = resolveDocTiersImpl(config, { root: workTree });
|
|
189
|
+
const diff = diffBudgetImpl(tierMap, baseline);
|
|
190
|
+
if (diff.grown.length > 0 || diff.absent.length > 0) {
|
|
191
|
+
return skip(logger, 'drift-not-downward');
|
|
192
|
+
}
|
|
193
|
+
if (diff.shrunk.length === 0) return skip(logger, 'no-shrink');
|
|
194
|
+
const tolerance = Number.isFinite(baseline.toleranceBytes)
|
|
195
|
+
? baseline.toleranceBytes
|
|
196
|
+
: DEFAULT_TOLERANCE_BYTES;
|
|
197
|
+
const { sha } = commitBaseline({
|
|
198
|
+
workTree,
|
|
199
|
+
git,
|
|
200
|
+
relPath: BASELINE_REL_PATH,
|
|
201
|
+
envelope: buildBaselineImpl(tierMap, tolerance),
|
|
202
|
+
storyId,
|
|
203
|
+
shrunk: diff.shrunk,
|
|
204
|
+
});
|
|
205
|
+
const tiers = diff.shrunk.map((s) => s.tier);
|
|
206
|
+
logger.warn?.(
|
|
207
|
+
`${TAG} wrote back lower totals for ${tiers.join(', ')} on story #${storyId}; committed as ${sha}.`,
|
|
208
|
+
);
|
|
209
|
+
return { ran: true, committed: true, sha, tiers };
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return skip(logger, `failed: ${err?.message ?? err}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
* object. To make the rules below actually fire on canonical plans, a string
|
|
11
11
|
* body is **parsed** back into its structured form via `parseStoryBody`
|
|
12
12
|
* before the section checks run (Story #3906 — previously the validator
|
|
13
|
-
* `shouldSkipTicket`-skipped every string body, so the
|
|
14
|
-
*
|
|
15
|
-
* decomposition). A still-structured object body (e.g. a caller that passes
|
|
13
|
+
* `shouldSkipTicket`-skipped every string body, so the vague-verb check and
|
|
14
|
+
* non-empty-goal check never ran on any real decomposition). A still-structured object body (e.g. a caller that passes
|
|
16
15
|
* the pre-serialize shape directly) is validated as-is.
|
|
17
16
|
*
|
|
18
17
|
* Only `type: 'story'` tickets are validated; Feature/Epic tickets and
|
|
@@ -42,44 +41,23 @@
|
|
|
42
41
|
* array uses the same object shape and is the home for paths the Story
|
|
43
42
|
* reads but does not modify (test fixtures, sibling modules, etc.).
|
|
44
43
|
*
|
|
45
|
-
* `body.verify` entries
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* first on the persist path (Story #5005) and **repairs** the entries whose
|
|
50
|
-
* tier `suggestVerifyFix` can infer, so the hard error below is reserved for
|
|
51
|
-
* the entries only the author can resolve.
|
|
44
|
+
* `body.verify` entries are commands, nothing more: Story #5312 deleted the
|
|
45
|
+
* `(<tier>)` suffix, the `manual:<reason>` escape and the repair pass that
|
|
46
|
+
* appended the suffix for the author. The only verify rule left is that the
|
|
47
|
+
* list is non-empty.
|
|
52
48
|
*
|
|
53
49
|
* The errors are batched and surfaced as a single thrown Error so the
|
|
54
50
|
* planner can see every offending slug in one pass instead of fixing one
|
|
55
51
|
* at a time.
|
|
56
52
|
*/
|
|
57
53
|
|
|
58
|
-
import {
|
|
59
|
-
suggestPathEntryFix,
|
|
60
|
-
suggestVerifyFix,
|
|
61
|
-
} from '../story-body/body-format-lints.js';
|
|
54
|
+
import { suggestPathEntryFix } from '../story-body/body-format-lints.js';
|
|
62
55
|
import {
|
|
63
56
|
parse as parseStoryBody,
|
|
64
57
|
StoryBodyParseError,
|
|
65
58
|
} from '../story-body/story-body.js';
|
|
66
59
|
import { FILE_ASSUMPTION_VALUES } from './file-assumption-enum.js';
|
|
67
60
|
|
|
68
|
-
/**
|
|
69
|
-
* Canonical testing-tier labels that a `verify[]` entry must name (in
|
|
70
|
-
* parentheses) to pass plan-time validation. Mirrors the verify-rules contract
|
|
71
|
-
* in `.agents/scripts/lib/templates/decomposer-prompts.js`.
|
|
72
|
-
*
|
|
73
|
-
* Entries that do not end with `(<tier>)` and are not `manual:<reason>` are
|
|
74
|
-
* rejected by `collectVerifyErrors`.
|
|
75
|
-
*/
|
|
76
|
-
export const VERIFY_TIER_VALUES = Object.freeze([
|
|
77
|
-
'unit',
|
|
78
|
-
'contract',
|
|
79
|
-
'e2e',
|
|
80
|
-
'validate',
|
|
81
|
-
]);
|
|
82
|
-
|
|
83
61
|
/**
|
|
84
62
|
* Predicate: should the validator skip this ticket entirely? Skip when:
|
|
85
63
|
* - it is not a Story (only `type: 'story'` tickets are validated here),
|
|
@@ -91,7 +69,7 @@ export const VERIFY_TIER_VALUES = Object.freeze([
|
|
|
91
69
|
* Story body to a markdown string, so a *string* body is NOT skipped here
|
|
92
70
|
* (Story #3906) — `validateTaskBodyShape` parses it back into structured
|
|
93
71
|
* form via `parseStoryBody` before applying the section rules. This is what
|
|
94
|
-
* makes the verify
|
|
72
|
+
* makes the non-empty-verify / vague-verb / non-empty-goal checks actually fire
|
|
95
73
|
* on real plans. Features (and everything else) use narrative string bodies
|
|
96
74
|
* and are skipped by the `type !== 'story'` guard.
|
|
97
75
|
*
|
|
@@ -207,7 +185,6 @@ export function validateTaskBodyShape(ticket) {
|
|
|
207
185
|
}
|
|
208
186
|
errors.push(...collectChangesErrors(prefix, body.changes));
|
|
209
187
|
errors.push(...collectAcceptanceErrors(prefix, body.acceptance));
|
|
210
|
-
// Tier-suffix validation is always enforced on Story bodies (2-tier world).
|
|
211
188
|
errors.push(...collectVerifyErrors(prefix, body.verify));
|
|
212
189
|
errors.push(...collectReferencesErrors(prefix, body.references));
|
|
213
190
|
return errors;
|
|
@@ -324,15 +301,6 @@ function collectAcceptanceErrors(prefix, rawAcceptance) {
|
|
|
324
301
|
return [];
|
|
325
302
|
}
|
|
326
303
|
|
|
327
|
-
/**
|
|
328
|
-
* Regex that matches a valid tier suffix at the end of a verify entry:
|
|
329
|
-
* a parenthesised word drawn from `VERIFY_TIER_VALUES` (e.g. `(unit)`).
|
|
330
|
-
* Whitespace before the opening paren is tolerated.
|
|
331
|
-
*/
|
|
332
|
-
const VERIFY_TIER_RE = new RegExp(
|
|
333
|
-
`\\((?:${VERIFY_TIER_VALUES.join('|')})\\)\\s*$`,
|
|
334
|
-
);
|
|
335
|
-
|
|
336
304
|
/**
|
|
337
305
|
* @param {string} prefix
|
|
338
306
|
* @param {unknown} rawVerify
|
|
@@ -342,31 +310,10 @@ function collectVerifyErrors(prefix, rawVerify) {
|
|
|
342
310
|
const verify = Array.isArray(rawVerify) ? rawVerify : [];
|
|
343
311
|
if (verify.length === 0) {
|
|
344
312
|
return [
|
|
345
|
-
`${prefix}: verify must list at least one entry — author it at the ticket's top level (preferred) or in the body's ## Verify section
|
|
313
|
+
`${prefix}: verify must list at least one entry — author it at the ticket's top level (preferred) or in the body's ## Verify section.`,
|
|
346
314
|
];
|
|
347
315
|
}
|
|
348
|
-
|
|
349
|
-
for (const v of verify) {
|
|
350
|
-
if (typeof v !== 'string') continue;
|
|
351
|
-
if (v.startsWith('manual:')) {
|
|
352
|
-
const reason = v.slice('manual:'.length).trim();
|
|
353
|
-
if (reason === '') {
|
|
354
|
-
errors.push(
|
|
355
|
-
`${prefix}: body.verify "manual:" entry has no reason after the colon.`,
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
// manual: entries are exempt from the tier-suffix check.
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
if (!VERIFY_TIER_RE.test(v)) {
|
|
362
|
-
const fix = suggestVerifyFix(v);
|
|
363
|
-
const fixIt = fix === null ? '' : ` Suggested fix: "${fix}".`;
|
|
364
|
-
errors.push(
|
|
365
|
-
`${prefix}: body.verify entry must end with a tier in parentheses — one of (${VERIFY_TIER_VALUES.join('|')}). Got: "${v}".${fixIt}`,
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return errors;
|
|
316
|
+
return [];
|
|
370
317
|
}
|
|
371
318
|
|
|
372
319
|
/**
|