mandrel 1.88.0 → 1.90.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 +62 -27
- package/.agents/docs/configuration.md +5 -4
- 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 +10 -6
- package/.agents/schemas/audit-rules.json +16 -2
- package/.agents/schemas/audit-rules.schema.json +7 -6
- package/.agents/schemas/lifecycle/epic.blocked.schema.json +1 -1
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +39 -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/coverage-capture.js +17 -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/close-validation/gates.js +64 -24
- package/.agents/scripts/lib/config/ci.js +12 -1
- package/.agents/scripts/lib/config/runners.js +13 -5
- package/.agents/scripts/lib/config/temp-paths.js +24 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -8
- 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 +47 -15
- 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/npm-scripts.js +55 -0
- 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 +193 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +6 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +248 -13
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +109 -12
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +47 -61
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +46 -4
- 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 +246 -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/close-validation.js +5 -1
- 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/pre-merge-validation.js +8 -1
- 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 +116 -43
- package/.agents/workflows/helpers/deliver-epic.md +123 -54
- package/.agents/workflows/helpers/deliver-stories.md +26 -0
- package/.agents/workflows/helpers/epic-audit.md +116 -366
- 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 -224
- 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,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* consolidation-precondition.js — deterministic dispatch gate for the Phase
|
|
3
|
+
* 8.3 Holistic Consolidation sub-agent (Story #4431, Epic #4429).
|
|
4
|
+
*
|
|
5
|
+
* The Phase 8.3 consolidation critic (`epic-plan-consolidate`) is a genuine
|
|
6
|
+
* fresh-context `Agent` dispatch — every call re-pays the full always-loaded
|
|
7
|
+
* context (`.agents/instructions.md` and its always-on rules, § 4). When the
|
|
8
|
+
* decomposer's draft `tickets.json` already matches the Tech Spec's `##
|
|
9
|
+
* Delivery Slicing` target 1:1 (same shippable-Story count, and the
|
|
10
|
+
* `depends_on` shape already agrees with each slice's declared
|
|
11
|
+
* "Independent?" answer), there is nothing left for the critic to
|
|
12
|
+
* reconcile — dispatching it is pure token spend for a no-op. This module
|
|
13
|
+
* computes that decision **deterministically**, off the same two inputs the
|
|
14
|
+
* critic itself reads (the draft array and the Epic body's Delivery Slicing
|
|
15
|
+
* table), so `helpers/plan-epic.md` Phase 8.3 can skip the sub-agent
|
|
16
|
+
* dispatch when it is provably safe to.
|
|
17
|
+
*
|
|
18
|
+
* **Fail-open by design.** Every ambiguous case — a missing or unparseable
|
|
19
|
+
* Delivery Slicing section, an unparseable "Independent?" cell — resolves to
|
|
20
|
+
* `dispatch: true`. This gate can only ever *save* a dispatch when it is
|
|
21
|
+
* confident the critic has nothing to do; it never disables the critic's
|
|
22
|
+
* ability to catch a real divergence. Phase 8.4 (reachability critic), 8.5
|
|
23
|
+
* (pre-mortem critic), and the deterministic ticket validator are
|
|
24
|
+
* unconditional — this precondition governs only the 8.3 dispatch.
|
|
25
|
+
*
|
|
26
|
+
* Pure, synchronous, no I/O — callers own reading `tickets.json` and the
|
|
27
|
+
* Epic body off disk / the GitHub API.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { DELIVERY_SLICING_RE as DELIVERY_SLICING_HEADING_RE } from '../epic-body-sections.js';
|
|
31
|
+
|
|
32
|
+
/** A row is a markdown table line: starts with `|` once trimmed. */
|
|
33
|
+
const TABLE_ROW_RE = /^\|/;
|
|
34
|
+
|
|
35
|
+
/** A markdown table separator row: `|---|:---:|---:|` (dashes, colons, pipes only). */
|
|
36
|
+
const TABLE_SEPARATOR_RE = /^\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?$/;
|
|
37
|
+
|
|
38
|
+
/** The literal goal-section token that marks the wave-0 BDD scaffold Story. */
|
|
39
|
+
const BDD_SCAFFOLD_GOAL_TOKEN = 'bdd-scaffold';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Split one markdown table row into trimmed cell strings.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} line
|
|
45
|
+
* @returns {string[]}
|
|
46
|
+
*/
|
|
47
|
+
function splitTableRow(line) {
|
|
48
|
+
let trimmed = line.trim();
|
|
49
|
+
if (trimmed.startsWith('|')) trimmed = trimmed.slice(1);
|
|
50
|
+
if (trimmed.endsWith('|')) trimmed = trimmed.slice(0, -1);
|
|
51
|
+
return trimmed.split('|').map((cell) => cell.trim());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse an "Independent?" cell per the pinned rule: match the cell's
|
|
56
|
+
* leading word case-insensitively as `Yes` or `No`. Any other leading word
|
|
57
|
+
* (or an empty cell) is unparseable and returns `null` — the caller must
|
|
58
|
+
* fail open (`dispatch: true`) rather than guess.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} cell
|
|
61
|
+
* @returns {boolean|null} `true` for Yes, `false` for No, `null` when unparseable.
|
|
62
|
+
*/
|
|
63
|
+
function parseIndependentCell(cell) {
|
|
64
|
+
const match = String(cell ?? '')
|
|
65
|
+
.trim()
|
|
66
|
+
.match(/^[A-Za-z]+/);
|
|
67
|
+
if (!match) return null;
|
|
68
|
+
const word = match[0].toLowerCase();
|
|
69
|
+
if (word === 'yes') return true;
|
|
70
|
+
if (word === 'no') return false;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Locate and parse the `## Delivery Slicing` markdown table out of the Epic
|
|
76
|
+
* body (which carries the folded Tech Spec sections — Story #4324). Returns
|
|
77
|
+
* `null` when the heading is absent, no table follows it, the table has no
|
|
78
|
+
* "Independent?" column, or any data row's "Independent?" cell is
|
|
79
|
+
* unparseable — every one of those is a fail-open signal for the caller.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} epicBody
|
|
82
|
+
* @returns {{ slice: string, independent: boolean }[] | null}
|
|
83
|
+
*/
|
|
84
|
+
export function parseDeliverySlicingTable(epicBody) {
|
|
85
|
+
if (typeof epicBody !== 'string' || epicBody.length === 0) return null;
|
|
86
|
+
|
|
87
|
+
const lines = epicBody.split(/\r?\n/);
|
|
88
|
+
const headingIdx = lines.findIndex((line) =>
|
|
89
|
+
DELIVERY_SLICING_HEADING_RE.test(line.trim()),
|
|
90
|
+
);
|
|
91
|
+
if (headingIdx === -1) return null;
|
|
92
|
+
|
|
93
|
+
let i = headingIdx + 1;
|
|
94
|
+
while (i < lines.length && lines[i].trim() === '') i++;
|
|
95
|
+
if (i >= lines.length || !TABLE_ROW_RE.test(lines[i].trim())) return null;
|
|
96
|
+
|
|
97
|
+
const headerCells = splitTableRow(lines[i]);
|
|
98
|
+
i++;
|
|
99
|
+
if (i >= lines.length || !TABLE_SEPARATOR_RE.test(lines[i].trim())) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
i++;
|
|
103
|
+
|
|
104
|
+
const independentIdx = headerCells.findIndex((cell) =>
|
|
105
|
+
/independent/i.test(cell),
|
|
106
|
+
);
|
|
107
|
+
if (independentIdx === -1) return null;
|
|
108
|
+
|
|
109
|
+
const rows = [];
|
|
110
|
+
while (i < lines.length && TABLE_ROW_RE.test(lines[i].trim())) {
|
|
111
|
+
const cells = splitTableRow(lines[i]);
|
|
112
|
+
const independent = parseIndependentCell(cells[independentIdx]);
|
|
113
|
+
if (independent === null) return null; // unparseable cell → fail open
|
|
114
|
+
rows.push({ slice: (cells[0] ?? '').trim(), independent });
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return rows.length > 0 ? rows : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* True when `story` is the recognized wave-0 BDD scaffold Story — identified
|
|
123
|
+
* by the literal `bdd-scaffold` goal token the `epic-plan-decompose-author`
|
|
124
|
+
* skill's WAVE-0 BDD SCAFFOLD STORY section requires. Scaffold Stories are
|
|
125
|
+
* not a Delivery Slicing slice, so they are excluded from the count
|
|
126
|
+
* comparison — BDD-adopting consumer repos still benefit from the
|
|
127
|
+
* precondition gate rather than always paying the 8.3 dispatch.
|
|
128
|
+
*
|
|
129
|
+
* @param {{ body?: unknown }} story
|
|
130
|
+
* @returns {boolean}
|
|
131
|
+
*/
|
|
132
|
+
function isBddScaffoldStory(story) {
|
|
133
|
+
const body = story?.body;
|
|
134
|
+
return (
|
|
135
|
+
typeof body === 'string' &&
|
|
136
|
+
body.toLowerCase().includes(BDD_SCAFFOLD_GOAL_TOKEN)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Evaluate whether the Phase 8.3 consolidation sub-agent needs to run.
|
|
142
|
+
*
|
|
143
|
+
* @param {object} input
|
|
144
|
+
* @param {object[]} input.draftStories - The draft `tickets.json` array
|
|
145
|
+
* (`epic-plan-decompose-author`'s output) — raw Story ticket objects with
|
|
146
|
+
* top-level `slug` / `depends_on` / `body` (serialized string).
|
|
147
|
+
* @param {string} input.epicBody - The Epic body carrying the folded Tech
|
|
148
|
+
* Spec sections (`## Delivery Slicing` onward).
|
|
149
|
+
* @returns {{ dispatch: boolean, reasons: string[] }} `dispatch: false` only
|
|
150
|
+
* when the draft matches the Delivery Slicing table 1:1 in count and
|
|
151
|
+
* dependency shape; `dispatch: true` (with `reasons`) otherwise, including
|
|
152
|
+
* every fail-open case.
|
|
153
|
+
*/
|
|
154
|
+
export function evaluateConsolidationPrecondition({ draftStories, epicBody }) {
|
|
155
|
+
if (!Array.isArray(draftStories)) {
|
|
156
|
+
throw new TypeError(
|
|
157
|
+
'evaluateConsolidationPrecondition: draftStories must be an array',
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const slicing = parseDeliverySlicingTable(epicBody);
|
|
162
|
+
if (!slicing) {
|
|
163
|
+
return {
|
|
164
|
+
dispatch: true,
|
|
165
|
+
reasons: [
|
|
166
|
+
'Delivery Slicing section is missing or unparseable — fail-open to the critic.',
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const slicedStories = draftStories.filter(
|
|
172
|
+
(story) => !isBddScaffoldStory(story),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (slicedStories.length !== slicing.length) {
|
|
176
|
+
return {
|
|
177
|
+
dispatch: true,
|
|
178
|
+
reasons: [
|
|
179
|
+
`Story count diverges from Delivery Slicing: ${slicing.length} proposed slice(s) vs ${slicedStories.length} non-scaffold draft Story(ies).`,
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const reasons = [];
|
|
185
|
+
for (let idx = 0; idx < slicing.length; idx++) {
|
|
186
|
+
const slice = slicing[idx];
|
|
187
|
+
const story = slicedStories[idx];
|
|
188
|
+
const dependsOn = Array.isArray(story?.depends_on) ? story.depends_on : [];
|
|
189
|
+
const hasDeps = dependsOn.length > 0;
|
|
190
|
+
const storyLabel = story?.slug ?? story?.title ?? `<story ${idx + 1}>`;
|
|
191
|
+
|
|
192
|
+
if (slice.independent === false && !hasDeps) {
|
|
193
|
+
reasons.push(
|
|
194
|
+
`Slice "${slice.slice}" (position ${idx + 1}) is marked Independent: No but draft Story "${storyLabel}" declares no depends_on.`,
|
|
195
|
+
);
|
|
196
|
+
} else if (slice.independent === true && hasDeps) {
|
|
197
|
+
reasons.push(
|
|
198
|
+
`Slice "${slice.slice}" (position ${idx + 1}) is marked Independent: Yes but draft Story "${storyLabel}" declares depends_on [${dependsOn.join(', ')}].`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (reasons.length > 0) {
|
|
204
|
+
return { dispatch: true, reasons };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
dispatch: false,
|
|
209
|
+
reasons: [
|
|
210
|
+
`Draft matches Delivery Slicing 1:1 in count and dependency shape (${slicing.length} slice(s)) — skipping the 8.3 consolidation dispatch.`,
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { getPaths } from '../config-resolver.js';
|
|
4
|
-
import { Logger } from '../Logger.js';
|
|
5
|
-
import { applyBudget } from './planning-context-budget.js';
|
|
6
3
|
|
|
7
4
|
/**
|
|
8
5
|
* Read an explicit list of doc files relative to `docsRoot`, returning one
|
|
9
6
|
* `{ name, path, content }` object per file that exists and reads cleanly.
|
|
10
|
-
* Missing or unreadable files are skipped silently
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* Missing or unreadable files are skipped silently. Order is preserved from
|
|
8
|
+
* the input list. This is the shared read/normalize seam the per-Epic docs
|
|
9
|
+
* digest builds on (Story #4338) so there is a single home for the fs read
|
|
10
|
+
* path.
|
|
14
11
|
*
|
|
15
12
|
* @param {{ files: string[], docsRoot?: string }} args
|
|
16
13
|
* @returns {Promise<Array<{ name: string, path: string, content: string }>>}
|
|
@@ -32,92 +29,3 @@ export async function readDocFiles({ files, docsRoot } = {}) {
|
|
|
32
29
|
});
|
|
33
30
|
return (await Promise.all(reads)).filter(Boolean);
|
|
34
31
|
}
|
|
35
|
-
|
|
36
|
-
async function readDocsFromRoot(docsRoot, settings) {
|
|
37
|
-
const explicit =
|
|
38
|
-
Array.isArray(settings.docsContextFiles) &&
|
|
39
|
-
settings.docsContextFiles.length > 0;
|
|
40
|
-
const usedFallback = !explicit;
|
|
41
|
-
let targetFiles;
|
|
42
|
-
if (explicit) {
|
|
43
|
-
targetFiles = settings.docsContextFiles.map((f) => ({
|
|
44
|
-
name: f,
|
|
45
|
-
full: path.join(docsRoot, f),
|
|
46
|
-
}));
|
|
47
|
-
} else {
|
|
48
|
-
const entries = fs.readdirSync(docsRoot, { withFileTypes: true });
|
|
49
|
-
targetFiles = entries
|
|
50
|
-
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
51
|
-
.map((e) => ({ name: e.name, full: path.join(docsRoot, e.name) }));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const reads = targetFiles.map(async ({ name, full }) => {
|
|
55
|
-
try {
|
|
56
|
-
const stat = await fs.promises.stat(full);
|
|
57
|
-
if (!stat.isFile()) return null;
|
|
58
|
-
const content = await fs.promises.readFile(full, 'utf-8');
|
|
59
|
-
return { name, path: name, content };
|
|
60
|
-
} catch (_e) {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const docs = (await Promise.all(reads)).filter(Boolean);
|
|
66
|
-
return { docs, usedFallback };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Read project documentation, returning raw doc objects so callers can apply
|
|
71
|
-
* the planning-context budget themselves. Each entry is `{ name, path,
|
|
72
|
-
* content }`. The legacy single-string concatenation is no longer the public
|
|
73
|
-
* surface — use {@link buildDocsContext} when an envelope-shaped value is
|
|
74
|
-
* needed.
|
|
75
|
-
*
|
|
76
|
-
* @param {object} settings — Resolved config bag (`project.paths.docsRoot`,
|
|
77
|
-
* `project.docsContextFiles`) or legacy-shim view (same shape).
|
|
78
|
-
* @returns {Promise<{ docs: Array<{name: string, path: string, content: string}>, usedFallback: boolean }>}
|
|
79
|
-
*/
|
|
80
|
-
export async function scrapeProjectDocs(settings) {
|
|
81
|
-
// `settings` is the legacy-shim view (`{ paths, docsContextFiles, ... }`)
|
|
82
|
-
// OR the post-reshape canonical (`{ project: { paths, ... } }`). Under the
|
|
83
|
-
// resolver shim `agentSettings.paths === project.paths`, so re-wrap to the
|
|
84
|
-
// canonical shape `getPaths` consumes.
|
|
85
|
-
const docsRoot = getPaths({
|
|
86
|
-
project: { paths: settings?.project?.paths ?? settings?.paths },
|
|
87
|
-
}).docsRoot;
|
|
88
|
-
if (!docsRoot || !fs.existsSync(docsRoot)) {
|
|
89
|
-
return { docs: [], usedFallback: false };
|
|
90
|
-
}
|
|
91
|
-
Logger.info(`[Epic Planner] Scraping project docs from ${docsRoot}...`);
|
|
92
|
-
try {
|
|
93
|
-
const result = await readDocsFromRoot(docsRoot, settings);
|
|
94
|
-
if (result.usedFallback) {
|
|
95
|
-
Logger.warn(
|
|
96
|
-
'[Epic Planner] ⚠️ project.docsContextFiles is unset — falling back to every top-level *.md under docsRoot. Configure docsContextFiles for production planning.',
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
return result;
|
|
100
|
-
} catch (err) {
|
|
101
|
-
Logger.warn(
|
|
102
|
-
`[Epic Planner] Warning: Failed to read docsRoot: ${err.message}`,
|
|
103
|
-
);
|
|
104
|
-
return { docs: [], usedFallback: false };
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Build the `docsContext` value emitted in `--emit-context` envelopes.
|
|
110
|
-
* Reads the docs and applies the planning-context budget so over-budget
|
|
111
|
-
* payloads downgrade to summary mode automatically. Callers pass
|
|
112
|
-
* `{ fullContext: true }` to honour the `--full-context` CLI opt-in.
|
|
113
|
-
*
|
|
114
|
-
* @param {object} settings — Resolved config bag (same shape as `scrapeProjectDocs`).
|
|
115
|
-
* @param {{ maxBytes?: number, summaryMode?: 'auto'|'always'|'never' }} [planningLimits]
|
|
116
|
-
* @param {{ fullContext?: boolean }} [opts]
|
|
117
|
-
* @returns {Promise<{ mode: 'full'|'summary', items: Array<object>, totalBytes: number, usedFallback: boolean }>}
|
|
118
|
-
*/
|
|
119
|
-
export async function buildDocsContext(settings, planningLimits, opts = {}) {
|
|
120
|
-
const { docs, usedFallback } = await scrapeProjectDocs(settings);
|
|
121
|
-
const budgeted = applyBudget(docs, planningLimits, opts);
|
|
122
|
-
return { ...budgeted, usedFallback };
|
|
123
|
-
}
|
|
@@ -12,8 +12,16 @@
|
|
|
12
12
|
*
|
|
13
13
|
* The heavy lifting of reading + normalizing doc bodies is delegated to
|
|
14
14
|
* `doc-reader.js` (`readDocFiles`), keeping a single home for the fs read path.
|
|
15
|
+
*
|
|
16
|
+
* Story #4433 extends this module with {@link ensureDocsDigest}, a shared
|
|
17
|
+
* generate-and-write export so the planner-context surface
|
|
18
|
+
* (`epic-plan-spec.js` / `authoring-context.js`) can produce a session docs
|
|
19
|
+
* digest without duplicating the mkdir+writeFile plumbing
|
|
20
|
+
* `epic-deliver-prepare.js` already owns for the delivery-children digest.
|
|
15
21
|
*/
|
|
16
22
|
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
17
25
|
import { readDocFiles } from './doc-reader.js';
|
|
18
26
|
|
|
19
27
|
/**
|
|
@@ -132,3 +140,29 @@ export async function buildDocsDigest({ docsContextFiles, docsRoot } = {}) {
|
|
|
132
140
|
const sections = docs.map(renderDocSection).join('\n');
|
|
133
141
|
return `${header}\n${sections}`.replace(/\n+$/, '\n');
|
|
134
142
|
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build the docs digest and write it to `outputPath`, returning `null` (no
|
|
146
|
+
* write) when there is nothing to digest. This is the single shared
|
|
147
|
+
* generate-and-persist export both digest producers call: the per-Epic
|
|
148
|
+
* delivery-children digest (`epic-deliver-prepare.js`) and the planner-
|
|
149
|
+
* context digest (`epic-plan-spec.js` → `authoring-context.js`, Story
|
|
150
|
+
* #4433). Callers own path construction (temp-root layout, epic id, etc.)
|
|
151
|
+
* so both surfaces can keep — or deliberately share — their own convention;
|
|
152
|
+
* this function only owns "build digest, ensure parent dir, write file".
|
|
153
|
+
*
|
|
154
|
+
* @param {{ docsContextFiles?: string[], docsRoot?: string, outputPath: string }} args
|
|
155
|
+
* @returns {Promise<{ digest: string, outputPath: string } | null>}
|
|
156
|
+
*/
|
|
157
|
+
export async function ensureDocsDigest({
|
|
158
|
+
docsContextFiles,
|
|
159
|
+
docsRoot,
|
|
160
|
+
outputPath,
|
|
161
|
+
} = {}) {
|
|
162
|
+
const digest = await buildDocsDigest({ docsContextFiles, docsRoot });
|
|
163
|
+
if (digest == null) return null;
|
|
164
|
+
|
|
165
|
+
await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
|
|
166
|
+
await fs.promises.writeFile(outputPath, digest, 'utf-8');
|
|
167
|
+
return { digest, outputPath };
|
|
168
|
+
}
|
|
@@ -15,18 +15,14 @@ import {
|
|
|
15
15
|
} from '../../../bdd-runner-detect.js';
|
|
16
16
|
import { scanBddScenarios } from '../../../bdd-scenario-scanner.js';
|
|
17
17
|
import { buildCodebaseSnapshot } from '../../../codebase-snapshot.js';
|
|
18
|
-
import { getLimits, PROJECT_ROOT } from '../../../config-resolver.js';
|
|
18
|
+
import { getLimits, getPaths, PROJECT_ROOT } from '../../../config-resolver.js';
|
|
19
19
|
import { hasEpicSection } from '../../../epic-body-sections.js';
|
|
20
20
|
import { scanMemoryFreshness } from '../../../feedback-loop/memory-freshness.js';
|
|
21
21
|
import { fetchPriorFeedback } from '../../../feedback-loop/prior-feedback-fetcher.js';
|
|
22
22
|
import { Logger } from '../../../Logger.js';
|
|
23
|
-
import {
|
|
23
|
+
import { ensureDocsDigest } from '../../docs-digest.js';
|
|
24
24
|
import { applyBudget } from '../../planning-context-budget.js';
|
|
25
25
|
import { collectReferences, hasNewFileCue } from '../../spec-freshness.js';
|
|
26
|
-
import {
|
|
27
|
-
ACCEPTANCE_SPEC_SYSTEM_PROMPT,
|
|
28
|
-
TECH_SPEC_SYSTEM_PROMPT,
|
|
29
|
-
} from './prompts.js';
|
|
30
26
|
import { buildAuthoringGrounding } from './spec-authoring-grounding.js';
|
|
31
27
|
|
|
32
28
|
/**
|
|
@@ -57,16 +53,59 @@ export function resolveMemoryDir({ github } = {}) {
|
|
|
57
53
|
return path.join(os.homedir(), '.claude', 'projects', repo, 'memory');
|
|
58
54
|
}
|
|
59
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Build the digest-first `docsContext` envelope field (Story #4433 — hard
|
|
58
|
+
* cutover of the § 3.1 planning read contract to digest-first with
|
|
59
|
+
* pull-on-demand, mirroring the Story #4324 delivery-children cutover).
|
|
60
|
+
*
|
|
61
|
+
* Ensures a docs digest exists at the **same** per-Epic temp path the
|
|
62
|
+
* `/deliver` story sub-agents already consume
|
|
63
|
+
* (`<tempRoot>/epic-<epicId>/docs-digest.md`) via the shared
|
|
64
|
+
* `ensureDocsDigest` export in `docs-digest.js` — one generator, one file,
|
|
65
|
+
* reused across the planning and delivery surfaces for the same Epic. The
|
|
66
|
+
* envelope carries only the digest path, not embedded doc content; the
|
|
67
|
+
* planner (host LLM or the `epic-plan-spec-author` Skill) reads the digest
|
|
68
|
+
* and pulls a full file/section on demand when it bears on the decision.
|
|
69
|
+
*
|
|
70
|
+
* Returns `null` — a silent no-op — when `project.docsContextFiles` is not
|
|
71
|
+
* configured. There is no scrape-every-markdown-under-docsRoot fallback for
|
|
72
|
+
* planning; that fallback remains a `doc-reader.js` primitive used
|
|
73
|
+
* elsewhere, not part of this envelope.
|
|
74
|
+
*
|
|
75
|
+
* @param {{ epicId: number, settings: object, cwd: string }} args
|
|
76
|
+
* @returns {Promise<{ mode: 'digest', digestPath: string } | null>}
|
|
77
|
+
*/
|
|
78
|
+
async function buildPlanningDocsContext({ epicId, settings, cwd }) {
|
|
79
|
+
const docsContextFiles = Array.isArray(settings?.docsContextFiles)
|
|
80
|
+
? settings.docsContextFiles
|
|
81
|
+
: [];
|
|
82
|
+
if (docsContextFiles.length === 0) return null;
|
|
83
|
+
|
|
84
|
+
const paths = getPaths({ project: { paths: settings?.paths } });
|
|
85
|
+
const docsRoot = path.resolve(cwd, paths.docsRoot);
|
|
86
|
+
const relPath = path.join(paths.tempRoot, `epic-${epicId}`, 'docs-digest.md');
|
|
87
|
+
const absPath = path.resolve(cwd, relPath);
|
|
88
|
+
|
|
89
|
+
const result = await ensureDocsDigest({
|
|
90
|
+
docsContextFiles,
|
|
91
|
+
docsRoot,
|
|
92
|
+
outputPath: absPath,
|
|
93
|
+
});
|
|
94
|
+
if (!result) return null;
|
|
95
|
+
return { mode: 'digest', digestPath: relPath };
|
|
96
|
+
}
|
|
97
|
+
|
|
60
98
|
/**
|
|
61
99
|
* Build the authoring context the host LLM (or the
|
|
62
100
|
* `epic-plan-spec-author` Skill) needs to write the Tech Spec.
|
|
63
101
|
*
|
|
64
|
-
* `docsContext` is
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
102
|
+
* `docsContext` is digest-first (Story #4433): a pointer at the per-Epic
|
|
103
|
+
* docs digest rather than embedded doc content, `null` when
|
|
104
|
+
* `project.docsContextFiles` is unset. The Epic body itself stays bounded by
|
|
105
|
+
* the planning-context budget (Epic #817 Story 9) — over-budget bodies
|
|
106
|
+
* downgrade to a summary representation with headings + bounded excerpts.
|
|
107
|
+
* Pass `{ fullContext: true }` (CLI: `--full-context`) to restore the
|
|
108
|
+
* unbounded full Epic body.
|
|
70
109
|
*/
|
|
71
110
|
export async function buildAuthoringContext(
|
|
72
111
|
epicId,
|
|
@@ -80,10 +119,12 @@ export async function buildAuthoringContext(
|
|
|
80
119
|
}
|
|
81
120
|
|
|
82
121
|
const planningLimits = getLimits(settings).planningContext;
|
|
83
|
-
const { fullContext = false } = opts;
|
|
122
|
+
const { fullContext = false, cwd = PROJECT_ROOT } = opts;
|
|
84
123
|
|
|
85
|
-
const docsContext = await
|
|
86
|
-
|
|
124
|
+
const docsContext = await buildPlanningDocsContext({
|
|
125
|
+
epicId: epic.id,
|
|
126
|
+
settings,
|
|
127
|
+
cwd,
|
|
87
128
|
});
|
|
88
129
|
|
|
89
130
|
const epicBody = applyBudget(
|
|
@@ -216,10 +257,6 @@ export async function buildAuthoringContext(
|
|
|
216
257
|
},
|
|
217
258
|
docsContext,
|
|
218
259
|
codebaseSnapshot,
|
|
219
|
-
systemPrompts: {
|
|
220
|
-
techSpec: TECH_SPEC_SYSTEM_PROMPT,
|
|
221
|
-
acceptanceSpec: ACCEPTANCE_SPEC_SYSTEM_PROMPT,
|
|
222
|
-
},
|
|
223
260
|
bddRunner,
|
|
224
261
|
bddScenarios,
|
|
225
262
|
memoryFreshness,
|
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
} from '../../epic-plan-state-store.js';
|
|
21
21
|
import { resolveReviewRouting } from '../../plan-review-routing.js';
|
|
22
22
|
import { deriveRiskEnvelope } from '../../planning-risk.js';
|
|
23
|
+
import {
|
|
24
|
+
formatMissingSectionMessage,
|
|
25
|
+
validateSpecSections,
|
|
26
|
+
} from '../../spec-section-validator.js';
|
|
23
27
|
import { upsertStructuredComment } from '../../ticketing.js';
|
|
24
28
|
import { planEpic } from './plan-epic.js';
|
|
25
29
|
import { runSpecFreshnessCheck } from './spec-freshness.js';
|
|
@@ -109,6 +113,24 @@ export async function runSpecPhase(
|
|
|
109
113
|
);
|
|
110
114
|
}
|
|
111
115
|
|
|
116
|
+
// Story #4403 (Finding 3): the "## Delivery Slicing" presence gate — the
|
|
117
|
+
// Phase 8-side anchor the Holistic Consolidation pass reconciles against —
|
|
118
|
+
// is validated here, against the in-memory authored content, before any
|
|
119
|
+
// GitHub mutation (lease acquisition, Epic body write, structured
|
|
120
|
+
// comments). This replaces the retired standalone `epic-plan-spec-validate.js`
|
|
121
|
+
// CLI / Phase 7.5 workflow step, whose documented ordering ran the check
|
|
122
|
+
// AFTER the persist path had already written to GitHub and deleted the
|
|
123
|
+
// temp file it read — so the "blocking gate" could never actually block.
|
|
124
|
+
const sectionCheck = validateSpecSections({ body: techSpecContent });
|
|
125
|
+
if (!sectionCheck.ok) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
formatMissingSectionMessage({
|
|
128
|
+
techspecPath: `Epic #${epicId} authored Tech Spec`,
|
|
129
|
+
missing: sectionCheck.missing,
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
112
134
|
// Story #4145 — probe the project's BDD runner. When none is detected
|
|
113
135
|
// (`fallback === true`, e.g. a node:test repo with no tests/features/**),
|
|
114
136
|
// the acceptance disposition is forced to not-applicable inside
|