@opengsd/gsd-core 1.5.0 → 1.6.0-rc.1
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/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-plan-checker.md +34 -0
- package/agents/gsd-planner.md +2 -0
- package/bin/install.js +108 -34
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +677 -2
- package/gsd-core/bin/lib/adr-parser.cjs +24 -17
- package/gsd-core/bin/lib/audit.cjs +2 -2
- package/gsd-core/bin/lib/capability-consent.cjs +763 -0
- package/gsd-core/bin/lib/capability-ledger.cjs +831 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1551 -0
- package/gsd-core/bin/lib/capability-loader.cjs +764 -0
- package/gsd-core/bin/lib/capability-lock.cjs +553 -0
- package/gsd-core/bin/lib/capability-registry.cjs +198 -4
- package/gsd-core/bin/lib/capability-source.cjs +1242 -0
- package/gsd-core/bin/lib/capability-state.cjs +9 -6
- package/gsd-core/bin/lib/capability-trust.cjs +550 -0
- package/gsd-core/bin/lib/capability-validator.cjs +2066 -0
- package/gsd-core/bin/lib/capability-writer.cjs +14 -5
- package/gsd-core/bin/lib/check-command-router.cjs +69 -18
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- package/gsd-core/bin/lib/config-loader.cjs +92 -84
- package/gsd-core/bin/lib/config-schema.cjs +26 -7
- package/gsd-core/bin/lib/config.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +149 -60
- package/gsd-core/bin/lib/gap-checker.cjs +126 -11
- package/gsd-core/bin/lib/init.cjs +91 -22
- package/gsd-core/bin/lib/legacy-cleanup.cjs +96 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +26 -2
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +471 -0
- package/gsd-core/bin/lib/milestone.cjs +41 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +5 -0
- package/gsd-core/bin/lib/phase-lifecycle.cjs +14 -5
- package/gsd-core/bin/lib/phase.cjs +29 -0
- package/gsd-core/bin/lib/project-root.cjs +89 -2
- package/gsd-core/bin/lib/resolution.cjs +26 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +44 -98
- package/gsd-core/bin/lib/runtime-homes.cjs +53 -1
- package/gsd-core/bin/lib/semver-compare.cjs +127 -0
- package/gsd-core/bin/lib/state-document.cjs +4 -2
- package/gsd-core/bin/lib/state.cjs +317 -161
- package/gsd-core/bin/lib/uat-predicate.cjs +7 -47
- package/gsd-core/bin/lib/uat.cjs +39 -26
- package/gsd-core/bin/lib/verify.cjs +29 -13
- package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +4 -1
- package/gsd-core/references/execute-phase-between-wave-reset.md +43 -0
- package/gsd-core/references/execute-phase-wave-guard.md +33 -0
- package/gsd-core/references/planner-antipatterns.md +48 -0
- package/gsd-core/references/planning-config.md +3 -0
- package/gsd-core/references/scout-codebase.md +2 -2
- package/gsd-core/workflows/discuss-phase/templates/context.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -2
- package/gsd-core/workflows/execute-phase.md +4 -6
- package/package.json +3 -3
- package/scripts/gen-capability-matrix.cjs +284 -0
- package/scripts/gen-capability-registry.cjs +96 -1853
- package/scripts/lint-regression-test-names.allowlist.json +1 -0
- package/scripts/lint-resolution-provenance.allowlist.json +1 -0
- package/scripts/lint-resolution-provenance.cjs +192 -0
- package/scripts/lint-test-file-count.allowlist.json +9 -0
- package/scripts/run-tests.cjs +14 -0
- package/scripts/sync-manifest-versions.cjs +77 -5
|
@@ -19,6 +19,9 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
19
19
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
20
20
|
const frontmatter = require("./frontmatter.cjs");
|
|
21
21
|
const { extractFrontmatter } = frontmatter;
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
23
|
+
const markdownSectionizer = require("./markdown-sectionizer.cjs");
|
|
24
|
+
const { stripFencedCode } = markdownSectionizer;
|
|
22
25
|
// ─── Blocking state sets (documented for maintainability) ─────────────────────
|
|
23
26
|
// UAT file frontmatter `status` values that indicate the file is not fully done
|
|
24
27
|
const BLOCKING_UAT_FM_STATUSES = new Set([
|
|
@@ -53,8 +56,8 @@ function stripFalsePositiveContexts(content) {
|
|
|
53
56
|
let stripped = content.replace(/^---\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/, '');
|
|
54
57
|
// Step (b): remove HTML comments anywhere; unterminated comment swallows to EOF
|
|
55
58
|
stripped = stripped.replace(/<!--[\s\S]*?(?:-->|$)/g, '');
|
|
56
|
-
// Step (c): remove fenced code blocks via
|
|
57
|
-
stripped =
|
|
59
|
+
// Step (c): remove fenced code blocks via the canonical seam (ADR-1372 T5)
|
|
60
|
+
stripped = stripFencedCode(stripped).text;
|
|
58
61
|
// Step (d): remove blockquote lines
|
|
59
62
|
stripped = stripped
|
|
60
63
|
.split('\n')
|
|
@@ -62,49 +65,6 @@ function stripFalsePositiveContexts(content) {
|
|
|
62
65
|
.join('\n');
|
|
63
66
|
return stripped;
|
|
64
67
|
}
|
|
65
|
-
/**
|
|
66
|
-
* CommonMark-style fenced-code-block stripper.
|
|
67
|
-
* Tracks the opening delimiter char and length so that a ~~~ line inside a
|
|
68
|
-
* ``` fence is correctly treated as fence content, not a closing delimiter.
|
|
69
|
-
*
|
|
70
|
-
* Opening rule: first delimiter line with char+len sets openFence.
|
|
71
|
-
* Closing rule: delimiter line with SAME char, run length >= openFence.len,
|
|
72
|
-
* and NO trailing non-whitespace text closes the fence.
|
|
73
|
-
* All delimiter and content lines are dropped; non-fence lines are kept.
|
|
74
|
-
* Returns the kept text plus unterminatedFence:true if EOF inside a fence.
|
|
75
|
-
*/
|
|
76
|
-
function _stripFencedBlocks(content) {
|
|
77
|
-
const lines = content.split('\n');
|
|
78
|
-
const kept = [];
|
|
79
|
-
let openFence = null;
|
|
80
|
-
const delimRe = /^(\s*)(`{3,}|~{3,})(.*)$/;
|
|
81
|
-
for (const rawLine of lines) {
|
|
82
|
-
// Tolerate CRLF: strip trailing \r for matching, but we work on split-by-\n lines
|
|
83
|
-
// (the outer caller joined by \n already; we just handle a stray \r in the last char)
|
|
84
|
-
const line = rawLine.replace(/\r$/, '');
|
|
85
|
-
const m = delimRe.exec(line);
|
|
86
|
-
if (m) {
|
|
87
|
-
const char = m[2][0];
|
|
88
|
-
const len = m[2].length;
|
|
89
|
-
const trailing = m[3];
|
|
90
|
-
if (openFence === null) {
|
|
91
|
-
// Opening delimiter — drop this line and record the fence
|
|
92
|
-
openFence = { char, len };
|
|
93
|
-
}
|
|
94
|
-
else if (char === openFence.char && len >= openFence.len && /^\s*$/.test(trailing)) {
|
|
95
|
-
// Closing delimiter (same char, sufficient length, no trailing text) — drop and close
|
|
96
|
-
openFence = null;
|
|
97
|
-
}
|
|
98
|
-
// else: mismatched delimiter inside fence (e.g. ~~~ inside ```) — drop as content
|
|
99
|
-
continue; // delimiter lines are always dropped
|
|
100
|
-
}
|
|
101
|
-
if (openFence === null) {
|
|
102
|
-
kept.push(rawLine);
|
|
103
|
-
}
|
|
104
|
-
// Lines inside fence are dropped
|
|
105
|
-
}
|
|
106
|
-
return { text: kept.join('\n'), unterminatedFence: openFence !== null };
|
|
107
|
-
}
|
|
108
68
|
/**
|
|
109
69
|
* Analyse raw markdown for structural anomalies (unterminated fence / comment).
|
|
110
70
|
* Exported for unit-testability and used by evaluateUatPassed for per-file malformed detection.
|
|
@@ -129,8 +89,8 @@ function analyzeMarkdown(raw) {
|
|
|
129
89
|
}
|
|
130
90
|
i = close + 3;
|
|
131
91
|
}
|
|
132
|
-
// Fence state machine gives the accurate unterminated-fence signal.
|
|
133
|
-
const { unterminatedFence } =
|
|
92
|
+
// Fence state machine gives the accurate unterminated-fence signal (seam, ADR-1372 T5).
|
|
93
|
+
const { unterminatedFence } = stripFencedCode(raw);
|
|
134
94
|
return { unterminatedFence, unterminatedComment };
|
|
135
95
|
}
|
|
136
96
|
// ─── parseUatResultItems ──────────────────────────────────────────────────────
|
package/gsd-core/bin/lib/uat.cjs
CHANGED
|
@@ -18,6 +18,9 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
18
18
|
const io = require("./io.cjs");
|
|
19
19
|
const { output, error } = io;
|
|
20
20
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
|
+
const markdownSectionizer = require("./markdown-sectionizer.cjs");
|
|
22
|
+
const { collectSection, tokenizeHeadings } = markdownSectionizer;
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
24
|
const roadmapParser = require("./roadmap-parser.cjs");
|
|
22
25
|
const { getMilestonePhaseFilter } = roadmapParser;
|
|
23
26
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -128,11 +131,16 @@ function cmdRenderCheckpoint(cwd, options = {}, raw) {
|
|
|
128
131
|
}
|
|
129
132
|
// ─── parseCurrentTest ─────────────────────────────────────────────────────────
|
|
130
133
|
function parseCurrentTest(content) {
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
// Use the seam to locate the ## Current Test section (ADR-1372 T5).
|
|
135
|
+
// HTML-comment stripping within the section body is UAT-specific, so we keep
|
|
136
|
+
// the comment removal caller-side after extracting the body.
|
|
137
|
+
const currentTestSection = collectSection(content, (h) => /^current\s+test$/i.test(h.text) && h.level === 2, { levelBounded: true });
|
|
138
|
+
if (!currentTestSection) {
|
|
133
139
|
error('UAT file is missing a Current Test section');
|
|
134
140
|
}
|
|
135
|
-
|
|
141
|
+
// Remove any leading HTML comment block (UAT-specific document structure)
|
|
142
|
+
const rawBody = currentTestSection.body.replace(/^<!--[\s\S]*?-->\s*\n?/, '');
|
|
143
|
+
const section = rawBody.trimEnd();
|
|
136
144
|
if (!section.trim()) {
|
|
137
145
|
error('Current Test section is empty');
|
|
138
146
|
}
|
|
@@ -173,36 +181,41 @@ function parseCurrentTest(content) {
|
|
|
173
181
|
};
|
|
174
182
|
}
|
|
175
183
|
function parseFirstPendingTest(content) {
|
|
176
|
-
|
|
177
|
-
|
|
184
|
+
// Use the seam to locate the ## Tests section (ADR-1372 T5).
|
|
185
|
+
const testsSection = collectSection(content, (h) => /^tests$/i.test(h.text) && h.level === 2, { levelBounded: true });
|
|
186
|
+
if (!testsSection) {
|
|
178
187
|
return null;
|
|
179
188
|
}
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const next = headings[i + 1];
|
|
194
|
-
const block = testsSection.slice(current.index, next ? next.index : undefined);
|
|
189
|
+
const sectionBody = testsSection.body;
|
|
190
|
+
// Within the Tests section body, find ### N. Name sub-headings.
|
|
191
|
+
// tokenizeHeadings operates on the section body as a standalone document,
|
|
192
|
+
// filtering to level-3 headings matching the UAT-specific "N. Name" pattern.
|
|
193
|
+
// The UAT-specific item parsing (number extraction, result parsing) stays caller-side.
|
|
194
|
+
const subHeadings = tokenizeHeadings(sectionBody).filter((h) => h.level === 3 && /^\d+\.\s+/.test(h.text));
|
|
195
|
+
for (let i = 0; i < subHeadings.length; i += 1) {
|
|
196
|
+
const current = subHeadings[i];
|
|
197
|
+
const next = subHeadings[i + 1];
|
|
198
|
+
// Slice the block for this sub-test from the section body text
|
|
199
|
+
const block = next
|
|
200
|
+
? sectionBody.slice(current.offset, next.offset)
|
|
201
|
+
: sectionBody.slice(current.offset);
|
|
195
202
|
if (!/^result:\s*\[?pending\]?\s*$/im.test(block)) {
|
|
196
203
|
continue;
|
|
197
204
|
}
|
|
205
|
+
// Extract the UAT-specific number and name from the heading text
|
|
206
|
+
const headingParts = current.text.match(/^(\d+)\.\s+(.+)$/);
|
|
207
|
+
if (!headingParts)
|
|
208
|
+
continue;
|
|
209
|
+
const testNumber = parseInt(headingParts[1], 10);
|
|
210
|
+
const testName = headingParts[2].trim();
|
|
198
211
|
const expected = parseExpectedFromTestBlock(block);
|
|
199
212
|
if (!expected) {
|
|
200
|
-
error(`Pending UAT test ${
|
|
213
|
+
error(`Pending UAT test ${testNumber} is missing an expected field`);
|
|
201
214
|
}
|
|
202
215
|
return {
|
|
203
216
|
complete: false,
|
|
204
|
-
number:
|
|
205
|
-
name: (0, security_cjs_1.sanitizeForDisplay)(
|
|
217
|
+
number: testNumber,
|
|
218
|
+
name: (0, security_cjs_1.sanitizeForDisplay)(testName),
|
|
206
219
|
expected: (0, security_cjs_1.sanitizeForDisplay)(expected),
|
|
207
220
|
};
|
|
208
221
|
}
|
|
@@ -273,10 +286,10 @@ function parseUatItems(content) {
|
|
|
273
286
|
function parseVerificationItems(content, status) {
|
|
274
287
|
const items = [];
|
|
275
288
|
if (status === 'human_needed') {
|
|
276
|
-
//
|
|
277
|
-
const hvSection = content
|
|
289
|
+
// Use the seam to locate the ## Human Verification section (ADR-1372 T5).
|
|
290
|
+
const hvSection = collectSection(content, (h) => /^human\s+verification/i.test(h.text) && h.level === 2, { levelBounded: true });
|
|
278
291
|
if (hvSection) {
|
|
279
|
-
const lines = hvSection
|
|
292
|
+
const lines = hvSection.body.split('\n');
|
|
280
293
|
for (const line of lines) {
|
|
281
294
|
// Match table rows: | N | description | ... |
|
|
282
295
|
const tableMatch = line.match(/\|\s*(\d+)\s*\|\s*([^|]+)/);
|
|
@@ -50,7 +50,7 @@ const { getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone } = ro
|
|
|
50
50
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
51
51
|
const worktreeSafetyMod = require("./worktree-safety.cjs");
|
|
52
52
|
const { inspectWorktreeHealth } = worktreeSafetyMod;
|
|
53
|
-
const { planningDir } = planningWorkspace;
|
|
53
|
+
const { planningDir, planningRoot } = planningWorkspace;
|
|
54
54
|
const { extractFrontmatter, parseMustHavesBlock } = frontmatterMod;
|
|
55
55
|
const { writeStateMd } = stateMod;
|
|
56
56
|
const { MODEL_PROFILES } = modelProfilesMod;
|
|
@@ -1112,12 +1112,18 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1112
1112
|
}, raw);
|
|
1113
1113
|
return;
|
|
1114
1114
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
const
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1115
|
+
// rootBase always resolves to .planning/ (shared root — PROJECT.md, config.json live here)
|
|
1116
|
+
// wsBase resolves to .planning/workstreams/<ws>/ when GSD_WORKSTREAM is set (STATE.md, ROADMAP.md, phases/)
|
|
1117
|
+
const rootBase = planningRoot(cwd);
|
|
1118
|
+
const wsBase = planningDir(cwd);
|
|
1119
|
+
// planBase is kept as an alias for wsBase for all the internal helpers (collectDiskPhases, etc.)
|
|
1120
|
+
// that are already parameterised on the workstream-aware path.
|
|
1121
|
+
const planBase = wsBase;
|
|
1122
|
+
const projectPath = node_path_1.default.join(rootBase, 'PROJECT.md');
|
|
1123
|
+
const roadmapPath = node_path_1.default.join(wsBase, 'ROADMAP.md');
|
|
1124
|
+
const statePath = node_path_1.default.join(wsBase, 'STATE.md');
|
|
1125
|
+
const configPath = node_path_1.default.join(rootBase, 'config.json');
|
|
1126
|
+
const phasesDir = node_path_1.default.join(wsBase, 'phases');
|
|
1121
1127
|
const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
|
|
1122
1128
|
const slash = (name) => (0, runtime_slash_cjs_1.formatGsdSlash)(name, _slashRuntime);
|
|
1123
1129
|
const errors = [];
|
|
@@ -1133,7 +1139,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1133
1139
|
else
|
|
1134
1140
|
info.push(issue);
|
|
1135
1141
|
};
|
|
1136
|
-
if (!node_fs_1.default.existsSync(
|
|
1142
|
+
if (!node_fs_1.default.existsSync(rootBase)) {
|
|
1137
1143
|
addIssue('error', 'E001', '.planning/ directory not found', `Run ${slash('new-project')} to initialize`);
|
|
1138
1144
|
output({ status: 'broken', errors, warnings, info, repairable_count: 0 }, raw);
|
|
1139
1145
|
return;
|
|
@@ -1404,7 +1410,17 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1404
1410
|
continue;
|
|
1405
1411
|
}
|
|
1406
1412
|
if (finding['kind'] === 'stale') {
|
|
1407
|
-
|
|
1413
|
+
// Do not flag the active session's worktree — removing it would be harmful.
|
|
1414
|
+
const worktreePath = finding['path'];
|
|
1415
|
+
const activeCwd = process.cwd();
|
|
1416
|
+
const normalizedWorktree = node_path_1.default.resolve(worktreePath);
|
|
1417
|
+
const normalizedCwd = node_path_1.default.resolve(activeCwd);
|
|
1418
|
+
// Skip if the worktree IS the cwd or is an ancestor of it.
|
|
1419
|
+
const isActiveWorktree = normalizedCwd === normalizedWorktree ||
|
|
1420
|
+
normalizedCwd.startsWith(normalizedWorktree + node_path_1.default.sep);
|
|
1421
|
+
if (isActiveWorktree)
|
|
1422
|
+
continue;
|
|
1423
|
+
addIssue('warning', 'W017', `Stale git worktree: ${worktreePath} (last modified ${finding['ageMinutes']} minutes ago)`, `Run: git worktree remove ${worktreePath} --force`);
|
|
1408
1424
|
}
|
|
1409
1425
|
}
|
|
1410
1426
|
}
|
|
@@ -1440,8 +1456,8 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1440
1456
|
catch {
|
|
1441
1457
|
/* W021 check is advisory — skip on error */
|
|
1442
1458
|
}
|
|
1443
|
-
const milestonesPath = node_path_1.default.join(
|
|
1444
|
-
const milestonesArchiveDir = node_path_1.default.join(
|
|
1459
|
+
const milestonesPath = node_path_1.default.join(rootBase, 'MILESTONES.md');
|
|
1460
|
+
const milestonesArchiveDir = node_path_1.default.join(rootBase, 'milestones');
|
|
1445
1461
|
const missingFromRegistry = [];
|
|
1446
1462
|
try {
|
|
1447
1463
|
if (node_fs_1.default.existsSync(milestonesArchiveDir)) {
|
|
@@ -1470,7 +1486,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1470
1486
|
/* intentionally empty — milestone sync check is advisory */
|
|
1471
1487
|
}
|
|
1472
1488
|
try {
|
|
1473
|
-
const entries = node_fs_1.default.readdirSync(
|
|
1489
|
+
const entries = node_fs_1.default.readdirSync(rootBase, { withFileTypes: true });
|
|
1474
1490
|
for (const entry of entries) {
|
|
1475
1491
|
if (!entry.isFile())
|
|
1476
1492
|
continue;
|
|
@@ -1566,7 +1582,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1566
1582
|
}
|
|
1567
1583
|
const milestone = getMilestoneInfo(cwd);
|
|
1568
1584
|
const projectRef = node_path_1.default
|
|
1569
|
-
.relative(cwd, node_path_1.default.join(
|
|
1585
|
+
.relative(cwd, node_path_1.default.join(rootBase, 'PROJECT.md'))
|
|
1570
1586
|
.split(node_path_1.default.sep)
|
|
1571
1587
|
.join('/');
|
|
1572
1588
|
let stateContent = `# Session State\n\n`;
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"workflow.subagent_timeout",
|
|
56
56
|
"workflow.test_command",
|
|
57
57
|
"workflow.build_command",
|
|
58
|
+
"workflow.mvp_mode",
|
|
58
59
|
"executor.stall_detect_interval_minutes",
|
|
59
60
|
"executor.stall_threshold_minutes",
|
|
60
61
|
"workflow.inline_plan_threshold",
|
|
@@ -91,7 +92,9 @@
|
|
|
91
92
|
"model_policy.high",
|
|
92
93
|
"model_policy.medium",
|
|
93
94
|
"model_policy.low",
|
|
94
|
-
"agent_skills_security.trusted_global_roots"
|
|
95
|
+
"agent_skills_security.trusted_global_roots",
|
|
96
|
+
"capabilities.strict_known_registries",
|
|
97
|
+
"capabilities.auto_update"
|
|
95
98
|
],
|
|
96
99
|
"runtimeStateKeys": [
|
|
97
100
|
"workflow._auto_chain_active"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
7b. **Pre-wave dependency check (waves 2+ only):**
|
|
2
|
+
Before wave N+1, run `gsd-tools.cjs query verify.key-links {phase_dir}/{plan}-PLAN.md` for each upcoming plan.
|
|
3
|
+
If any PRIOR-wave artifact link fails, present:
|
|
4
|
+
- `## Cross-Plan Wiring Gap` with plan/link/from/pattern rows
|
|
5
|
+
- Options: investigate+fix before continue, or continue with cascade risk
|
|
6
|
+
Skip key-links that reference files in the CURRENT (upcoming) wave.
|
|
7
|
+
|
|
8
|
+
7c. **Between-wave manifest reset and worktree base refresh (waves 2+ only — #1369):**
|
|
9
|
+
|
|
10
|
+
**REQUIRED before each wave transition when `USE_WORKTREES != "false"` and `RUNTIME = "claude"`.**
|
|
11
|
+
|
|
12
|
+
Wave N's `WAVE_WORKTREE_MANIFEST` was consumed by `worktree.cleanup-wave` in step 5.5. It must be
|
|
13
|
+
unset so wave N+1's step 3 creates a fresh manifest for the new wave's worktrees. Without this,
|
|
14
|
+
the wave N+1 manifest guard (step 5.5, #3384) blocks on the stale/empty consumed file.
|
|
15
|
+
|
|
16
|
+
After wave N merges and tracking commits, the orchestrator HEAD has advanced past the commit the
|
|
17
|
+
Claude Code harness may have cached as the worktree fork base at session start. New worktrees
|
|
18
|
+
spawned for wave N+1 could fork from the stale pre-wave-N HEAD, causing every executor to trip the
|
|
19
|
+
`worktree_branch_check` FATAL guard immediately (symptom: `HEAD is <old-sha>, expected <new-sha>`).
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Unset per-wave manifest so wave N+1 creates a fresh one (#3384, #1369).
|
|
23
|
+
unset WAVE_WORKTREE_MANIFEST
|
|
24
|
+
|
|
25
|
+
# Between-wave base refresh (#1369): after wave N merges and tracking commits, HEAD has
|
|
26
|
+
# advanced. Re-assert worktree.baseRef:"head" (idempotent — no-op if already set) so the
|
|
27
|
+
# Claude Code harness re-reads the live HEAD on the next Agent(isolation="worktree") call
|
|
28
|
+
# rather than using a cached session-start commit as the fork base.
|
|
29
|
+
if [ "$RUNTIME" = "claude" ] && [ "$USE_WORKTREES" != "false" ]; then
|
|
30
|
+
gsd_run query worktree.set-baseref 2>/dev/null || true
|
|
31
|
+
|
|
32
|
+
# Safety re-check: evaluate degradation AFTER the wave N commits. If HEAD has diverged
|
|
33
|
+
# from origin/HEAD and baseRef is NOT "head", degrade remaining waves to sequential to
|
|
34
|
+
# avoid the base-mismatch FATAL in executor agents.
|
|
35
|
+
_BETWEEN_DEGRADE=$(gsd_run query worktree.base-check --pick shouldDegrade 2>/dev/null || echo "false")
|
|
36
|
+
if [ "$_BETWEEN_DEGRADE" = "true" ]; then
|
|
37
|
+
_DEGRADE_MSG=$(gsd_run query worktree.base-check --pick message 2>/dev/null || true)
|
|
38
|
+
[ -n "$_DEGRADE_MSG" ] && printf '%s\n' "$_DEGRADE_MSG" >&2
|
|
39
|
+
printf 'Degrading to sequential mode for remaining waves: HEAD advanced past worktree fork base after wave %s merge (#1369).\n' "${N}" >&2
|
|
40
|
+
USE_WORKTREES=false
|
|
41
|
+
fi
|
|
42
|
+
fi
|
|
43
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
0.5. **Inter-wave worktree base re-check (wave N+1 guard — #1369):**
|
|
2
|
+
|
|
3
|
+
After Wave N merges and tracking commits advance orchestrator HEAD, Claude Code's
|
|
4
|
+
`isolation="worktree"` still forks new worktrees from `origin/HEAD` (the "fresh" base),
|
|
5
|
+
not the live HEAD. This means Wave N+1 worktrees would be created from the stale
|
|
6
|
+
pre-Wave-N base, causing the `worktree_branch_check` guard inside each executor to halt
|
|
7
|
+
immediately with a base-mismatch fatal.
|
|
8
|
+
|
|
9
|
+
**Run this check at the start of every wave when `USE_WORKTREES != "false"` and
|
|
10
|
+
`RUNTIME = "claude"`**, including Wave 1 (where it mirrors the initialize-step check):
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
if [ "$RUNTIME" = "claude" ] && [ "${USE_WORKTREES:-true}" != "false" ]; then
|
|
14
|
+
_WAVE_DEGRADE=$(gsd_run query worktree.base-check --pick shouldDegrade 2>/dev/null || true)
|
|
15
|
+
if [ "$_WAVE_DEGRADE" = "true" ]; then
|
|
16
|
+
_WAVE_DEGRADE_MSG=$(gsd_run query worktree.base-check --pick message 2>/dev/null || true)
|
|
17
|
+
[ -n "$_WAVE_DEGRADE_MSG" ] && printf '%s\n' "$_WAVE_DEGRADE_MSG" >&2
|
|
18
|
+
echo "⚠ [#1369] Worktree fork base diverged from orchestrator HEAD (wave merges advanced HEAD past origin/HEAD). Auto-degrading to sequential mode for this wave to avoid base-mismatch halts." >&2
|
|
19
|
+
USE_WORKTREES=false
|
|
20
|
+
fi
|
|
21
|
+
fi
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If `shouldDegrade` is `true`, override `USE_WORKTREES=false` for **this wave only** —
|
|
25
|
+
all plans in this wave execute sequentially on the main working tree. Later waves re-run
|
|
26
|
+
this check and may re-enable worktree isolation if `origin/HEAD` is updated (e.g. via
|
|
27
|
+
`git fetch` or `worktree.baseRef:"head"` config).
|
|
28
|
+
|
|
29
|
+
**To avoid this degrade across all waves:** set `worktree.baseRef:"head"` in
|
|
30
|
+
`.claude/settings.local.json` (or run `gsd-tools worktree set-baseref`). This tells
|
|
31
|
+
Claude Code to fork from the live HEAD instead of `origin/HEAD`, so each wave's new
|
|
32
|
+
worktrees always start from the correct post-merge base. See #683 for the base-ref
|
|
33
|
+
configuration detail.
|
|
@@ -174,3 +174,51 @@ If region-scoping is genuinely impractical and the file split is intentional, su
|
|
|
174
174
|
```
|
|
175
175
|
|
|
176
176
|
One marker per pattern. The marker exempts only the exact pattern it names. Prefer region-scoping over suppression.
|
|
177
|
+
|
|
178
|
+
## CLI Output Format Anchor Mismatch (#1478)
|
|
179
|
+
|
|
180
|
+
`pnpm ls vite | grep -E '^vite@7\.'` looks correct but silently fails. `pnpm ls` uses tree characters as line prefixes:
|
|
181
|
+
```
|
|
182
|
+
my-project@1.0.0
|
|
183
|
+
└── vite@7.3.5
|
|
184
|
+
```
|
|
185
|
+
Lines begin with `└──`, not `vite`. The `^` anchor matches line start, which is a tree character — the grep finds nothing.
|
|
186
|
+
|
|
187
|
+
**Bad:** `pnpm ls vite | grep -E '^vite@7\.'`
|
|
188
|
+
**Good:** `pnpm ls vite | grep -E 'vite@7\.'`
|
|
189
|
+
**Good (strict):** `pnpm ls vite | grep -E '(└|├)── vite@7\.'`
|
|
190
|
+
|
|
191
|
+
Same trap: `npm ls`, `yarn list`, `docker ps` column output, `kubectl get` table output.
|
|
192
|
+
|
|
193
|
+
## Fabricated Numeric Baselines (#1478)
|
|
194
|
+
|
|
195
|
+
Never emit `grep '714 tests'` or `grep '52 test files'` unless you ran the count command in this session. Model-recalled counts are stale from training.
|
|
196
|
+
|
|
197
|
+
**Bad:** `npm test 2>&1 | grep '714 passed'`
|
|
198
|
+
**Good:** `npm test 2>&1 | grep -E '[0-9]+ passed'` or just `npm test`
|
|
199
|
+
|
|
200
|
+
## Error-Suppressing Fallbacks in Verify Gates (#1479)
|
|
201
|
+
|
|
202
|
+
`2>/dev/null || echo "0"` in an assignment that feeds a comparison converts any failure into a passing gate that measures nothing.
|
|
203
|
+
|
|
204
|
+
**Bad — both sides default to "0" when files are missing:**
|
|
205
|
+
```bash
|
|
206
|
+
EN_KEYS=$(jq 'keys | length' i18n/en.json 2>/dev/null || echo "0")
|
|
207
|
+
DE_KEYS=$(jq 'keys | length' i18n/de.json 2>/dev/null || echo "0")
|
|
208
|
+
[ "$EN_KEYS" = "$DE_KEYS" ] && echo "ok"
|
|
209
|
+
```
|
|
210
|
+
If files don't exist (wrong path, etc.), both sides become `"0"`. Comparison passes. Gate certifies parity while measuring nothing.
|
|
211
|
+
|
|
212
|
+
**Good — let failure propagate:**
|
|
213
|
+
```bash
|
|
214
|
+
EN_KEYS=$(jq 'keys | length' src/i18n/en.json)
|
|
215
|
+
DE_KEYS=$(jq 'keys | length' src/i18n/de.json)
|
|
216
|
+
[ "$EN_KEYS" = "$DE_KEYS" ] && echo "ok"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Good — explicit guard:**
|
|
220
|
+
```bash
|
|
221
|
+
test -f src/i18n/en.json && test -f src/i18n/de.json || { echo "missing input files"; exit 1; }
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**When `|| echo "default"` is acceptable:** only when absence is semantically the default AND the result is NOT used in a comparison that should detect absence.
|
|
@@ -266,6 +266,9 @@ Set via `workflow.*` namespace in config.json (e.g., `"workflow": { "research":
|
|
|
266
266
|
| `workflow.subagent_timeout` | number | `300000` | Any positive integer (ms) | Timeout for parallel subagent tasks (default: 5 minutes) |
|
|
267
267
|
| `workflow.test_command` | string\|null | `null` | Any shell command | Regression/test gate command run by verify-phase, execute-phase, audit-fix, and post-merge-gate. Unset → GSD auto-detects (Makefile / package.json / Cargo.toml / go.mod / pyproject.toml). |
|
|
268
268
|
| `workflow.build_command` | string\|null | `null` | Any shell command | Build gate command run by the post-merge gate. Unset → build step auto-detected/skipped. |
|
|
269
|
+
| `workflow.mvp_mode` | boolean | `false` | `true`, `false` | Persist the MVP-mode flag in config so every phase defaults to MVP framing without requiring `--mvp` on the CLI. Resolved via the chain: `--mvp` CLI flag → ROADMAP.md `**Mode:** mvp` field → this config value → `false`. When `true`, the planner, executor, verifier, and discovery surfaces (progress, stats, graphify) all treat the phase as an MVP vertical slice (UI → API → DB) of one user-visible capability. |
|
|
270
|
+
| `workflow.plan_chunked` | boolean | `false` | `true`, `false` | Enable chunked planning mode. When `true`, the plan-phase orchestrator splits the single long-lived planner Task into a short outline Task followed by N short per-plan Tasks (~3–5 min each). Each plan is committed individually for crash resilience. Particularly useful on Windows where long-lived Tasks may hang on stdio. Also activated by the `--chunked` flag. |
|
|
271
|
+
| `workflow.code_review_command` | string\|null | `null` | Any shell command | External code-review command integrated into `/gsd:ship`. The diff is piped to the command via stdin; the command must output JSON with a `verdict` field (`"APPROVED"` or `"REVISE"`). Non-zero exit or `"REVISE"` verdict blocks the ship workflow. When unset, the built-in review flow runs. Example: `my-review-tool --review`. |
|
|
269
272
|
| `workflow.inline_plan_threshold` | number | `2` | `0`–`10` | Plans with ≤N tasks execute inline instead of spawning a subagent |
|
|
270
273
|
| `workflow.code_review` | boolean | `true` | `true`, `false` | Enable built-in code review step in the ship workflow |
|
|
271
274
|
| `workflow.code_review_depth` | string | `"standard"` | `"light"`, `"standard"`, `"deep"` | Depth level for code review analysis in the ship workflow |
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Codebase scout — map selection table
|
|
2
2
|
|
|
3
3
|
> Lazy-loaded reference for the `scout_codebase` step in
|
|
4
|
-
> `workflows/discuss-phase.md` (extracted via
|
|
5
|
-
>
|
|
4
|
+
> `workflows/discuss-phase.md` (extracted via the discuss-phase/modes progressive-disclosure split, #717).
|
|
5
|
+
> Read this only when prior `.planning/codebase/*.md` maps exist
|
|
6
6
|
> and the workflow needs to pick which 2–3 to load.
|
|
7
7
|
|
|
8
8
|
## Phase-type → recommended maps
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
> `workflows/discuss-phase.md`, immediately before writing
|
|
5
5
|
> `${phase_dir}/${padded_phase}-CONTEXT.md`. Do not put a reference to this
|
|
6
6
|
> file in `<required_reading>` — that defeats the progressive-disclosure
|
|
7
|
-
> savings
|
|
7
|
+
> savings from the discuss-phase/modes split (#717).
|
|
8
8
|
|
|
9
9
|
## Variable substitutions
|
|
10
10
|
|
|
@@ -19,8 +19,7 @@ You are a thinking partner, not an interviewer. The user is the visionary — yo
|
|
|
19
19
|
|
|
20
20
|
<progressive_disclosure>
|
|
21
21
|
**Per-mode bodies, templates, and the advisor flow are lazy-loaded** to keep
|
|
22
|
-
this file under the
|
|
23
|
-
budget). Read only the files needed for the current invocation:
|
|
22
|
+
this file under the discuss-phase byte budget (32000 bytes, #717; mirrors the agent size-budget convention). Read only the files needed for the current invocation:
|
|
24
23
|
|
|
25
24
|
| When | Read |
|
|
26
25
|
|---|---|
|
|
@@ -491,6 +491,8 @@ increases monotonically across waves. `{status}` is `complete` (success),
|
|
|
491
491
|
|
|
492
492
|
**For each wave:**
|
|
493
493
|
|
|
494
|
+
@~/.claude/gsd-core/references/execute-phase-wave-guard.md
|
|
495
|
+
|
|
494
496
|
1. **Intra-wave files_modified overlap check (BEFORE spawning):**
|
|
495
497
|
|
|
496
498
|
Before spawning any agents for this wave, inspect the `files_modified` list of all plans
|
|
@@ -1038,12 +1040,8 @@ increases monotonically across waves. `{status}` is `complete` (success),
|
|
|
1038
1040
|
**Step 7.3 — `class == "unknown-failure"`:**
|
|
1039
1041
|
Report failed plan and ask Continue/Stop; continuing may cascade into dependent plan failures.
|
|
1040
1042
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
If any PRIOR-wave artifact link fails, present:
|
|
1044
|
-
- `## Cross-Plan Wiring Gap` with plan/link/from/pattern rows
|
|
1045
|
-
- Options: investigate+fix before continue, or continue with cascade risk
|
|
1046
|
-
Skip key-links that reference files in the CURRENT (upcoming) wave.
|
|
1043
|
+
@~/.claude/gsd-core/references/execute-phase-between-wave-reset.md
|
|
1044
|
+
|
|
1047
1045
|
8. **Execute checkpoint plans between waves** — see `<checkpoint_handling>`.
|
|
1048
1046
|
9. **Proceed to next wave.**
|
|
1049
1047
|
</step>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengsd/gsd-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0-rc.1",
|
|
4
4
|
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gsd-core": "bin/install.js",
|
|
@@ -86,13 +86,13 @@
|
|
|
86
86
|
"gen:capability-registry": "node scripts/gen-capability-registry.cjs --write",
|
|
87
87
|
"prepack": "npm run build:lib",
|
|
88
88
|
"prepare": "npm run build:lib",
|
|
89
|
-
"version": "node scripts/sync-manifest-versions.cjs --stage",
|
|
89
|
+
"version": "node scripts/sync-manifest-versions.cjs --stage && node scripts/gen-capability-registry.cjs --write && git add gsd-core/bin/lib/capability-registry.cjs",
|
|
90
90
|
"prepublishOnly": "npm run build:lib && npm run build:hooks",
|
|
91
91
|
"pretest": "npm run build:lib && npm run lint:skill-deps",
|
|
92
92
|
"pretest:coverage": "npm run build:lib && npm run lint:skill-deps",
|
|
93
93
|
"lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
|
|
94
94
|
"lint:fix": "eslint . --fix",
|
|
95
|
-
"lint:ci": "npm run lint && npm run lint:skill-deps && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-windows-test-portability.cjs && node scripts/lint-allow-test-rule-refs.cjs",
|
|
95
|
+
"lint:ci": "npm run lint && npm run lint:skill-deps && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-windows-test-portability.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs",
|
|
96
96
|
"lint:allow-test-rule-refs": "node scripts/lint-allow-test-rule-refs.cjs",
|
|
97
97
|
"lint:windows-test-portability": "node scripts/lint-windows-test-portability.cjs",
|
|
98
98
|
"lint:regression-names": "node scripts/lint-regression-test-names.cjs",
|