cleargate 0.8.2 → 0.10.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/CHANGELOG.md +190 -0
- package/README.md +11 -0
- package/dist/MANIFEST.json +259 -28
- package/dist/{chunk-OM4FAEA7.js → chunk-Q3BTSXCK.js} +69 -3
- package/dist/chunk-Q3BTSXCK.js.map +1 -0
- package/dist/cli.cjs +2621 -548
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2548 -560
- package/dist/cli.js.map +1 -1
- package/dist/lib/ledger.cjs +120 -0
- package/dist/lib/ledger.cjs.map +1 -0
- package/dist/lib/ledger.d.cts +64 -0
- package/dist/lib/ledger.d.ts +64 -0
- package/dist/lib/ledger.js +96 -0
- package/dist/lib/ledger.js.map +1 -0
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +10 -8
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +108 -0
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +49 -3
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +6 -1
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +29 -2
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +50 -1
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +31 -9
- package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +148 -0
- package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +6 -0
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +314 -96
- package/dist/templates/cleargate-planning/.claude/settings.json +4 -0
- package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +473 -0
- package/dist/templates/cleargate-planning/.cleargate/config.example.yml +19 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +542 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +102 -428
- package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +31 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +71 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +24 -2
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +387 -27
- package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +54 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +378 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +888 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +355 -13
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +125 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +24 -1
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +32 -1
- package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +37 -3
- package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +50 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
- package/dist/templates/cleargate-planning/.cleargate/templates/proposal.md +17 -4
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +55 -3
- package/dist/templates/cleargate-planning/CLAUDE.md +28 -10
- package/dist/templates/cleargate-planning/MANIFEST.json +259 -28
- package/dist/{whoami-CX7CXJD5.js → whoami-W4U6DPVG.js} +17 -17
- package/dist/whoami-W4U6DPVG.js.map +1 -0
- package/package.json +13 -2
- package/templates/cleargate-planning/.claude/agents/architect.md +10 -8
- package/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +108 -0
- package/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +49 -3
- package/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +6 -1
- package/templates/cleargate-planning/.claude/agents/developer.md +29 -2
- package/templates/cleargate-planning/.claude/agents/qa.md +50 -1
- package/templates/cleargate-planning/.claude/agents/reporter.md +31 -9
- package/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +148 -0
- package/templates/cleargate-planning/.claude/hooks/session-start.sh +6 -0
- package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +314 -96
- package/templates/cleargate-planning/.claude/settings.json +4 -0
- package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +473 -0
- package/templates/cleargate-planning/.cleargate/config.example.yml +19 -0
- package/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +542 -0
- package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +102 -428
- package/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +31 -0
- package/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +71 -0
- package/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +24 -2
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +387 -27
- package/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
- package/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +54 -0
- package/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +378 -0
- package/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +888 -0
- package/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
- package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +355 -13
- package/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
- package/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
- package/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +125 -0
- package/templates/cleargate-planning/.cleargate/templates/Bug.md +24 -1
- package/templates/cleargate-planning/.cleargate/templates/CR.md +32 -1
- package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
- package/templates/cleargate-planning/.cleargate/templates/epic.md +37 -3
- package/templates/cleargate-planning/.cleargate/templates/hotfix.md +50 -0
- package/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
- package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
- package/templates/cleargate-planning/.cleargate/templates/story.md +55 -3
- package/templates/cleargate-planning/CLAUDE.md +28 -10
- package/templates/cleargate-planning/MANIFEST.json +259 -28
- package/dist/chunk-OM4FAEA7.js.map +0 -1
- package/dist/whoami-CX7CXJD5.js.map +0 -1
- package/templates/cleargate-planning/.cleargate/templates/proposal.md +0 -61
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// schema_version: 1 — frozen for SPRINT-19 M8
|
|
3
|
+
//
|
|
4
|
+
// prep_qa_context.mjs — QA context-bundle assembler
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// node prep_qa_context.mjs <story-id> <worktree-path> [--output <path>] [--dev-handoff-json <path>]
|
|
8
|
+
//
|
|
9
|
+
// Positional args:
|
|
10
|
+
// <story-id> — e.g. STORY-025-04 or CR-024
|
|
11
|
+
// <worktree-path> — absolute path to the developer's worktree
|
|
12
|
+
//
|
|
13
|
+
// Options:
|
|
14
|
+
// --output <path> — override default output path
|
|
15
|
+
// --dev-handoff-json <path>— path to JSON file containing dev's STATUS=done handoff
|
|
16
|
+
//
|
|
17
|
+
// Env overrides:
|
|
18
|
+
// CLEARGATE_SPRINT_DIR — override the sprint-runs/<sprint-id> directory
|
|
19
|
+
// CLEARGATE_PENDING_SYNC_DIR — override .cleargate/delivery/pending-sync/
|
|
20
|
+
//
|
|
21
|
+
// Output file: <sprint-dir>/.qa-context-<story-id>.md (default)
|
|
22
|
+
//
|
|
23
|
+
// Bundle target: ≤20KB. If exceeded, warns to stderr but still writes.
|
|
24
|
+
//
|
|
25
|
+
// Schema freeze contract (M8 reads exactly v1):
|
|
26
|
+
// ```json
|
|
27
|
+
// {
|
|
28
|
+
// "schema_version": 1,
|
|
29
|
+
// "story_id": "STORY-NNN-NN",
|
|
30
|
+
// "sprint_id": "SPRINT-NN",
|
|
31
|
+
// "generated_at": "ISO-8601",
|
|
32
|
+
// "worktree": {
|
|
33
|
+
// "path": "string",
|
|
34
|
+
// "branch": "string",
|
|
35
|
+
// "head_sha": "string",
|
|
36
|
+
// "dev_status_block_present": "boolean"
|
|
37
|
+
// },
|
|
38
|
+
// "spec_sources": {
|
|
39
|
+
// "story_path": "string|null",
|
|
40
|
+
// "plan_path": "string|null",
|
|
41
|
+
// "spec_pointers": [{"section": "string", "path": "string", "line_range": "string"}]
|
|
42
|
+
// },
|
|
43
|
+
// "baseline": {
|
|
44
|
+
// "main_head_sha": "string",
|
|
45
|
+
// "baseline_unavailable": "boolean",
|
|
46
|
+
// "failures": [{"file": "string", "count": "integer"}]
|
|
47
|
+
// },
|
|
48
|
+
// "adjacent": {
|
|
49
|
+
// "touched_files": ["string"],
|
|
50
|
+
// "adjacent_test_files": ["string"],
|
|
51
|
+
// "mirror_pairs": [{"touched": "string", "mirror": "string"}]
|
|
52
|
+
// },
|
|
53
|
+
// "cross_story_map": [
|
|
54
|
+
// {"story_id": "string", "branch": "string", "head_sha": "string", "shared_files": ["string"]}
|
|
55
|
+
// ],
|
|
56
|
+
// "flashcard_slice": {
|
|
57
|
+
// "tags_inferred": ["string"],
|
|
58
|
+
// "entries": ["string"]
|
|
59
|
+
// },
|
|
60
|
+
// "lane": {
|
|
61
|
+
// "value": "fast|standard|runtime",
|
|
62
|
+
// "source": "state.json|default|not-yet-runtime-aware"
|
|
63
|
+
// },
|
|
64
|
+
// "dev_handoff": {
|
|
65
|
+
// "format": "structured|legacy|absent",
|
|
66
|
+
// "status": "done|blocked|null",
|
|
67
|
+
// "commit": "string|null",
|
|
68
|
+
// "typecheck": "pass|fail|null",
|
|
69
|
+
// "tests": "string|null",
|
|
70
|
+
// "files_changed": ["string"],
|
|
71
|
+
// "notes": "string|null",
|
|
72
|
+
// "r_coverage": [{"r_id": "string", "covered": "boolean", "deferred": "boolean", "clarified": "boolean"}],
|
|
73
|
+
// "plan_deviations": [{"what": "string", "why": "string", "orchestrator_confirmed": "boolean"}],
|
|
74
|
+
// "adjacent_files": ["string"],
|
|
75
|
+
// "flashcards_flagged": ["string"]
|
|
76
|
+
// }
|
|
77
|
+
// }
|
|
78
|
+
// ```
|
|
79
|
+
//
|
|
80
|
+
// Exit codes:
|
|
81
|
+
// 0 — success (including all R4 soft-degrades)
|
|
82
|
+
// 1 — hard error (worktree path doesn't exist, git command fails unrecoverably)
|
|
83
|
+
// 2 — usage error (missing required args)
|
|
84
|
+
//
|
|
85
|
+
// M8 hand-off: M8 (CR-024 S2) consumes this schema. M2 freezes; M8 wires.
|
|
86
|
+
|
|
87
|
+
import fs from 'node:fs';
|
|
88
|
+
import path from 'node:path';
|
|
89
|
+
import { execSync } from 'node:child_process';
|
|
90
|
+
import { fileURLToPath } from 'node:url';
|
|
91
|
+
import { VALID_STATES, TERMINAL_STATES } from './constants.mjs';
|
|
92
|
+
|
|
93
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
94
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
95
|
+
|
|
96
|
+
// ── Env overrides ─────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
function resolveSprintDir(sprintId) {
|
|
99
|
+
return process.env.CLEARGATE_SPRINT_DIR
|
|
100
|
+
? path.resolve(process.env.CLEARGATE_SPRINT_DIR)
|
|
101
|
+
: path.join(REPO_ROOT, '.cleargate', 'sprint-runs', sprintId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolvePendingSyncDir() {
|
|
105
|
+
return process.env.CLEARGATE_PENDING_SYNC_DIR
|
|
106
|
+
? path.resolve(process.env.CLEARGATE_PENDING_SYNC_DIR)
|
|
107
|
+
: path.join(REPO_ROOT, '.cleargate', 'delivery', 'pending-sync');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveArchiveDir() {
|
|
111
|
+
return path.join(REPO_ROOT, '.cleargate', 'delivery', 'archive');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Atomic write ──────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
function atomicWrite(filePath, content) {
|
|
117
|
+
const tmpFile = `${filePath}.tmp.${process.pid}`;
|
|
118
|
+
fs.writeFileSync(tmpFile, content, 'utf8');
|
|
119
|
+
fs.renameSync(tmpFile, filePath);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Frontmatter parser ────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
function parseFrontmatter(content) {
|
|
125
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
|
126
|
+
if (!fmMatch) return {};
|
|
127
|
+
const fields = {};
|
|
128
|
+
for (const line of fmMatch[1].split('\n')) {
|
|
129
|
+
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*"?([^"]*)"?\s*$/);
|
|
130
|
+
if (m) {
|
|
131
|
+
const val = m[2].trim();
|
|
132
|
+
fields[m[1]] = val === 'null' || val === '' ? null : val;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return fields;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Git helpers ───────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
function gitExec(args, cwd) {
|
|
141
|
+
try {
|
|
142
|
+
return execSync(`git -C "${cwd}" ${args}`, {
|
|
143
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
144
|
+
encoding: 'utf8',
|
|
145
|
+
}).trim();
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Sprint ID derivation ──────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
function deriveSprintId() {
|
|
154
|
+
if (process.env.CLEARGATE_SPRINT_DIR) {
|
|
155
|
+
return path.basename(path.resolve(process.env.CLEARGATE_SPRINT_DIR));
|
|
156
|
+
}
|
|
157
|
+
const activePath = path.join(REPO_ROOT, '.cleargate', 'sprint-runs', '.active');
|
|
158
|
+
if (fs.existsSync(activePath)) {
|
|
159
|
+
return fs.readFileSync(activePath, 'utf8').trim();
|
|
160
|
+
}
|
|
161
|
+
return 'SPRINT-UNKNOWN';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Section 1: Worktree + Commit ──────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
function buildWorktreeSection(storyId, worktreePath, devHandoffJson) {
|
|
167
|
+
const branch = (() => {
|
|
168
|
+
const raw = gitExec('symbolic-ref HEAD', worktreePath);
|
|
169
|
+
if (!raw) return 'unknown';
|
|
170
|
+
return raw.replace(/^refs\/heads\//, '');
|
|
171
|
+
})();
|
|
172
|
+
|
|
173
|
+
const headSha = gitExec('rev-parse HEAD', worktreePath) || 'unknown';
|
|
174
|
+
|
|
175
|
+
// Check if STATUS=done block is present
|
|
176
|
+
let devStatusBlockPresent = false;
|
|
177
|
+
if (devHandoffJson) {
|
|
178
|
+
try {
|
|
179
|
+
const handoff = JSON.parse(fs.readFileSync(devHandoffJson, 'utf8'));
|
|
180
|
+
devStatusBlockPresent = !!(handoff.status && /done|blocked/i.test(handoff.status));
|
|
181
|
+
} catch {
|
|
182
|
+
// fallthrough
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!devStatusBlockPresent && headSha !== 'unknown') {
|
|
186
|
+
const commitMsg = gitExec(`log -1 --format=%B ${headSha}`, worktreePath);
|
|
187
|
+
if (commitMsg && /STATUS:\s*(done|blocked)/i.test(commitMsg)) {
|
|
188
|
+
devStatusBlockPresent = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
path: worktreePath,
|
|
194
|
+
branch,
|
|
195
|
+
head_sha: headSha,
|
|
196
|
+
dev_status_block_present: devStatusBlockPresent,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Section 2: Spec Sources ───────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
function buildSpecSources(storyId, sprintId, sprintDir) {
|
|
203
|
+
// Find story file
|
|
204
|
+
let storyPath = null;
|
|
205
|
+
const pendingSync = resolvePendingSyncDir();
|
|
206
|
+
const archive = resolveArchiveDir();
|
|
207
|
+
for (const dir of [pendingSync, archive]) {
|
|
208
|
+
if (!fs.existsSync(dir)) continue;
|
|
209
|
+
const files = fs.readdirSync(dir).filter(
|
|
210
|
+
f => f.startsWith(storyId + '_') && f.endsWith('.md')
|
|
211
|
+
);
|
|
212
|
+
if (files.length > 0) {
|
|
213
|
+
storyPath = path.join(dir, files[0]);
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
// Also try: file that contains storyId anywhere before the first _ delimiter
|
|
217
|
+
// Pattern: <storyId>_* (already covered above)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Find plan file — look in sprintDir/plans/ for any M*.md containing ## storyId heading
|
|
221
|
+
let planPath = null;
|
|
222
|
+
const specPointers = [];
|
|
223
|
+
const plansDir = path.join(sprintDir, 'plans');
|
|
224
|
+
if (fs.existsSync(plansDir)) {
|
|
225
|
+
const planFiles = fs.readdirSync(plansDir)
|
|
226
|
+
.filter(f => /^M\d+\.md$/.test(f))
|
|
227
|
+
.sort();
|
|
228
|
+
for (const pf of planFiles) {
|
|
229
|
+
const pfPath = path.join(plansDir, pf);
|
|
230
|
+
const content = fs.readFileSync(pfPath, 'utf8');
|
|
231
|
+
const lines = content.split('\n');
|
|
232
|
+
// Find heading with the story ID
|
|
233
|
+
for (let i = 0; i < lines.length; i++) {
|
|
234
|
+
if (/^#{2,4}\s/.test(lines[i]) && lines[i].includes(storyId)) {
|
|
235
|
+
planPath = pfPath;
|
|
236
|
+
// Find end of section
|
|
237
|
+
let endLine = i + 1;
|
|
238
|
+
while (endLine < lines.length && !/^#{2,4}\s/.test(lines[endLine])) {
|
|
239
|
+
endLine++;
|
|
240
|
+
}
|
|
241
|
+
specPointers.push({
|
|
242
|
+
section: 'Per-story blueprint',
|
|
243
|
+
path: pfPath,
|
|
244
|
+
line_range: `${i + 1}-${endLine}`,
|
|
245
|
+
});
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (planPath) break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
story_path: storyPath,
|
|
255
|
+
plan_path: planPath,
|
|
256
|
+
spec_pointers: specPointers,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Section 3: Baseline ───────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
function buildBaseline(sprintDir, worktreePath) {
|
|
263
|
+
const mainHeadSha = gitExec('rev-parse main', worktreePath) || 'unknown';
|
|
264
|
+
|
|
265
|
+
const baselinePath = path.join(sprintDir, '.baseline-failures.json');
|
|
266
|
+
if (!fs.existsSync(baselinePath)) {
|
|
267
|
+
return {
|
|
268
|
+
main_head_sha: mainHeadSha,
|
|
269
|
+
baseline_unavailable: true,
|
|
270
|
+
failures: [],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const parsed = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
|
|
276
|
+
const failures = Array.isArray(parsed) ? parsed : (parsed.failures || []);
|
|
277
|
+
return {
|
|
278
|
+
main_head_sha: mainHeadSha,
|
|
279
|
+
baseline_unavailable: false,
|
|
280
|
+
failures,
|
|
281
|
+
};
|
|
282
|
+
} catch {
|
|
283
|
+
return {
|
|
284
|
+
main_head_sha: mainHeadSha,
|
|
285
|
+
baseline_unavailable: true,
|
|
286
|
+
failures: [],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Section 4: Adjacent Files ─────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
function inferMirrorPairs(touchedFiles) {
|
|
294
|
+
const pairs = [];
|
|
295
|
+
const mirrorMap = [
|
|
296
|
+
['.cleargate/scripts/', 'cleargate-planning/.cleargate/scripts/'],
|
|
297
|
+
['cleargate-planning/.cleargate/scripts/', '.cleargate/scripts/'],
|
|
298
|
+
['.claude/agents/', 'cleargate-planning/.claude/agents/'],
|
|
299
|
+
['cleargate-planning/.claude/agents/', '.claude/agents/'],
|
|
300
|
+
['.cleargate/templates/', 'cleargate-planning/.cleargate/templates/'],
|
|
301
|
+
['cleargate-planning/.cleargate/templates/', '.cleargate/templates/'],
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
for (const touchedFile of touchedFiles) {
|
|
305
|
+
for (const [prefix, mirrorPrefix] of mirrorMap) {
|
|
306
|
+
if (touchedFile.startsWith(prefix)) {
|
|
307
|
+
const relative = touchedFile.slice(prefix.length);
|
|
308
|
+
const mirrorPath = mirrorPrefix + relative;
|
|
309
|
+
// Only emit pair if the mirror actually exists
|
|
310
|
+
const fullMirrorPath = path.join(REPO_ROOT, mirrorPath);
|
|
311
|
+
if (fs.existsSync(fullMirrorPath)) {
|
|
312
|
+
pairs.push({ touched: touchedFile, mirror: mirrorPath });
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return pairs;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function buildAdjacent(worktreePath) {
|
|
323
|
+
// Get touched files from git diff
|
|
324
|
+
let touchedFiles = [];
|
|
325
|
+
const diffOutput = gitExec('diff --name-only main..HEAD', worktreePath);
|
|
326
|
+
if (diffOutput) {
|
|
327
|
+
touchedFiles = diffOutput.split('\n').filter(Boolean);
|
|
328
|
+
} else {
|
|
329
|
+
process.stderr.write(
|
|
330
|
+
'Warning: git diff --name-only main..HEAD failed — touched_files will be empty\n'
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Find adjacent test files
|
|
335
|
+
const adjacentTestFiles = [];
|
|
336
|
+
const seen = new Set();
|
|
337
|
+
for (const f of touchedFiles) {
|
|
338
|
+
const dir = path.dirname(f);
|
|
339
|
+
const fullDir = path.join(REPO_ROOT, dir);
|
|
340
|
+
if (!fs.existsSync(fullDir)) continue;
|
|
341
|
+
try {
|
|
342
|
+
const siblings = fs.readdirSync(fullDir);
|
|
343
|
+
for (const sibling of siblings) {
|
|
344
|
+
if (
|
|
345
|
+
(sibling.endsWith('.test.ts') || sibling.endsWith('.test.sh') ||
|
|
346
|
+
sibling.startsWith('test_') && sibling.endsWith('.sh')) &&
|
|
347
|
+
!seen.has(sibling)
|
|
348
|
+
) {
|
|
349
|
+
const relPath = path.join(dir, sibling);
|
|
350
|
+
if (!seen.has(relPath)) {
|
|
351
|
+
seen.add(relPath);
|
|
352
|
+
adjacentTestFiles.push(relPath);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch {
|
|
357
|
+
// non-fatal
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const mirrorPairs = inferMirrorPairs(touchedFiles);
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
touched_files: touchedFiles,
|
|
365
|
+
adjacent_test_files: adjacentTestFiles,
|
|
366
|
+
mirror_pairs: mirrorPairs,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── Section 5: Cross-Story Map ────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
const IN_FLIGHT_STATES = new Set(
|
|
373
|
+
VALID_STATES.filter(s => !TERMINAL_STATES.includes(s))
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
function buildCrossStoryMap(sprintDir, touchedFiles, currentStoryId) {
|
|
377
|
+
const stateFile = path.join(sprintDir, 'state.json');
|
|
378
|
+
if (!fs.existsSync(stateFile)) return [];
|
|
379
|
+
|
|
380
|
+
let state;
|
|
381
|
+
try {
|
|
382
|
+
state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
383
|
+
} catch {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const stories = state.stories || {};
|
|
388
|
+
const crossMap = [];
|
|
389
|
+
|
|
390
|
+
for (const [sid, entry] of Object.entries(stories)) {
|
|
391
|
+
if (sid === currentStoryId) continue;
|
|
392
|
+
if (!IN_FLIGHT_STATES.has(entry.state)) continue;
|
|
393
|
+
|
|
394
|
+
const branch = `story/${sid}`;
|
|
395
|
+
const headSha = gitExec(`rev-parse ${branch}`, REPO_ROOT) || 'unknown';
|
|
396
|
+
|
|
397
|
+
let storyTouched = [];
|
|
398
|
+
const storyDiff = gitExec(`diff --name-only main..${branch}`, REPO_ROOT);
|
|
399
|
+
if (storyDiff) {
|
|
400
|
+
storyTouched = storyDiff.split('\n').filter(Boolean);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const shared = touchedFiles.filter(f => storyTouched.includes(f));
|
|
404
|
+
if (shared.length > 0) {
|
|
405
|
+
crossMap.push({
|
|
406
|
+
story_id: sid,
|
|
407
|
+
branch,
|
|
408
|
+
head_sha: headSha,
|
|
409
|
+
shared_files: shared,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Cap at 5 stories
|
|
414
|
+
if (crossMap.length >= 5) break;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return crossMap;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── Section 6: Flashcard Slice ────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
const TAG_PREFIX_TABLE = [
|
|
423
|
+
{ prefix: 'cleargate-cli/src/commands/', tags: ['#cli', '#commander'] },
|
|
424
|
+
{ prefix: 'cleargate-cli/src/auth/', tags: ['#auth'] },
|
|
425
|
+
{ prefix: 'cleargate-cli/src/', tags: ['#cli'] },
|
|
426
|
+
{ prefix: 'mcp/src/auth/', tags: ['#auth', '#mcp'] },
|
|
427
|
+
{ prefix: 'mcp/src/db/', tags: ['#schema', '#migrations', '#mcp'] },
|
|
428
|
+
{ prefix: 'mcp/src/', tags: ['#mcp', '#fastify'] },
|
|
429
|
+
{ prefix: '.cleargate/scripts/', tags: ['#scripts', '#test-harness'] },
|
|
430
|
+
{ prefix: '.claude/agents/', tags: ['#agents'] },
|
|
431
|
+
{ prefix: '.cleargate/knowledge/', tags: ['#protocol', '#wiki'] },
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
function inferTagsFromPaths(touchedFiles) {
|
|
435
|
+
const tagSet = new Set();
|
|
436
|
+
for (const f of touchedFiles) {
|
|
437
|
+
let matched = false;
|
|
438
|
+
for (const { prefix, tags } of TAG_PREFIX_TABLE) {
|
|
439
|
+
if (f.startsWith(prefix)) {
|
|
440
|
+
for (const t of tags) tagSet.add(t);
|
|
441
|
+
matched = true;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (!matched) tagSet.add('#general');
|
|
446
|
+
}
|
|
447
|
+
return Array.from(tagSet);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function buildFlashcardSlice(touchedFiles) {
|
|
451
|
+
const tagsInferred = inferTagsFromPaths(touchedFiles);
|
|
452
|
+
|
|
453
|
+
const flashcardFile = path.join(REPO_ROOT, '.cleargate', 'FLASHCARD.md');
|
|
454
|
+
if (!fs.existsSync(flashcardFile)) {
|
|
455
|
+
return {
|
|
456
|
+
tags_inferred: tagsInferred,
|
|
457
|
+
entries: [],
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const content = fs.readFileSync(flashcardFile, 'utf8');
|
|
462
|
+
const lines = content.split('\n').filter(l => /^\d{4}-\d{2}-\d{2}\s+·/.test(l));
|
|
463
|
+
|
|
464
|
+
// Build grep pattern from tags
|
|
465
|
+
const tagPattern = tagsInferred.join('|');
|
|
466
|
+
const re = new RegExp(tagPattern.replace(/#/g, '#'), 'i');
|
|
467
|
+
const matching = lines.filter(l => re.test(l)).slice(0, 20);
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
tags_inferred: tagsInferred,
|
|
471
|
+
entries: matching,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── Section 7: Lane ───────────────────────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
function buildLane(storyId, sprintDir, touchedFiles) {
|
|
478
|
+
const stateFile = path.join(sprintDir, 'state.json');
|
|
479
|
+
if (fs.existsSync(stateFile)) {
|
|
480
|
+
try {
|
|
481
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
482
|
+
const storyEntry = (state.stories || {})[storyId];
|
|
483
|
+
if (storyEntry && storyEntry.lane !== undefined) {
|
|
484
|
+
const laneValue = storyEntry.lane;
|
|
485
|
+
// Heuristic: flag not-yet-runtime-aware if standard + touches CLI commands
|
|
486
|
+
const touchesCli = touchedFiles.some(f => f.startsWith('cleargate-cli/src/commands/'));
|
|
487
|
+
const source =
|
|
488
|
+
laneValue === 'standard' && touchesCli
|
|
489
|
+
? 'not-yet-runtime-aware'
|
|
490
|
+
: 'state.json';
|
|
491
|
+
return { value: laneValue, source };
|
|
492
|
+
}
|
|
493
|
+
} catch {
|
|
494
|
+
// fallthrough
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return { value: 'standard', source: 'default' };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ── Section 8: Dev Handoff ────────────────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
function parseStructuredStatusBlock(text) {
|
|
504
|
+
if (!text) return null;
|
|
505
|
+
|
|
506
|
+
const statusMatch = text.match(/STATUS:\s*(done|blocked)/i);
|
|
507
|
+
const commitMatch = text.match(/COMMIT:\s*([^\n]+)/i);
|
|
508
|
+
const typecheckMatch = text.match(/TYPECHECK:\s*(pass|fail)/i);
|
|
509
|
+
const testsMatch = text.match(/TESTS:\s*([^\n]+)/i);
|
|
510
|
+
const filesChangedMatch = text.match(/FILES_CHANGED:\s*([^\n]+)/i);
|
|
511
|
+
const notesMatch = text.match(/NOTES:\s*([\s\S]*?)(?=\nFLASHCARDS_FLAGGED:|$)/i);
|
|
512
|
+
const flashcardsMatch = text.match(/flashcards_flagged:([\s\S]*?)(?=\n[A-Z_]+:|$)/i);
|
|
513
|
+
|
|
514
|
+
if (!statusMatch) return null;
|
|
515
|
+
|
|
516
|
+
// Structured = has r_coverage:/plan_deviations:/adjacent_files: as field keys (with colon)
|
|
517
|
+
const hasStructuredFields =
|
|
518
|
+
/r_coverage\s*:/.test(text) ||
|
|
519
|
+
/plan_deviations\s*:/.test(text) ||
|
|
520
|
+
/adjacent_files\s*:/.test(text);
|
|
521
|
+
|
|
522
|
+
const filesChanged = filesChangedMatch
|
|
523
|
+
? filesChangedMatch[1].trim().split(/\s*,\s*|\s+/).filter(Boolean)
|
|
524
|
+
: [];
|
|
525
|
+
|
|
526
|
+
const flashcardsFlagged = flashcardsMatch
|
|
527
|
+
? flashcardsMatch[1].trim().split('\n').map(l => l.replace(/^\s*-\s*"?/, '').replace(/"?\s*$/, '')).filter(Boolean)
|
|
528
|
+
: [];
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
format: hasStructuredFields ? 'structured' : 'legacy',
|
|
532
|
+
status: (statusMatch[1] || '').toLowerCase(),
|
|
533
|
+
commit: commitMatch ? commitMatch[1].trim() : null,
|
|
534
|
+
typecheck: typecheckMatch ? typecheckMatch[1].toLowerCase() : null,
|
|
535
|
+
tests: testsMatch ? testsMatch[1].trim() : null,
|
|
536
|
+
files_changed: filesChanged,
|
|
537
|
+
notes: notesMatch ? notesMatch[1].trim() : null,
|
|
538
|
+
r_coverage: [],
|
|
539
|
+
plan_deviations: [],
|
|
540
|
+
adjacent_files: [],
|
|
541
|
+
flashcards_flagged: flashcardsFlagged,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function buildDevHandoff(worktreePath, devHandoffJsonPath) {
|
|
546
|
+
// Try --dev-handoff-json first
|
|
547
|
+
if (devHandoffJsonPath && fs.existsSync(devHandoffJsonPath)) {
|
|
548
|
+
try {
|
|
549
|
+
const raw = JSON.parse(fs.readFileSync(devHandoffJsonPath, 'utf8'));
|
|
550
|
+
// If it already has 'format' field, treat as pre-parsed
|
|
551
|
+
if (raw.format) return raw;
|
|
552
|
+
// Otherwise treat JSON as the full text to parse
|
|
553
|
+
const text = JSON.stringify(raw);
|
|
554
|
+
const parsed = parseStructuredStatusBlock(text);
|
|
555
|
+
if (parsed) return parsed;
|
|
556
|
+
} catch {
|
|
557
|
+
// fallthrough
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Try to extract from commit message
|
|
562
|
+
const headSha = gitExec('rev-parse HEAD', worktreePath);
|
|
563
|
+
if (headSha) {
|
|
564
|
+
const commitMsg = gitExec(`log -1 --format=%B ${headSha}`, worktreePath);
|
|
565
|
+
if (commitMsg) {
|
|
566
|
+
const parsed = parseStructuredStatusBlock(commitMsg);
|
|
567
|
+
if (parsed) return parsed;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return {
|
|
572
|
+
format: 'absent',
|
|
573
|
+
status: null,
|
|
574
|
+
commit: null,
|
|
575
|
+
typecheck: null,
|
|
576
|
+
tests: null,
|
|
577
|
+
files_changed: [],
|
|
578
|
+
notes: null,
|
|
579
|
+
r_coverage: [],
|
|
580
|
+
plan_deviations: [],
|
|
581
|
+
adjacent_files: [],
|
|
582
|
+
flashcards_flagged: [],
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ── Prose section builders ────────────────────────────────────────────────────
|
|
587
|
+
|
|
588
|
+
function proseWorktree(w) {
|
|
589
|
+
return [
|
|
590
|
+
'## Worktree + Commit',
|
|
591
|
+
'',
|
|
592
|
+
`- **Path:** \`${w.path}\``,
|
|
593
|
+
`- **Branch:** \`${w.branch}\``,
|
|
594
|
+
`- **HEAD SHA:** \`${w.head_sha}\``,
|
|
595
|
+
`- **Dev STATUS block present:** ${w.dev_status_block_present}`,
|
|
596
|
+
'',
|
|
597
|
+
].join('\n');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function proseSpecSources(s) {
|
|
601
|
+
const lines = ['## Spec Sources', ''];
|
|
602
|
+
if (s.story_path) {
|
|
603
|
+
lines.push(`- **Story file:** \`${s.story_path}\``);
|
|
604
|
+
} else {
|
|
605
|
+
lines.push('_Story file not found for this story._');
|
|
606
|
+
}
|
|
607
|
+
if (s.plan_path) {
|
|
608
|
+
lines.push(`- **Plan file:** \`${s.plan_path}\``);
|
|
609
|
+
} else {
|
|
610
|
+
lines.push('_Plan file not found (fast-lane or unplanned story)._');
|
|
611
|
+
}
|
|
612
|
+
if (s.spec_pointers.length > 0) {
|
|
613
|
+
lines.push('');
|
|
614
|
+
lines.push('**Spec pointers:**');
|
|
615
|
+
for (const sp of s.spec_pointers) {
|
|
616
|
+
lines.push(`- ${sp.section}: \`${sp.path}\` lines ${sp.line_range}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
lines.push('');
|
|
620
|
+
return lines.join('\n');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function proseBaseline(b) {
|
|
624
|
+
const lines = ['## Baseline', ''];
|
|
625
|
+
lines.push(`- **main HEAD SHA:** \`${b.main_head_sha}\``);
|
|
626
|
+
if (b.baseline_unavailable) {
|
|
627
|
+
lines.push('- **Baseline cache:** unavailable');
|
|
628
|
+
lines.push('');
|
|
629
|
+
lines.push('_Baseline cache stale or absent — recompute via `cleargate gate test` on main._');
|
|
630
|
+
} else {
|
|
631
|
+
lines.push(`- **Baseline cache:** available (${b.failures.length} known failure(s))`);
|
|
632
|
+
for (const f of b.failures) {
|
|
633
|
+
lines.push(` - \`${f.file}\`: ${f.count} failure(s)`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
lines.push('');
|
|
637
|
+
return lines.join('\n');
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function proseAdjacent(a) {
|
|
641
|
+
const lines = ['## Adjacent Files', ''];
|
|
642
|
+
lines.push(`**Touched files (${a.touched_files.length}):**`);
|
|
643
|
+
if (a.touched_files.length === 0) {
|
|
644
|
+
lines.push('_(none — diff vs main empty or unavailable)_');
|
|
645
|
+
} else {
|
|
646
|
+
for (const f of a.touched_files) lines.push(`- \`${f}\``);
|
|
647
|
+
}
|
|
648
|
+
lines.push('');
|
|
649
|
+
lines.push(`**Adjacent test files (${a.adjacent_test_files.length}):**`);
|
|
650
|
+
if (a.adjacent_test_files.length === 0) {
|
|
651
|
+
lines.push('_(none found)_');
|
|
652
|
+
} else {
|
|
653
|
+
for (const f of a.adjacent_test_files) lines.push(`- \`${f}\``);
|
|
654
|
+
}
|
|
655
|
+
lines.push('');
|
|
656
|
+
if (a.mirror_pairs.length > 0) {
|
|
657
|
+
lines.push('**Mirror pairs:**');
|
|
658
|
+
for (const mp of a.mirror_pairs) {
|
|
659
|
+
lines.push(`- \`${mp.touched}\` ↔ \`${mp.mirror}\``);
|
|
660
|
+
}
|
|
661
|
+
lines.push('');
|
|
662
|
+
}
|
|
663
|
+
return lines.join('\n');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function proseCrossStoryMap(csList) {
|
|
667
|
+
const lines = ['## Cross-Story Map', ''];
|
|
668
|
+
if (csList.length === 0) {
|
|
669
|
+
lines.push('_No in-flight stories share files with this story._');
|
|
670
|
+
} else {
|
|
671
|
+
for (const cs of csList) {
|
|
672
|
+
lines.push(`### ${cs.story_id} (\`${cs.branch}\` @ \`${cs.head_sha.slice(0, 8)}\`)`);
|
|
673
|
+
lines.push('**Shared files:**');
|
|
674
|
+
for (const f of cs.shared_files) lines.push(`- \`${f}\``);
|
|
675
|
+
lines.push('');
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
lines.push('');
|
|
679
|
+
return lines.join('\n');
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function proseFlashcardSlice(fc) {
|
|
683
|
+
const lines = ['## Flashcard Slice', ''];
|
|
684
|
+
lines.push(`**Tags inferred from touched paths:** ${fc.tags_inferred.join(', ') || '(none)'}`);
|
|
685
|
+
lines.push('');
|
|
686
|
+
if (fc.entries.length === 0) {
|
|
687
|
+
lines.push('_No matching flashcard entries for inferred tags._');
|
|
688
|
+
} else {
|
|
689
|
+
lines.push(`**Matching entries (${fc.entries.length}, capped at 20):**`);
|
|
690
|
+
lines.push('');
|
|
691
|
+
for (const e of fc.entries) lines.push(e);
|
|
692
|
+
}
|
|
693
|
+
lines.push('');
|
|
694
|
+
return lines.join('\n');
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function proseLane(l) {
|
|
698
|
+
const lines = ['## Lane', ''];
|
|
699
|
+
lines.push(`- **Value:** \`${l.value}\``);
|
|
700
|
+
lines.push(`- **Source:** \`${l.source}\``);
|
|
701
|
+
if (l.source === 'not-yet-runtime-aware') {
|
|
702
|
+
lines.push('');
|
|
703
|
+
lines.push(
|
|
704
|
+
'_Heuristic: story lane is `standard` but touches CLI command files — ' +
|
|
705
|
+
'QA may want to apply `runtime` playbook depth. See CR-024 M8 for lane-playbook dispatch._'
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
lines.push('');
|
|
709
|
+
return lines.join('\n');
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function proseDevHandoff(dh) {
|
|
713
|
+
const lines = ['## Dev Handoff', ''];
|
|
714
|
+
if (dh.format === 'absent') {
|
|
715
|
+
lines.push('_No dev handoff found in commit message or `--dev-handoff-json`. Context limited._');
|
|
716
|
+
} else if (dh.format === 'legacy') {
|
|
717
|
+
lines.push(
|
|
718
|
+
'_SCHEMA_INCOMPLETE — context limited; old-format STATUS=done found, ' +
|
|
719
|
+
'no `r_coverage`/`plan_deviations`/`adjacent_files`._'
|
|
720
|
+
);
|
|
721
|
+
lines.push('');
|
|
722
|
+
lines.push(`- **Status:** ${dh.status}`);
|
|
723
|
+
lines.push(`- **Commit:** ${dh.commit || '(not found)'}`);
|
|
724
|
+
lines.push(`- **Typecheck:** ${dh.typecheck || '(not found)'}`);
|
|
725
|
+
lines.push(`- **Tests:** ${dh.tests || '(not found)'}`);
|
|
726
|
+
if (dh.notes) {
|
|
727
|
+
lines.push('');
|
|
728
|
+
lines.push(`**Notes:** ${dh.notes}`);
|
|
729
|
+
}
|
|
730
|
+
} else {
|
|
731
|
+
// structured
|
|
732
|
+
lines.push(`- **Status:** ${dh.status}`);
|
|
733
|
+
lines.push(`- **Commit:** ${dh.commit || '(not found)'}`);
|
|
734
|
+
lines.push(`- **Typecheck:** ${dh.typecheck || '(not found)'}`);
|
|
735
|
+
lines.push(`- **Tests:** ${dh.tests || '(not found)'}`);
|
|
736
|
+
if (dh.files_changed.length > 0) {
|
|
737
|
+
lines.push('- **Files changed:**');
|
|
738
|
+
for (const f of dh.files_changed) lines.push(` - \`${f}\``);
|
|
739
|
+
}
|
|
740
|
+
if (dh.r_coverage && dh.r_coverage.length > 0) {
|
|
741
|
+
lines.push('');
|
|
742
|
+
lines.push('**R-coverage:**');
|
|
743
|
+
for (const r of dh.r_coverage) {
|
|
744
|
+
lines.push(`- ${r.r_id}: covered=${r.covered} deferred=${r.deferred} clarified=${r.clarified}`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (dh.plan_deviations && dh.plan_deviations.length > 0) {
|
|
748
|
+
lines.push('');
|
|
749
|
+
lines.push('**Plan deviations:**');
|
|
750
|
+
for (const pd of dh.plan_deviations) {
|
|
751
|
+
lines.push(`- **${pd.what}**: ${pd.why} (orchestrator_confirmed=${pd.orchestrator_confirmed})`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (dh.adjacent_files && dh.adjacent_files.length > 0) {
|
|
755
|
+
lines.push('');
|
|
756
|
+
lines.push('**Adjacent files flagged by dev:**');
|
|
757
|
+
for (const af of dh.adjacent_files) lines.push(`- \`${af}\``);
|
|
758
|
+
}
|
|
759
|
+
if (dh.notes) {
|
|
760
|
+
lines.push('');
|
|
761
|
+
lines.push(`**Notes:** ${dh.notes}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
lines.push('');
|
|
765
|
+
return lines.join('\n');
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// ── Main ───────────────────────────────────────────────────────────────────────
|
|
769
|
+
|
|
770
|
+
function main() {
|
|
771
|
+
const rawArgs = process.argv.slice(2);
|
|
772
|
+
|
|
773
|
+
// Parse args
|
|
774
|
+
const positionals = [];
|
|
775
|
+
let outputPath = null;
|
|
776
|
+
let devHandoffJsonPath = null;
|
|
777
|
+
|
|
778
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
779
|
+
if (rawArgs[i] === '--output' && rawArgs[i + 1]) {
|
|
780
|
+
outputPath = rawArgs[++i];
|
|
781
|
+
} else if (rawArgs[i] === '--dev-handoff-json' && rawArgs[i + 1]) {
|
|
782
|
+
devHandoffJsonPath = rawArgs[++i];
|
|
783
|
+
} else if (!rawArgs[i].startsWith('--')) {
|
|
784
|
+
positionals.push(rawArgs[i]);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const storyId = positionals[0];
|
|
789
|
+
const worktreePath = positionals[1];
|
|
790
|
+
|
|
791
|
+
if (!storyId || !worktreePath) {
|
|
792
|
+
process.stderr.write(
|
|
793
|
+
'Usage: node prep_qa_context.mjs <story-id> <worktree-path> [--output <path>] [--dev-handoff-json <path>]\n'
|
|
794
|
+
);
|
|
795
|
+
process.exit(2);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Validate worktree path
|
|
799
|
+
if (!fs.existsSync(worktreePath)) {
|
|
800
|
+
process.stderr.write(`Error: worktree path does not exist: ${worktreePath}\n`);
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const sprintId = deriveSprintId();
|
|
805
|
+
const sprintDir = resolveSprintDir(sprintId);
|
|
806
|
+
|
|
807
|
+
// Default output path
|
|
808
|
+
if (!outputPath) {
|
|
809
|
+
outputPath = path.join(sprintDir, `.qa-context-${storyId}.md`);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Ensure sprint dir exists for output
|
|
813
|
+
if (!fs.existsSync(sprintDir)) {
|
|
814
|
+
try {
|
|
815
|
+
fs.mkdirSync(sprintDir, { recursive: true });
|
|
816
|
+
} catch (err) {
|
|
817
|
+
process.stderr.write(`Warning: could not create sprint dir ${sprintDir}: ${err.message}\n`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Build all sections
|
|
822
|
+
const worktreeData = buildWorktreeSection(storyId, worktreePath, devHandoffJsonPath);
|
|
823
|
+
const specSources = buildSpecSources(storyId, sprintId, sprintDir);
|
|
824
|
+
const baseline = buildBaseline(sprintDir, worktreePath);
|
|
825
|
+
const adjacent = buildAdjacent(worktreePath);
|
|
826
|
+
const crossStoryMap = buildCrossStoryMap(sprintDir, adjacent.touched_files, storyId);
|
|
827
|
+
const flashcardSlice = buildFlashcardSlice(adjacent.touched_files);
|
|
828
|
+
const lane = buildLane(storyId, sprintDir, adjacent.touched_files);
|
|
829
|
+
const devHandoff = buildDevHandoff(worktreePath, devHandoffJsonPath);
|
|
830
|
+
|
|
831
|
+
// Build machine-readable JSON block
|
|
832
|
+
const jsonPayload = {
|
|
833
|
+
schema_version: 1,
|
|
834
|
+
story_id: storyId,
|
|
835
|
+
sprint_id: sprintId,
|
|
836
|
+
generated_at: new Date().toISOString(),
|
|
837
|
+
worktree: worktreeData,
|
|
838
|
+
spec_sources: specSources,
|
|
839
|
+
baseline,
|
|
840
|
+
adjacent,
|
|
841
|
+
cross_story_map: crossStoryMap,
|
|
842
|
+
flashcard_slice: flashcardSlice,
|
|
843
|
+
lane,
|
|
844
|
+
dev_handoff: devHandoff,
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
// Build bundle
|
|
848
|
+
const bundleParts = [
|
|
849
|
+
`# QA Context Pack — ${storyId}\n`,
|
|
850
|
+
`_Generated: ${jsonPayload.generated_at}_\n`,
|
|
851
|
+
`_Sprint: ${sprintId}_\n`,
|
|
852
|
+
'---\n',
|
|
853
|
+
'```json',
|
|
854
|
+
JSON.stringify(jsonPayload, null, 2),
|
|
855
|
+
'```\n',
|
|
856
|
+
'---\n',
|
|
857
|
+
proseWorktree(worktreeData),
|
|
858
|
+
proseSpecSources(specSources),
|
|
859
|
+
proseBaseline(baseline),
|
|
860
|
+
proseAdjacent(adjacent),
|
|
861
|
+
proseCrossStoryMap(crossStoryMap),
|
|
862
|
+
proseFlashcardSlice(flashcardSlice),
|
|
863
|
+
proseLane(lane),
|
|
864
|
+
proseDevHandoff(devHandoff),
|
|
865
|
+
];
|
|
866
|
+
|
|
867
|
+
const bundle = bundleParts.join('\n');
|
|
868
|
+
const bundleBytes = Buffer.byteLength(bundle, 'utf8');
|
|
869
|
+
|
|
870
|
+
// Write bundle (always write, even if oversized — R4)
|
|
871
|
+
const outputDir = path.dirname(outputPath);
|
|
872
|
+
if (!fs.existsSync(outputDir)) {
|
|
873
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
874
|
+
}
|
|
875
|
+
atomicWrite(outputPath, bundle);
|
|
876
|
+
|
|
877
|
+
const kb = (bundleBytes / 1024).toFixed(1);
|
|
878
|
+
if (bundleBytes > 20480) {
|
|
879
|
+
process.stderr.write(
|
|
880
|
+
`Warning: bundle exceeds 20KB target (${kb}KB) — QA context pack is oversized\n`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
process.stdout.write(`QA context pack ready: ${kb}KB at ${outputPath}\n`);
|
|
885
|
+
process.exit(0);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
main();
|