mandrel 1.87.0 → 1.89.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/README.md +18 -13
- package/.agents/audit-checklists/architecture.md +24 -0
- package/.agents/audit-checklists/clean-code.md +24 -0
- package/.agents/audit-checklists/dependencies.md +14 -0
- package/.agents/audit-checklists/devops.md +17 -0
- package/.agents/audit-checklists/documentation.md +22 -0
- package/.agents/audit-checklists/lighthouse.md +15 -0
- package/.agents/audit-checklists/navigability.md +14 -0
- package/.agents/audit-checklists/performance.md +22 -0
- package/.agents/audit-checklists/privacy.md +21 -0
- package/.agents/audit-checklists/quality.md +18 -0
- package/.agents/audit-checklists/security.md +22 -0
- package/.agents/audit-checklists/seo.md +16 -0
- package/.agents/audit-checklists/sre.md +24 -0
- package/.agents/audit-checklists/ux-ui.md +21 -0
- package/.agents/docs/SDLC.md +63 -16
- package/.agents/docs/configuration.md +5 -3
- package/.agents/instructions.md +51 -21
- package/.agents/personas/architect.md +10 -7
- package/.agents/personas/engineer.md +4 -3
- package/.agents/personas/project-manager.md +5 -2
- package/.agents/personas/refactorer.md +5 -3
- package/.agents/rules/git-conventions.md +77 -0
- package/.agents/schemas/agentrc.schema.json +16 -4
- package/.agents/schemas/audit-rules.json +16 -2
- package/.agents/schemas/audit-rules.schema.json +7 -6
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +38 -0
- package/.agents/schemas/signal-event.schema.json +28 -13
- package/.agents/scripts/acceptance-spec-reconciler.js +6 -4
- package/.agents/scripts/check-context-budget.js +320 -0
- package/.agents/scripts/diagnose-friction.js +4 -4
- package/.agents/scripts/epic-audit-prepare.js +30 -2
- package/.agents/scripts/epic-audit-recheck.js +46 -13
- package/.agents/scripts/epic-deliver-prepare.js +80 -8
- package/.agents/scripts/epic-plan-spec.js +4 -8
- package/.agents/scripts/generate-lens-checklists.js +180 -0
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +300 -0
- package/.agents/scripts/lib/audit-suite/findings.js +27 -0
- package/.agents/scripts/lib/audit-suite/index.js +9 -0
- package/.agents/scripts/lib/audit-suite/lens-checklist.js +212 -0
- package/.agents/scripts/lib/audit-suite/selector.js +136 -5
- package/.agents/scripts/lib/checks/loop-health.js +340 -0
- package/.agents/scripts/lib/cli-args.js +8 -0
- package/.agents/scripts/lib/config/explain.js +4 -0
- package/.agents/scripts/lib/config/runners.js +21 -2
- package/.agents/scripts/lib/config/temp-paths.js +24 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +23 -3
- package/.agents/scripts/lib/config-settings-schema-quality.js +7 -0
- package/.agents/scripts/lib/doc-tiers.js +291 -0
- package/.agents/scripts/lib/epic-body-sections.js +5 -2
- package/.agents/scripts/lib/epic-merge-lock.js +83 -0
- package/.agents/scripts/lib/epic-plan-clarity.js +3 -1
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +66 -20
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +395 -86
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +299 -72
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +438 -0
- package/.agents/scripts/lib/gates/friction.js +15 -5
- package/.agents/scripts/lib/observability/perf-aggregator.js +30 -104
- package/.agents/scripts/lib/observability/perf-report-readers.js +1 -1
- package/.agents/scripts/lib/observability/signal-validator.js +204 -0
- package/.agents/scripts/lib/observability/signals-writer.js +157 -54
- package/.agents/scripts/lib/observability/tool-trace-hook.js +42 -4
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
- package/.agents/scripts/lib/orchestration/code-review.js +74 -4
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +213 -0
- package/.agents/scripts/lib/orchestration/doc-reader.js +4 -96
- package/.agents/scripts/lib/orchestration/docs-digest.js +34 -0
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +56 -19
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +22 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +188 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +6 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +69 -8
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +11 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +47 -61
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +39 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +258 -14
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +6 -0
- package/.agents/scripts/lib/orchestration/merge-block-class.js +218 -0
- package/.agents/scripts/lib/orchestration/plan-review-routing.js +1 -1
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +3 -3
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +63 -34
- package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +167 -52
- package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +49 -2
- package/.agents/scripts/lib/orchestration/retro-proposals.js +12 -55
- package/.agents/scripts/lib/orchestration/retro-runner.js +9 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +8 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +419 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +35 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +353 -69
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +66 -4
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +60 -9
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +7 -5
- package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +24 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +167 -8
- package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +163 -0
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +20 -9
- package/.agents/scripts/lib/planning-corpus.js +306 -0
- package/.agents/scripts/lib/signals/detectors/common.js +10 -10
- package/.agents/scripts/lib/signals/detectors/index.js +4 -4
- package/.agents/scripts/lib/signals/detectors/retry.js +19 -18
- package/.agents/scripts/lib/signals/detectors/rework.js +1 -1
- package/.agents/scripts/lib/signals/schema.js +56 -81
- package/.agents/scripts/lib/signals/span-tree.js +6 -5
- package/.agents/scripts/lib/story-plan.js +3 -0
- package/.agents/scripts/lib/wave-runner/tick.js +10 -2
- package/.agents/scripts/lifecycle-emit.js +39 -8
- package/.agents/scripts/providers/github/issues.js +12 -1
- package/.agents/scripts/resolve-doc-tiers.js +83 -0
- package/.agents/scripts/retro-run.js +51 -0
- package/.agents/scripts/signals-view.js +1 -1
- package/.agents/scripts/single-story-close.js +20 -1
- package/.agents/scripts/standalone-feedback-rollup.js +188 -0
- package/.agents/scripts/story-close.js +48 -0
- package/.agents/scripts/story-plan.js +51 -12
- package/.agents/scripts/validate-docs-freshness.js +69 -15
- package/.agents/skills/core/documentation-and-adrs/SKILL.md +58 -0
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +5 -3
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +20 -7
- package/.agents/skills/core/scope-triage/SKILL.md +61 -0
- package/.agents/skills/skills.index.json +3 -3
- package/.agents/workflows/audit-documentation.md +82 -2
- package/.agents/workflows/helpers/code-review.md +193 -44
- package/.agents/workflows/helpers/deliver-epic.md +128 -39
- package/.agents/workflows/helpers/deliver-stories.md +26 -0
- package/.agents/workflows/helpers/epic-audit.md +116 -283
- package/.agents/workflows/helpers/epic-deliver-story.md +14 -0
- package/.agents/workflows/helpers/epic-plan-decompose.md +18 -200
- package/.agents/workflows/helpers/epic-plan-spec.md +18 -180
- package/.agents/workflows/helpers/plan-epic.md +141 -105
- package/.agents/workflows/helpers/plan-story.md +32 -0
- package/.agents/workflows/helpers/single-story-deliver.md +43 -0
- package/.agents/workflows/loops/nightly-audit.md +9 -7
- package/docs/CHANGELOG.md +29 -0
- package/lib/cli/doctor.js +44 -0
- package/package.json +4 -3
- package/.agents/scripts/epic-plan-spec-validate.js +0 -111
- package/.agents/scripts/lib/feedback-loop/code-review-graduator.js +0 -207
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +0 -58
- package/.agents/scripts/lib/signals/detectors/hotspot.js +0 -292
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: ratchet-down gate for the always-loaded documentation context budget
|
|
3
|
+
* (Story #4438, Epic #4430 — Context Economy).
|
|
4
|
+
*
|
|
5
|
+
* Follows the standalone `check-arch-cycles.js` / `check-dead-exports.js`
|
|
6
|
+
* precedent — a pure-Node, baseline-aware, sub-second checker wired into the
|
|
7
|
+
* CI `baselines` job — rather than a `baselines/kinds/` metric. It measures the
|
|
8
|
+
* live byte total of two documentation read-tiers and compares each against a
|
|
9
|
+
* single committed budget in `baselines/context-budget.json`:
|
|
10
|
+
*
|
|
11
|
+
* - `alwaysLoaded` — the `CLAUDE.md` `@`-import closure re-paid on every
|
|
12
|
+
* session and every subagent spawn (instructions.md § 4).
|
|
13
|
+
* - `mandatoryRead` — the resolved `project.docsContextFiles` set.
|
|
14
|
+
*
|
|
15
|
+
* A tier that resolves **empty** is skipped silently (the `docsContextFiles`
|
|
16
|
+
* half skips when unconfigured / its files are absent), so a repo with no
|
|
17
|
+
* `CLAUDE.md` and no context docs is a clean no-op.
|
|
18
|
+
*
|
|
19
|
+
* Ratchet semantics (mirroring the sibling ratchets):
|
|
20
|
+
* - A gated tier grows beyond `baseline.tiers.<tier>.totalBytes +
|
|
21
|
+
* baseline.toleranceBytes` → exit 1, naming the tier and its delta.
|
|
22
|
+
* - A gated tier shrinks below its baseline total → printed as a `-`
|
|
23
|
+
* (removal) note, warning the baseline can be refreshed downward.
|
|
24
|
+
* Shrink-only exits 0.
|
|
25
|
+
* - Within tolerance / clean → exit 0.
|
|
26
|
+
* - Baseline file absent → warn + exit 0 (no-op; nothing to ratchet against).
|
|
27
|
+
*
|
|
28
|
+
* Tolerance lives in the baseline JSON (`toleranceBytes`) — there is **no**
|
|
29
|
+
* `.agentrc.json` config key; this is a framework-internal dogfooding ratchet,
|
|
30
|
+
* like arch-cycles.
|
|
31
|
+
*
|
|
32
|
+
* Flags:
|
|
33
|
+
* --baseline <path> override the budget path (default
|
|
34
|
+
* `baselines/context-budget.json`, resolved from cwd).
|
|
35
|
+
* --root <path> resolve tiers against an explicit repo root (default cwd).
|
|
36
|
+
* --update reseed the baseline from the current measurement (keeps
|
|
37
|
+
* the existing `toleranceBytes`, or defaults it) and exit 0.
|
|
38
|
+
* --json write the structured envelope to stdout.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import fs from 'node:fs';
|
|
42
|
+
import path from 'node:path';
|
|
43
|
+
import process from 'node:process';
|
|
44
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
45
|
+
import { resolveConfig } from './lib/config-resolver.js';
|
|
46
|
+
import { resolveDocTiers, tierTotalBytes } from './lib/doc-tiers.js';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The tiers this ratchet gates (in report order). `digestVisible` and
|
|
50
|
+
* `onDemand` are resolved by the tier map for the lens, but the byte budget
|
|
51
|
+
* intentionally gates only the two tiers the Epic AC names.
|
|
52
|
+
* @type {Array<'alwaysLoaded' | 'mandatoryRead'>}
|
|
53
|
+
*/
|
|
54
|
+
export const GATED_TIERS = ['alwaysLoaded', 'mandatoryRead'];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Default tolerance (bytes) seeded into a fresh baseline by `--update` when the
|
|
58
|
+
* existing baseline carries none.
|
|
59
|
+
* @type {number}
|
|
60
|
+
*/
|
|
61
|
+
export const DEFAULT_TOLERANCE_BYTES = 2048;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse argv for `--baseline <path>`, `--root <path>`, `--update`, `--json`.
|
|
65
|
+
* Exported so unit tests can pin the parser.
|
|
66
|
+
*
|
|
67
|
+
* @param {string[]} argv
|
|
68
|
+
* @returns {{ baselinePath: string|null, rootPath: string|null, update: boolean, json: boolean }}
|
|
69
|
+
*/
|
|
70
|
+
export function parseArgv(argv = []) {
|
|
71
|
+
let baselinePath = null;
|
|
72
|
+
let rootPath = null;
|
|
73
|
+
let update = false;
|
|
74
|
+
let json = false;
|
|
75
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
76
|
+
const a = argv[i];
|
|
77
|
+
if (a === '--baseline') {
|
|
78
|
+
const next = argv[i + 1];
|
|
79
|
+
if (next && !next.startsWith('--')) {
|
|
80
|
+
baselinePath = next;
|
|
81
|
+
i += 1;
|
|
82
|
+
}
|
|
83
|
+
} else if (a === '--root') {
|
|
84
|
+
const next = argv[i + 1];
|
|
85
|
+
if (next && !next.startsWith('--')) {
|
|
86
|
+
rootPath = next;
|
|
87
|
+
i += 1;
|
|
88
|
+
}
|
|
89
|
+
} else if (a === '--update') {
|
|
90
|
+
update = true;
|
|
91
|
+
} else if (a === '--json') {
|
|
92
|
+
json = true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { baselinePath, rootPath, update, json };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Read the committed budget envelope from disk. Returns the parsed object or
|
|
100
|
+
* `null` when the file is missing or unparseable.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} baselinePath
|
|
103
|
+
* @returns {{ toleranceBytes?: number, tiers?: Record<string, { totalBytes: number, files?: Array<{ path: string, bytes: number }> }> } | null}
|
|
104
|
+
*/
|
|
105
|
+
export function loadBaseline(baselinePath) {
|
|
106
|
+
try {
|
|
107
|
+
if (!fs.existsSync(baselinePath)) return null;
|
|
108
|
+
const parsed = JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
|
|
109
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
110
|
+
return parsed;
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build the committed-baseline envelope from a resolved tier map. Only the
|
|
118
|
+
* gated tiers are recorded (each as `{ totalBytes, files }`).
|
|
119
|
+
*
|
|
120
|
+
* @param {{ tiers: Record<string, Array<{ path: string, bytes: number }>> }} tierMap
|
|
121
|
+
* @param {number} toleranceBytes
|
|
122
|
+
* @returns {object}
|
|
123
|
+
*/
|
|
124
|
+
export function buildBaseline(tierMap, toleranceBytes) {
|
|
125
|
+
const tiers = {};
|
|
126
|
+
for (const name of GATED_TIERS) {
|
|
127
|
+
const files = tierMap.tiers[name] ?? [];
|
|
128
|
+
tiers[name] = { totalBytes: tierTotalBytes(files), files };
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
$schema: 'https://mandrel.dev/baselines/context-budget.schema.json',
|
|
132
|
+
generatedAt: new Date().toISOString(),
|
|
133
|
+
toleranceBytes,
|
|
134
|
+
tiers,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Pure diff: compare the current tier map against the committed baseline. A
|
|
140
|
+
* gated tier with no current files is skipped; a tier absent from the baseline
|
|
141
|
+
* is skipped. `grown` entries fail the gate; `shrunk` entries are informational.
|
|
142
|
+
*
|
|
143
|
+
* @param {{ tiers: Record<string, Array<{ path: string, bytes: number }>> }} tierMap
|
|
144
|
+
* @param {{ toleranceBytes?: number, tiers?: Record<string, { totalBytes: number }> }} baseline
|
|
145
|
+
* @returns {{
|
|
146
|
+
* grown: Array<{ tier: string, current: number, baseline: number, tolerance: number, delta: number }>,
|
|
147
|
+
* shrunk: Array<{ tier: string, current: number, baseline: number }>,
|
|
148
|
+
* skipped: string[],
|
|
149
|
+
* }}
|
|
150
|
+
*/
|
|
151
|
+
export function diffBudget(tierMap, baseline) {
|
|
152
|
+
const tolerance = Number.isFinite(baseline?.toleranceBytes)
|
|
153
|
+
? baseline.toleranceBytes
|
|
154
|
+
: 0;
|
|
155
|
+
const grown = [];
|
|
156
|
+
const shrunk = [];
|
|
157
|
+
const skipped = [];
|
|
158
|
+
for (const tier of GATED_TIERS) {
|
|
159
|
+
const files = tierMap.tiers[tier] ?? [];
|
|
160
|
+
const current = tierTotalBytes(files);
|
|
161
|
+
const baseTier = baseline?.tiers?.[tier];
|
|
162
|
+
if (
|
|
163
|
+
files.length === 0 ||
|
|
164
|
+
!baseTier ||
|
|
165
|
+
!Number.isFinite(baseTier.totalBytes)
|
|
166
|
+
) {
|
|
167
|
+
skipped.push(tier);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const baselineBytes = baseTier.totalBytes;
|
|
171
|
+
if (current > baselineBytes + tolerance) {
|
|
172
|
+
grown.push({
|
|
173
|
+
tier,
|
|
174
|
+
current,
|
|
175
|
+
baseline: baselineBytes,
|
|
176
|
+
tolerance,
|
|
177
|
+
delta: current - baselineBytes,
|
|
178
|
+
});
|
|
179
|
+
} else if (current < baselineBytes) {
|
|
180
|
+
shrunk.push({ tier, current, baseline: baselineBytes });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { grown, shrunk, skipped };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Render the human-readable diff. `+` lines are tiers that grew beyond
|
|
188
|
+
* tolerance (gate fail); `-` lines are tiers that shrank (refreshable
|
|
189
|
+
* baseline). A one-line summary always follows.
|
|
190
|
+
*
|
|
191
|
+
* @param {ReturnType<typeof diffBudget>} diff
|
|
192
|
+
* @returns {string}
|
|
193
|
+
*/
|
|
194
|
+
export function renderDiff(diff) {
|
|
195
|
+
const lines = [];
|
|
196
|
+
for (const g of diff.grown) {
|
|
197
|
+
lines.push(
|
|
198
|
+
`+ ${g.tier}: ${g.current} bytes exceeds budget ${g.baseline} + tolerance ${g.tolerance} (delta +${g.delta})`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
for (const s of diff.shrunk) {
|
|
202
|
+
lines.push(
|
|
203
|
+
`- ${s.tier}: ${s.current} bytes below baseline ${s.baseline} — refresh baselines/context-budget.json`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const tag = diff.grown.length > 0 ? '(gate fail)' : '(ok)';
|
|
207
|
+
lines.push(
|
|
208
|
+
`[context-budget] grown=${diff.grown.length} shrunk=${diff.shrunk.length} skipped=${diff.skipped.length} ${tag}`,
|
|
209
|
+
);
|
|
210
|
+
return lines.join('\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Top-level CLI entry. Exported so tests can drive the full pipeline against a
|
|
215
|
+
* tmpdir fixture with an injected config and sinks.
|
|
216
|
+
*
|
|
217
|
+
* @param {{
|
|
218
|
+
* argv?: string[],
|
|
219
|
+
* cwd?: string,
|
|
220
|
+
* config?: object,
|
|
221
|
+
* stdout?: { write: (s: string) => void },
|
|
222
|
+
* stderr?: { write: (s: string) => void },
|
|
223
|
+
* }} [opts]
|
|
224
|
+
* @returns {Promise<number>} 0 = clean / within tolerance / shrink-only / no-op;
|
|
225
|
+
* 1 = a gated tier grew beyond tolerance
|
|
226
|
+
*/
|
|
227
|
+
export async function runCli({
|
|
228
|
+
argv = process.argv.slice(2),
|
|
229
|
+
cwd = process.cwd(),
|
|
230
|
+
config,
|
|
231
|
+
stdout = process.stdout,
|
|
232
|
+
stderr = process.stderr,
|
|
233
|
+
} = {}) {
|
|
234
|
+
const { baselinePath, rootPath, update, json } = parseArgv(argv);
|
|
235
|
+
const root = rootPath ? path.resolve(cwd, rootPath) : path.resolve(cwd);
|
|
236
|
+
const resolvedBaselinePath = path.resolve(
|
|
237
|
+
cwd,
|
|
238
|
+
baselinePath ?? path.join('baselines', 'context-budget.json'),
|
|
239
|
+
);
|
|
240
|
+
const resolvedConfig = config ?? resolveConfig();
|
|
241
|
+
const tierMap = resolveDocTiers(resolvedConfig, { root });
|
|
242
|
+
|
|
243
|
+
if (update) {
|
|
244
|
+
const existing = loadBaseline(resolvedBaselinePath);
|
|
245
|
+
const tolerance = Number.isFinite(existing?.toleranceBytes)
|
|
246
|
+
? existing.toleranceBytes
|
|
247
|
+
: DEFAULT_TOLERANCE_BYTES;
|
|
248
|
+
const envelope = buildBaseline(tierMap, tolerance);
|
|
249
|
+
fs.mkdirSync(path.dirname(resolvedBaselinePath), { recursive: true });
|
|
250
|
+
fs.writeFileSync(
|
|
251
|
+
resolvedBaselinePath,
|
|
252
|
+
`${JSON.stringify(envelope, null, 2)}\n`,
|
|
253
|
+
);
|
|
254
|
+
if (!json) {
|
|
255
|
+
stdout.write(
|
|
256
|
+
`[context-budget] wrote baseline ${resolvedBaselinePath} (tolerance ${tolerance} bytes)\n`,
|
|
257
|
+
);
|
|
258
|
+
} else {
|
|
259
|
+
stdout.write(
|
|
260
|
+
`${JSON.stringify({ kind: 'context-budget-update', baselinePath: resolvedBaselinePath, envelope }, null, 2)}\n`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const baseline = loadBaseline(resolvedBaselinePath);
|
|
267
|
+
if (!baseline) {
|
|
268
|
+
if (json) {
|
|
269
|
+
stdout.write(
|
|
270
|
+
`${JSON.stringify({ kind: 'context-budget-report', baselinePath: resolvedBaselinePath, tiers: tierMap.tiers, grown: [], shrunk: [], skipped: GATED_TIERS, exitCode: 0, noBaseline: true }, null, 2)}\n`,
|
|
271
|
+
);
|
|
272
|
+
} else {
|
|
273
|
+
stderr.write(
|
|
274
|
+
`[context-budget] ⚠ budget not found at ${resolvedBaselinePath} — skipping (no-op)\n`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const diff = diffBudget(tierMap, baseline);
|
|
281
|
+
const exitCode = diff.grown.length > 0 ? 1 : 0;
|
|
282
|
+
|
|
283
|
+
if (json) {
|
|
284
|
+
const envelope = {
|
|
285
|
+
kind: 'context-budget-report',
|
|
286
|
+
baselinePath: resolvedBaselinePath,
|
|
287
|
+
toleranceBytes: Number.isFinite(baseline.toleranceBytes)
|
|
288
|
+
? baseline.toleranceBytes
|
|
289
|
+
: 0,
|
|
290
|
+
current: Object.fromEntries(
|
|
291
|
+
GATED_TIERS.map((t) => [t, tierTotalBytes(tierMap.tiers[t] ?? [])]),
|
|
292
|
+
),
|
|
293
|
+
grown: diff.grown,
|
|
294
|
+
shrunk: diff.shrunk,
|
|
295
|
+
skipped: diff.skipped,
|
|
296
|
+
exitCode,
|
|
297
|
+
};
|
|
298
|
+
stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
|
|
299
|
+
} else {
|
|
300
|
+
stdout.write(`\n--- context-budget preview ---\n`);
|
|
301
|
+
stdout.write(`${renderDiff(diff)}\n`);
|
|
302
|
+
if (exitCode === 1) {
|
|
303
|
+
stderr.write(
|
|
304
|
+
`[context-budget] ❌ a documentation tier grew beyond tolerance — refresh the budget consciously with \`node .agents/scripts/check-context-budget.js --update\` once the growth is intentional\n`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return exitCode;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function main() {
|
|
313
|
+
return runCli();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
runAsCli(import.meta.url, main, {
|
|
317
|
+
source: 'context-budget',
|
|
318
|
+
propagateExitCode: true,
|
|
319
|
+
errorPrefix: '[context-budget] ❌ Fatal error',
|
|
320
|
+
});
|
|
@@ -131,7 +131,7 @@ function buildFrictionSignal({
|
|
|
131
131
|
return {
|
|
132
132
|
kind: 'friction',
|
|
133
133
|
eventId: crypto.randomUUID(),
|
|
134
|
-
|
|
134
|
+
ts: new Date().toISOString(),
|
|
135
135
|
epicId: epicId ?? null,
|
|
136
136
|
storyId: storyId ?? null,
|
|
137
137
|
// 2-tier hierarchy (Epic #3163): no Task tier, so friction signals
|
|
@@ -139,11 +139,11 @@ function buildFrictionSignal({
|
|
|
139
139
|
// and always null.
|
|
140
140
|
taskId: null,
|
|
141
141
|
category,
|
|
142
|
-
|
|
142
|
+
emitter: {
|
|
143
143
|
tool: 'diagnose-friction.js',
|
|
144
144
|
command: commandStr,
|
|
145
145
|
},
|
|
146
|
-
details: errorPreview,
|
|
146
|
+
details: { errorPreview },
|
|
147
147
|
};
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -209,7 +209,7 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
209
209
|
|
|
210
210
|
// Story #2874 — accept story-only context (no parent Epic). When
|
|
211
211
|
// only the story is resolved, write to the standalone signals
|
|
212
|
-
// stream at `<tempRoot>/standalone/story-<sid>/signals.ndjson`
|
|
212
|
+
// stream at `<tempRoot>/standalone/stories/story-<sid>/signals.ndjson`
|
|
213
213
|
// by passing `epicId: null` through to the writer. The only case
|
|
214
214
|
// we still skip is fully-no-context (story unresolved).
|
|
215
215
|
if (resolvedStoryId != null) {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*
|
|
24
24
|
* Story #3939 — depth-aware lenses. The `depth` field tells the audit
|
|
25
25
|
* executor (`helpers/epic-audit.md`) how thorough each selected lens should
|
|
26
|
-
* be on this Epic without ever skipping a selected
|
|
26
|
+
* be on this Epic without ever skipping a selected lens:
|
|
27
27
|
* `light` shrinks a lens's sweep to the changed surface + Critical/High
|
|
28
28
|
* findings, `standard` is today's behavior, and `deep` widens the sweep to
|
|
29
29
|
* the directly-touched modules. Depth never changes which lenses fire, the
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
* "epicBranch": "epic/2586",
|
|
54
54
|
* "depth": "deep",
|
|
55
55
|
* "selectedAudits": ["audit-security", "audit-privacy"],
|
|
56
|
+
* "epicCloseLenses": ["audit-security"],
|
|
56
57
|
* "changeSetAudits": ["audit-privacy"],
|
|
57
58
|
* "riskRoutedAudits": ["audit-security"],
|
|
58
59
|
* "globalLenses": [],
|
|
@@ -97,7 +98,10 @@ import {
|
|
|
97
98
|
import { defineFlags } from './lib/cli-args.js';
|
|
98
99
|
import { runAsCli } from './lib/cli-utils.js';
|
|
99
100
|
import { resolveConfig } from './lib/config-resolver.js';
|
|
100
|
-
import {
|
|
101
|
+
import {
|
|
102
|
+
resolveAuditLenses,
|
|
103
|
+
selectEpicCloseLenses,
|
|
104
|
+
} from './lib/orchestration/code-review.js';
|
|
101
105
|
import { read as readPlanState } from './lib/orchestration/epic-plan-state-store.js';
|
|
102
106
|
import { resolveDepth } from './lib/orchestration/review-depth.js';
|
|
103
107
|
import { createProvider } from './lib/provider-factory.js';
|
|
@@ -121,6 +125,14 @@ Output (JSON envelope on stdout):
|
|
|
121
125
|
each SELECTED lens runs; it never changes which lenses
|
|
122
126
|
fire, the severity taxonomy, or the Phase 4 halting rule.
|
|
123
127
|
selectedAudits De-duplicated union of changeSetAudits + riskRoutedAudits.
|
|
128
|
+
epicCloseLenses The SLIM Epic-close roster (Story #4412): selectedAudits
|
|
129
|
+
with every local-tier change-set lens excluded, keeping
|
|
130
|
+
only cumulative + global change-set lenses plus every
|
|
131
|
+
risk-routed lens. This is the roster the Phase 5
|
|
132
|
+
code-review pass walks over the cumulative Epic diff —
|
|
133
|
+
local-tier concerns are already verified shift-left
|
|
134
|
+
(write-time checklists + the Story-scope local-lens pass),
|
|
135
|
+
so they are not re-run at Epic close.
|
|
124
136
|
changeSetAudits Lenses the change-set selector chose.
|
|
125
137
|
riskRoutedAudits Lenses routed from the model-judged high-risk axes plus the
|
|
126
138
|
navigability lens when a changed file matches a configured
|
|
@@ -409,6 +421,21 @@ export async function runEpicAuditPrepare(values, deps = {}) {
|
|
|
409
421
|
);
|
|
410
422
|
const selectedAudits = unionAudits(changeSetAudits, riskRoutedAudits);
|
|
411
423
|
|
|
424
|
+
// Story #4412 (Epic #4405) — the SLIM Epic-close roster. Restrict the gate3
|
|
425
|
+
// selection to the tiers the Epic-close pass owns: cumulative + global
|
|
426
|
+
// change-set lenses plus every risk-routed lens, dropping every local-tier
|
|
427
|
+
// change-set lens (routed off `resolveLensTier` in `selectEpicCloseLenses`).
|
|
428
|
+
// Local-tier concerns are already verified shift-left — the write-time
|
|
429
|
+
// checklist threading (#4410) and the maker-blind Story-scope local-lens
|
|
430
|
+
// pass (#4409) — so re-running them over the cumulative diff at close would
|
|
431
|
+
// verify the same concern at a second tier. This is the roster the Phase 5
|
|
432
|
+
// code-review pass walks; `selectedAudits` stays the pre-slim union for
|
|
433
|
+
// observability.
|
|
434
|
+
const epicCloseLenses = selectEpicCloseLenses({
|
|
435
|
+
changeSetAudits,
|
|
436
|
+
riskRoutedAudits,
|
|
437
|
+
});
|
|
438
|
+
|
|
412
439
|
// Epic #4131 (F2) — surface which selected lenses are on the global-lens
|
|
413
440
|
// allowlist so the helper runs them against the WHOLE route tree, exempt from
|
|
414
441
|
// the cross-epic-leak guard's change-set narrowing (`#3362`). The exemption
|
|
@@ -439,6 +466,7 @@ export async function runEpicAuditPrepare(values, deps = {}) {
|
|
|
439
466
|
epicBranch,
|
|
440
467
|
depth,
|
|
441
468
|
selectedAudits,
|
|
469
|
+
epicCloseLenses,
|
|
442
470
|
changeSetAudits,
|
|
443
471
|
riskRoutedAudits,
|
|
444
472
|
globalLenses,
|
|
@@ -10,10 +10,18 @@
|
|
|
10
10
|
* subset of audit lenses whose `filePatterns` overlap that list.
|
|
11
11
|
*
|
|
12
12
|
* The CLI is intentionally narrower than `select-audits.js`: it does NOT
|
|
13
|
-
* run a git diff
|
|
14
|
-
* triggers
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* run a git diff and does NOT consult the Epic ticket body for keyword
|
|
14
|
+
* triggers. The auto-fix tail already knows exactly which paths it touched —
|
|
15
|
+
* only file-pattern overlap is relevant for deciding which lenses are stale
|
|
16
|
+
* and need re-invocation.
|
|
17
|
+
*
|
|
18
|
+
* Tier gate (Epic #4405): this is the **Epic-close** re-check, so it emits
|
|
19
|
+
* only non-`local` lenses. A `local`-tier lens is verified shift-left at
|
|
20
|
+
* write-time and Story-scope review; re-running it here would verify the same
|
|
21
|
+
* concern at a second tier. The overlap set is therefore filtered through
|
|
22
|
+
* `resolveLensTier`, dropping every `local` lens (including the universal
|
|
23
|
+
* `audit-clean-code`) so a focused-fix commit cannot drag one back into the
|
|
24
|
+
* Epic-close re-check.
|
|
17
25
|
*
|
|
18
26
|
* Usage:
|
|
19
27
|
* node .agents/scripts/epic-audit-recheck.js \
|
|
@@ -40,7 +48,10 @@
|
|
|
40
48
|
|
|
41
49
|
import fs from 'node:fs';
|
|
42
50
|
import path from 'node:path';
|
|
43
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
matchesAnyFilePattern,
|
|
53
|
+
resolveLensTier,
|
|
54
|
+
} from './lib/audit-suite/index.js';
|
|
44
55
|
import { defineFlags } from './lib/cli-args.js';
|
|
45
56
|
import { runAsCli } from './lib/cli-utils.js';
|
|
46
57
|
import {
|
|
@@ -148,23 +159,45 @@ export function loadAuditRules(deps = {}) {
|
|
|
148
159
|
|
|
149
160
|
/**
|
|
150
161
|
* Pure overlap detector. Walks the audit-rules document and returns the
|
|
151
|
-
* names of every audit whose `triggers.filePatterns` matches
|
|
152
|
-
* entry in `files`. Lenses without `filePatterns` (or with an
|
|
153
|
-
* array) NEVER appear in the output —
|
|
154
|
-
*
|
|
155
|
-
*
|
|
162
|
+
* names of every **non-`local`** audit whose `triggers.filePatterns` matches
|
|
163
|
+
* at least one entry in `files`. Lenses without `filePatterns` (or with an
|
|
164
|
+
* empty array) NEVER appear in the output — keyword triggers are
|
|
165
|
+
* intentionally ignored: this CLI answers "what was invalidated by the
|
|
166
|
+
* touched paths?", not "what should I run from scratch?".
|
|
167
|
+
*
|
|
168
|
+
* The output is additionally gated by tier (Epic #4405): a `local`-tier lens
|
|
169
|
+
* is verified shift-left (write-time + Story-scope) and is never re-run at
|
|
170
|
+
* Epic close, so it is dropped here even when its patterns overlap. This
|
|
171
|
+
* matters now that `audit-clean-code` carries the universal `**` glob — its
|
|
172
|
+
* overlap is universal, so without the tier gate every focused-fix commit
|
|
173
|
+
* would drag it (and any other local lens) back into the Epic-close re-check.
|
|
156
174
|
*
|
|
157
175
|
* @param {{ audits: Record<string, { triggers?: { filePatterns?: string[] } }> }} rules
|
|
158
176
|
* @param {string[]} files
|
|
177
|
+
* @param {(lens: string) => string} [resolveLensTierFn] tier resolver
|
|
178
|
+
* (injectable seam; defaults to {@link resolveLensTier}).
|
|
159
179
|
*/
|
|
160
|
-
export function selectOverlappingAudits(
|
|
180
|
+
export function selectOverlappingAudits(
|
|
181
|
+
rules,
|
|
182
|
+
files,
|
|
183
|
+
resolveLensTierFn = resolveLensTier,
|
|
184
|
+
) {
|
|
161
185
|
const selected = [];
|
|
162
186
|
for (const [auditName, ruleOpts] of Object.entries(rules?.audits ?? {})) {
|
|
163
187
|
const patterns = ruleOpts?.triggers?.filePatterns;
|
|
164
188
|
if (!Array.isArray(patterns) || patterns.length === 0) continue;
|
|
165
|
-
if (matchesAnyFilePattern(patterns, files))
|
|
166
|
-
|
|
189
|
+
if (!matchesAnyFilePattern(patterns, files)) continue;
|
|
190
|
+
// Epic-close tier gate: skip `local` lenses (verified shift-left). A
|
|
191
|
+
// malformed/unknown tier is drift the schema guards against — skip it
|
|
192
|
+
// rather than throw the whole selection.
|
|
193
|
+
let tier;
|
|
194
|
+
try {
|
|
195
|
+
tier = resolveLensTierFn(auditName);
|
|
196
|
+
} catch {
|
|
197
|
+
continue;
|
|
167
198
|
}
|
|
199
|
+
if (tier === 'local') continue;
|
|
200
|
+
selected.push(auditName);
|
|
168
201
|
}
|
|
169
202
|
return selected;
|
|
170
203
|
}
|
|
@@ -37,12 +37,13 @@ import path from 'node:path';
|
|
|
37
37
|
import { parseArgs } from 'node:util';
|
|
38
38
|
|
|
39
39
|
import { runBootSweep } from './boot-sweep.js';
|
|
40
|
+
import { buildChecklistPayload } from './lib/audit-suite/index.js';
|
|
40
41
|
import { runAsCli } from './lib/cli-utils.js';
|
|
41
42
|
import { getPaths, getRunners, resolveConfig } from './lib/config-resolver.js';
|
|
42
43
|
import { currentBranch as gitCurrentBranch } from './lib/git-branch-lifecycle.js';
|
|
43
44
|
import { getEpicBranch, gitSpawn } from './lib/git-utils.js';
|
|
44
45
|
import { Logger } from './lib/Logger.js';
|
|
45
|
-
import {
|
|
46
|
+
import { ensureDocsDigest } from './lib/orchestration/docs-digest.js';
|
|
46
47
|
import {
|
|
47
48
|
resolveOperator,
|
|
48
49
|
runPrepareGuards,
|
|
@@ -60,6 +61,7 @@ import {
|
|
|
60
61
|
import { runBuildWaveDagPhase } from './lib/orchestration/epic-runner/phases/build-wave-dag.js';
|
|
61
62
|
import { runSnapshotPhase } from './lib/orchestration/epic-runner/phases/snapshot.js';
|
|
62
63
|
import { StoryLauncher } from './lib/orchestration/epic-runner/story-launcher.js';
|
|
64
|
+
import { collectStoryAssumptionEntries } from './lib/orchestration/file-assumptions.js';
|
|
63
65
|
import {
|
|
64
66
|
computeBaseSha,
|
|
65
67
|
readPreflightCache,
|
|
@@ -357,14 +359,69 @@ async function writeDocsDigest({ epicId, cwd, config }) {
|
|
|
357
359
|
const paths = getPaths(config);
|
|
358
360
|
const root = path.resolve(cwd ?? process.cwd());
|
|
359
361
|
const docsRoot = path.resolve(root, paths.docsRoot);
|
|
360
|
-
const digest = await buildDocsDigest({ docsContextFiles, docsRoot });
|
|
361
|
-
if (digest == null) return null;
|
|
362
|
-
|
|
363
362
|
const relPath = path.join(paths.tempRoot, `epic-${epicId}`, 'docs-digest.md');
|
|
364
363
|
const absPath = path.resolve(root, relPath);
|
|
365
|
-
await
|
|
366
|
-
|
|
367
|
-
|
|
364
|
+
const result = await ensureDocsDigest({
|
|
365
|
+
docsContextFiles,
|
|
366
|
+
docsRoot,
|
|
367
|
+
outputPath: absPath,
|
|
368
|
+
});
|
|
369
|
+
return result ? relPath : null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Thread footprint-matched **local**-lens authoring checklists into the
|
|
374
|
+
* per-Story dispatch entries (Epic #4405, Story #4410). For each planned Story,
|
|
375
|
+
* derive its predicted footprint from the full ticket body's `changes[]` /
|
|
376
|
+
* `references[]` entries, build the budget-capped checklist payload (matched by
|
|
377
|
+
* `resolveLensTier(lens) === 'local'` + `matchesAnyFilePattern` — NOT
|
|
378
|
+
* `selectAudits`, so no provider or git diff runs here), and write it to
|
|
379
|
+
* `<tempRoot>/epic-<id>/checklists/story-<sid>.md`. The parent threads the
|
|
380
|
+
* returned repo-relative `checklistPath` into that child's maker prompt, next
|
|
381
|
+
* to `docsDigestPath`; a Story that matches no local lens gets a `null` path
|
|
382
|
+
* and no file.
|
|
383
|
+
*
|
|
384
|
+
* @param {{
|
|
385
|
+
* epicId: number,
|
|
386
|
+
* cwd?: string,
|
|
387
|
+
* config: object,
|
|
388
|
+
* stories: Array<{ storyId: number, worktree?: string, title?: string }>,
|
|
389
|
+
* storyById: Map<number, object>,
|
|
390
|
+
* }} args
|
|
391
|
+
* @returns {Promise<Array<{ storyId: number, worktree?: string, title?: string, checklistPath: string|null }>>}
|
|
392
|
+
*/
|
|
393
|
+
export async function writeStoryChecklists({
|
|
394
|
+
epicId,
|
|
395
|
+
cwd,
|
|
396
|
+
config,
|
|
397
|
+
stories,
|
|
398
|
+
storyById,
|
|
399
|
+
buildPayload = buildChecklistPayload,
|
|
400
|
+
}) {
|
|
401
|
+
const paths = getPaths(config);
|
|
402
|
+
const root = path.resolve(cwd ?? process.cwd());
|
|
403
|
+
|
|
404
|
+
return Promise.all(
|
|
405
|
+
stories.map(async (entry) => {
|
|
406
|
+
const ticket = storyById.get(Number(entry.storyId));
|
|
407
|
+
const footprint = ticket
|
|
408
|
+
? collectStoryAssumptionEntries(ticket).map((e) => e.path)
|
|
409
|
+
: [];
|
|
410
|
+
const { payload } = buildPayload({ footprint, logger: Logger });
|
|
411
|
+
if (!payload) return { ...entry, checklistPath: null };
|
|
412
|
+
|
|
413
|
+
const relPath = path.join(
|
|
414
|
+
paths.tempRoot,
|
|
415
|
+
`epic-${epicId}`,
|
|
416
|
+
'checklists',
|
|
417
|
+
`story-${entry.storyId}.md`,
|
|
418
|
+
);
|
|
419
|
+
const absPath = path.resolve(root, relPath);
|
|
420
|
+
await fs.promises.mkdir(path.dirname(absPath), { recursive: true });
|
|
421
|
+
await fs.promises.writeFile(absPath, payload, 'utf-8');
|
|
422
|
+
return { ...entry, checklistPath: relPath };
|
|
423
|
+
}),
|
|
424
|
+
);
|
|
368
425
|
}
|
|
369
426
|
|
|
370
427
|
export async function runEpicDeliverPrepare({
|
|
@@ -450,11 +507,26 @@ export async function runEpicDeliverPrepare({
|
|
|
450
507
|
// dispatch from. This is a dispatch *hint* — the ready-set tick decides
|
|
451
508
|
// which Stories to dispatch on each beat; the prepare only enumerates them.
|
|
452
509
|
const launcher = new StoryLauncher({ concurrencyCap });
|
|
453
|
-
const
|
|
510
|
+
const plannedStories = launcher.planWave(openStories).map((entry, i) => ({
|
|
454
511
|
...entry,
|
|
455
512
|
title: openStories[i]?.title ?? '',
|
|
456
513
|
}));
|
|
457
514
|
|
|
515
|
+
// Thread footprint-matched local-lens authoring checklists into each Story's
|
|
516
|
+
// dispatch entry (Story #4410). Keyed off the full discovered tickets (which
|
|
517
|
+
// carry the `changes[]`/`references[]` bodies the footprint is derived from);
|
|
518
|
+
// a Story that matches no local lens gets a `null` checklistPath.
|
|
519
|
+
const storyById = new Map(
|
|
520
|
+
(state.stories ?? []).map((t) => [Number(t.id ?? t.number), t]),
|
|
521
|
+
);
|
|
522
|
+
const stories = await writeStoryChecklists({
|
|
523
|
+
epicId,
|
|
524
|
+
cwd,
|
|
525
|
+
config,
|
|
526
|
+
stories: plannedStories,
|
|
527
|
+
storyById,
|
|
528
|
+
});
|
|
529
|
+
|
|
458
530
|
// Persist the `--ignore-concurrency-hazards` flag on the checkpoint so
|
|
459
531
|
// retro tooling can flag a run that shipped despite an outstanding hazard
|
|
460
532
|
// (the warning above is one-shot; the checkpoint is durable).
|
|
@@ -70,10 +70,6 @@ import {
|
|
|
70
70
|
planEpic,
|
|
71
71
|
resolveAcceptancePersistence,
|
|
72
72
|
} from './lib/orchestration/epic-plan-spec/phases/plan-epic.js';
|
|
73
|
-
import {
|
|
74
|
-
ACCEPTANCE_SPEC_SYSTEM_PROMPT,
|
|
75
|
-
TECH_SPEC_SYSTEM_PROMPT,
|
|
76
|
-
} from './lib/orchestration/epic-plan-spec/phases/prompts.js';
|
|
77
73
|
import {
|
|
78
74
|
loadRiskVerdict,
|
|
79
75
|
validateRiskVerdict,
|
|
@@ -86,7 +82,6 @@ import { createProvider } from './lib/provider-factory.js';
|
|
|
86
82
|
// Re-exports for stable public API: tests and external callers import these
|
|
87
83
|
// from `epic-plan-spec.js`. The implementations live in `phases/`.
|
|
88
84
|
export {
|
|
89
|
-
ACCEPTANCE_SPEC_SYSTEM_PROMPT,
|
|
90
85
|
buildAuthoringContext,
|
|
91
86
|
drainPendingCleanupAtBoot,
|
|
92
87
|
loadRiskVerdict,
|
|
@@ -96,7 +91,6 @@ export {
|
|
|
96
91
|
resolveReviewRouting,
|
|
97
92
|
runSpecFreshnessCheck,
|
|
98
93
|
runSpecPhase,
|
|
99
|
-
TECH_SPEC_SYSTEM_PROMPT,
|
|
100
94
|
validateRiskVerdict,
|
|
101
95
|
};
|
|
102
96
|
|
|
@@ -126,8 +120,10 @@ async function main() {
|
|
|
126
120
|
// Story #2278 — in --emit-context mode stdout is reserved for the JSON
|
|
127
121
|
// envelope. Flip every Logger sink that could land on stdout to stderr
|
|
128
122
|
// *before* any pipeline code runs (drainPendingCleanupAtBoot,
|
|
129
|
-
// buildAuthoringContext →
|
|
130
|
-
//
|
|
123
|
+
// buildAuthoringContext → ensureDocsDigest — Story #4433 cut the digest
|
|
124
|
+
// build over from the old buildDocsContext/scrapeProjectDocs full-content
|
|
125
|
+
// read path), so a captured file is unconditionally parseable by
|
|
126
|
+
// `JSON.parse`.
|
|
131
127
|
if (emitContext) routeAllOutputToStderr();
|
|
132
128
|
|
|
133
129
|
try {
|