@opengsd/gsd-core 1.7.0-rc.1 → 1.7.0-rc.2
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/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +711 -0
- package/agents/gsd-advisor-researcher.md +2 -0
- package/agents/gsd-ai-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +2 -0
- package/agents/gsd-code-fixer.md +2 -0
- package/agents/gsd-code-reviewer.md +2 -0
- package/agents/gsd-codebase-mapper.md +2 -0
- package/agents/gsd-debugger.md +2 -0
- package/agents/gsd-doc-writer.md +2 -0
- package/agents/gsd-eval-auditor.md +2 -0
- package/agents/gsd-executor.md +9 -6
- package/agents/gsd-integration-checker.md +2 -0
- package/agents/gsd-nyquist-auditor.md +2 -0
- package/agents/gsd-phase-researcher.md +2 -0
- package/agents/gsd-plan-checker.md +2 -0
- package/agents/gsd-planner.md +2 -0
- package/agents/gsd-project-researcher.md +2 -0
- package/agents/gsd-research-synthesizer.md +2 -0
- package/agents/gsd-roadmapper.md +2 -0
- package/agents/gsd-security-auditor.md +2 -0
- package/agents/gsd-ui-auditor.md +2 -0
- package/agents/gsd-ui-checker.md +2 -0
- package/agents/gsd-ui-researcher.md +2 -0
- package/agents/gsd-verifier.md +4 -2
- package/bin/install.js +118 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +18 -7
- package/gsd-core/bin/lib/capability-loader.cjs +27 -9
- package/gsd-core/bin/lib/capability-registry.cjs +51 -49
- package/gsd-core/bin/lib/capability-source.cjs +22 -7
- package/gsd-core/bin/lib/capability-validator.cjs +24 -2
- package/gsd-core/bin/lib/commands.cjs +2 -1
- package/gsd-core/bin/lib/frontmatter.cjs +53 -6
- package/gsd-core/bin/lib/handshake-serialized.cjs +70 -0
- package/gsd-core/bin/lib/host-integration-sdk.cjs +53 -0
- package/gsd-core/bin/lib/host-integration.cjs +61 -0
- package/gsd-core/bin/lib/init.cjs +34 -6
- package/gsd-core/bin/lib/milestone.cjs +49 -10
- package/gsd-core/bin/lib/phase-id.cjs +18 -0
- package/gsd-core/bin/lib/phase.cjs +37 -27
- package/gsd-core/bin/lib/phases-command-router.cjs +4 -3
- package/gsd-core/bin/lib/probe-core.cjs +44 -4
- package/gsd-core/bin/lib/roadmap-command-router.cjs +3 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +21 -11
- package/gsd-core/bin/lib/roadmap.cjs +28 -20
- package/gsd-core/bin/lib/state-transition.cjs +15 -0
- package/gsd-core/bin/lib/state.cjs +27 -8
- package/gsd-core/bin/lib/validate.cjs +2 -1
- package/gsd-core/bin/lib/verify.cjs +6 -4
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +12 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +28 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -8
- package/gsd-core/references/agent-skills-bootstrap.md +60 -0
- package/gsd-core/references/model-profiles.md +27 -0
- package/gsd-core/workflows/autonomous.md +22 -24
- package/gsd-core/workflows/complete-milestone.md +6 -10
- package/gsd-core/workflows/execute-phase.md +1 -1
- package/gsd-core/workflows/forensics.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +3 -3
- package/gsd-core/workflows/new-milestone.md +6 -0
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +42 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +102 -0
- package/gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md +23 -0
- package/gsd-core/workflows/plan-phase.md +3 -158
- package/gsd-core/workflows/review.md +7 -2
- package/gsd-core/workflows/settings-advanced.md +10 -10
- package/gsd-core/workflows/verify-work.md +1 -2
- package/package.json +3 -1
- package/scripts/run-tests.cjs +51 -1
- package/scripts/sync-manifest-versions.cjs +66 -14
- package/skills/gsd-review/SKILL.md +6 -0
|
@@ -48,11 +48,40 @@ exports.VALID_STATUS = ['resolved', 'dismissed', 'unresolved'];
|
|
|
48
48
|
function errMessage(e) {
|
|
49
49
|
return e instanceof Error ? e.message : String(e);
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Structural guard for ONE item of the report `items[]` — enforces the `Item` contract (#1907).
|
|
53
|
+
* `analyzeCoverage` always emits fully-populated, category/status-validated Items, so this never
|
|
54
|
+
* rejects legitimate output; it catches an adapter that bypasses the merge and hands back per-item
|
|
55
|
+
* garbage (e.g. `items:[{}]`) inside a well-shaped envelope — which the container-only guard let
|
|
56
|
+
* sail through as green output despite the fail-closed docstring below.
|
|
57
|
+
*/
|
|
58
|
+
function isValidItem(item) {
|
|
59
|
+
if (item == null || typeof item !== 'object')
|
|
60
|
+
return false;
|
|
61
|
+
const i = item;
|
|
62
|
+
if (typeof i.requirement_id !== 'string' || !i.requirement_id.trim())
|
|
63
|
+
return false;
|
|
64
|
+
if (typeof i.category !== 'string' || !i.category.trim())
|
|
65
|
+
return false;
|
|
66
|
+
if (!exports.VALID_STATUS.includes(i.status))
|
|
67
|
+
return false;
|
|
68
|
+
if (typeof i.probe !== 'string')
|
|
69
|
+
return false;
|
|
70
|
+
// The three nullable fields must be a string or null — never some other type.
|
|
71
|
+
if (i.verification !== null && typeof i.verification !== 'string')
|
|
72
|
+
return false;
|
|
73
|
+
if (i.resolution !== null && typeof i.resolution !== 'string')
|
|
74
|
+
return false;
|
|
75
|
+
if (i.reason !== null && typeof i.reason !== 'string')
|
|
76
|
+
return false;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
51
79
|
/**
|
|
52
80
|
* Structural guard for the report an adapter's `analyze` returns. The scaffold types `analyze`
|
|
53
81
|
* loosely (it runs over JSON-parsed input the adapter `as`-casts), so a future adapter (#644)
|
|
54
82
|
* that forgets to validate inside its closure could hand back a malformed object. Rather than
|
|
55
|
-
* stringify garbage as green output, `runProbeCli` checks the report shape
|
|
83
|
+
* stringify garbage as green output, `runProbeCli` checks the report shape — container AND every
|
|
84
|
+
* item — and fails closed.
|
|
56
85
|
*/
|
|
57
86
|
function isValidReport(report) {
|
|
58
87
|
if (report == null || typeof report !== 'object')
|
|
@@ -60,6 +89,8 @@ function isValidReport(report) {
|
|
|
60
89
|
const r = report;
|
|
61
90
|
if (!Array.isArray(r.items))
|
|
62
91
|
return false;
|
|
92
|
+
if (!r.items.every(isValidItem))
|
|
93
|
+
return false;
|
|
63
94
|
const c = r.coverage;
|
|
64
95
|
if (c == null || typeof c !== 'object')
|
|
65
96
|
return false;
|
|
@@ -373,13 +404,22 @@ function truthStatement(truth) {
|
|
|
373
404
|
/**
|
|
374
405
|
* Extract a truth's verification tier, or `null` when it carries none (a plain string, or an object
|
|
375
406
|
* with no/garbled marker). Failing toward `null` is the Postel-safe direction: an unrecognized marker
|
|
376
|
-
* grades NORMALLY (never a spurious abstention — the over-abstention guard, AC#3)
|
|
377
|
-
*
|
|
407
|
+
* grades NORMALLY (never a spurious abstention — the over-abstention guard, AC#3).
|
|
408
|
+
*
|
|
409
|
+
* The marker is NOT only machine-emitted: `must_haves` markers can be authored BY HAND (#1820's
|
|
410
|
+
* spec-optional predicate rail), and the frontmatter continuation-KV parser preserves stray
|
|
411
|
+
* surrounding whitespace/quotes on a hand-authored value. So we normalize before comparison
|
|
412
|
+
* (Postel: be liberal in what you accept) — `'backstop '`, `' backstop'`, `'"backstop"'` all
|
|
413
|
+
* recognize as the tier. Without this, a hand-authored non-inferable `backstop` truth with a stray
|
|
414
|
+
* trailing space silently grades green instead of abstaining — the exact #1154 false-pass (#1905).
|
|
415
|
+
* An unrecoverably-corrupted marker (e.g. an embedded quote) stays unrecognized → null → graded
|
|
416
|
+
* normally (AC#3): we cannot know its intent, and abstaining on it would be a spurious abstention.
|
|
378
417
|
*/
|
|
379
418
|
function truthVerification(truth) {
|
|
380
419
|
if (truth == null || typeof truth !== 'object')
|
|
381
420
|
return null;
|
|
382
|
-
const
|
|
421
|
+
const raw = truth.verification;
|
|
422
|
+
const v = typeof raw === 'string' ? raw.trim().replace(/^["']|["']$/g, '').trim() : raw;
|
|
383
423
|
return v === 'explicit' || v === 'backstop' ? v : null;
|
|
384
424
|
}
|
|
385
425
|
/**
|
|
@@ -46,9 +46,10 @@ function checkW021(content) {
|
|
|
46
46
|
// Capture the major integer.
|
|
47
47
|
const MILESTONE_RE = /^#{1,3}\s+(?:\[[^\]]+\]\s+|Roadmap\s+|[✅🚧]\s*)?v(\d+)\.\d+(?:\s|:|\s*—)/iu;
|
|
48
48
|
// Migrated phase heading: ### Phase M-NN: Name (M-NN or unpadded M-N form)
|
|
49
|
-
|
|
49
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
50
|
+
const PHASE_RE = /^#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+(\d+)-(\d+)(?:-\d+)*(?:\s*\([^)\n]*\))?\s*:/i;
|
|
50
51
|
// Unprefixed legacy phase heading: ### Phase N: Name (no hyphen sub-index)
|
|
51
|
-
const UNPREFIXED_PHASE_RE = /^#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+(\d+[A-Za-z]?(?:\.\d+)*)\s*:/i;
|
|
52
|
+
const UNPREFIXED_PHASE_RE = /^#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+(\d+[A-Za-z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:/i;
|
|
52
53
|
let currentMilestoneMajor = null;
|
|
53
54
|
const lines = content.split('\n');
|
|
54
55
|
for (const line of lines) {
|
|
@@ -22,7 +22,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
22
22
|
const node_path_1 = __importDefault(require("node:path"));
|
|
23
23
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
24
|
const phaseIdModule = require("./phase-id.cjs");
|
|
25
|
-
const { escapeRegex, phaseMarkdownRegexSource, phaseMarkdownRegexSourceExact, stripProjectCodePrefix, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, } = phaseIdModule;
|
|
25
|
+
const { escapeRegex, phaseMarkdownRegexSource, phaseMarkdownRegexSourceExact, stripProjectCodePrefix, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, } = phaseIdModule;
|
|
26
26
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
27
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
28
28
|
const { planningDir } = planningWorkspace;
|
|
@@ -82,7 +82,8 @@ function extractCurrentMilestone(content, cwd) {
|
|
|
82
82
|
const preambleCutoff = firstMilestoneMatch ? firstMilestoneMatch.index : detailsOpenIdx;
|
|
83
83
|
const preamble = content.slice(0, preambleCutoff)
|
|
84
84
|
.replace(/<details>[\s\S]*?<\/details>/gi, '')
|
|
85
|
-
|
|
85
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
86
|
+
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*(?:\s*\([^)\n]*\))?\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
|
|
86
87
|
.replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
|
|
87
88
|
return preamble + content.slice(detailsOpenIdx, detailsEnd);
|
|
88
89
|
}
|
|
@@ -151,7 +152,8 @@ function extractCurrentMilestone(content, cwd) {
|
|
|
151
152
|
}
|
|
152
153
|
const preamble = beforeMilestones
|
|
153
154
|
.replace(/<details>[\s\S]*?<\/details>/gi, '')
|
|
154
|
-
|
|
155
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
156
|
+
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*(?:\s*\([^)\n]*\))?\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
|
|
155
157
|
.replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
|
|
156
158
|
return detailsSection
|
|
157
159
|
? preamble + currentSection + '\n' + detailsSection
|
|
@@ -171,16 +173,20 @@ function replaceInCurrentMilestone(content, pattern, replacement) {
|
|
|
171
173
|
return before + after.replace(pattern, replacement);
|
|
172
174
|
}
|
|
173
175
|
function findRoadmapPhaseInContent(content, phaseNum, phaseSource) {
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
+
// #1729: OPTIONAL_PHASE_TAG_SOURCE after the number tolerates a pre-colon ( ) tag.
|
|
177
|
+
const headingPattern = new RegExp(`^(?:\\[[^\\]]+\\]\\s*)?Phase\\s+${phaseSource ?? phaseMarkdownRegexSource(phaseNum)}${OPTIONAL_PHASE_TAG_SOURCE}:\\s*(.+)$`, 'i');
|
|
178
|
+
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
179
|
+
const headingIndex = headings.findIndex((heading) => headingPattern.test(heading.text));
|
|
180
|
+
if (headingIndex === -1)
|
|
181
|
+
return null;
|
|
182
|
+
const heading = headings[headingIndex];
|
|
183
|
+
const headerMatch = heading.text.match(headingPattern);
|
|
176
184
|
if (!headerMatch)
|
|
177
185
|
return null;
|
|
178
186
|
const phaseName = headerMatch[1].trim();
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index : content.length;
|
|
183
|
-
const section = content.slice(headerIndex, sectionEnd).trim();
|
|
187
|
+
const nextHeading = headings.slice(headingIndex + 1).find((candidate) => candidate.level <= heading.level);
|
|
188
|
+
const sectionEnd = nextHeading ? nextHeading.offset : content.length;
|
|
189
|
+
const section = content.slice(heading.offset, sectionEnd).trim();
|
|
184
190
|
const goalMatch = section.match(/\*\*Goal(?:\*\*:|\*?\*?:\*\*)\s*([^\n]+)/i);
|
|
185
191
|
const goal = goalMatch ? goalMatch[1].trim() : null;
|
|
186
192
|
return {
|
|
@@ -209,6 +215,9 @@ function roadmapPhaseLookupSources(phaseNum) {
|
|
|
209
215
|
function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
210
216
|
if (!phaseNum)
|
|
211
217
|
return null;
|
|
218
|
+
const normalizedPhase = stripProjectCodePrefix(phaseNum);
|
|
219
|
+
if (/^999(?:\.|$)/.test(normalizedPhase))
|
|
220
|
+
return null;
|
|
212
221
|
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
213
222
|
if (!node_fs_1.default.existsSync(roadmapPath))
|
|
214
223
|
return null;
|
|
@@ -369,7 +378,8 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
369
378
|
}
|
|
370
379
|
// Use tokenizeHeadings (fence-aware) instead of stripFencedLines + regex.
|
|
371
380
|
// T4 seam migration: phase headings inside fences are excluded automatically.
|
|
372
|
-
|
|
381
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
382
|
+
const phaseHeadingPattern = /^(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]*\))?\s*:/i;
|
|
373
383
|
for (const h of (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(roadmap)) {
|
|
374
384
|
if (h.level < 2 || h.level > 4)
|
|
375
385
|
continue;
|
|
@@ -17,13 +17,14 @@ const ioMod = require("./io.cjs");
|
|
|
17
17
|
const { output, error } = ioMod;
|
|
18
18
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
19
19
|
const phaseIdMod = require("./phase-id.cjs");
|
|
20
|
-
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, phaseMarkdownRegexSourceExact, phaseTokenMatches } = phaseIdMod;
|
|
20
|
+
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, phaseMarkdownRegexSourceExact, phaseTokenMatches, stripProjectCodePrefix, OPTIONAL_PHASE_TAG_SOURCE } = phaseIdMod;
|
|
21
21
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
22
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
23
23
|
const { findPhaseInternal } = phaseLocatorMod;
|
|
24
24
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
25
25
|
const roadmapParserModule = require("./roadmap-parser.cjs");
|
|
26
26
|
const { stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone } = roadmapParserModule;
|
|
27
|
+
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
27
28
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
28
29
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
30
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
@@ -94,12 +95,14 @@ function countPhasePlansAndSummaries(phaseDir) {
|
|
|
94
95
|
* checklist-only match), or null if the phase is not present at all.
|
|
95
96
|
*/
|
|
96
97
|
function searchPhaseInContent(content, escapedPhase, phaseNum) {
|
|
97
|
-
//
|
|
98
|
-
const
|
|
99
|
-
const
|
|
98
|
+
// #1729: OPTIONAL_PHASE_TAG_SOURCE after the number tolerates a pre-colon ( ) tag.
|
|
99
|
+
const headingPattern = new RegExp(`^(?:\\[[^\\]]+\\]\\s*)?Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}:\\s*(.+)$`, 'i');
|
|
100
|
+
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
101
|
+
const headingIndex = headings.findIndex((heading) => headingPattern.test(heading.text));
|
|
102
|
+
const headerMatch = headingIndex === -1 ? null : headings[headingIndex].text.match(headingPattern);
|
|
100
103
|
if (!headerMatch) {
|
|
101
104
|
// Fallback: check if phase exists in summary list but missing detail section
|
|
102
|
-
const checklistPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${escapedPhase}:\\s*([^*]+)\\*\\*`, 'i');
|
|
105
|
+
const checklistPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}:\\s*([^*]+)\\*\\*`, 'i');
|
|
103
106
|
const checklistMatch = content.match(checklistPattern);
|
|
104
107
|
if (checklistMatch) {
|
|
105
108
|
return {
|
|
@@ -113,14 +116,12 @@ function searchPhaseInContent(content, escapedPhase, phaseNum) {
|
|
|
113
116
|
return null;
|
|
114
117
|
}
|
|
115
118
|
const phaseName = headerMatch[1].trim();
|
|
116
|
-
const headerIndex =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const sectionEnd =
|
|
122
|
-
? headerIndex + nextHeaderMatch.index
|
|
123
|
-
: content.length;
|
|
119
|
+
const headerIndex = headings[headingIndex].offset;
|
|
120
|
+
const currentHeading = headings[headingIndex];
|
|
121
|
+
const nextHeading = headings
|
|
122
|
+
.slice(headingIndex + 1)
|
|
123
|
+
.find((candidate) => candidate.level <= currentHeading.level);
|
|
124
|
+
const sectionEnd = nextHeading ? nextHeading.offset : content.length;
|
|
124
125
|
const section = content.slice(headerIndex, sectionEnd).trim();
|
|
125
126
|
// Extract goal if present (supports both **Goal:** and **Goal**: formats)
|
|
126
127
|
const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
|
@@ -159,6 +160,8 @@ function searchPhaseInContent(content, escapedPhase, phaseNum) {
|
|
|
159
160
|
* phase resolution as `roadmap.get-phase` — not a milestone-only subset.
|
|
160
161
|
*/
|
|
161
162
|
function getRoadmapPhaseWithFallback(cwd, phaseNum) {
|
|
163
|
+
if (/^999(?:\.|$)/.test(stripProjectCodePrefix(phaseNum)))
|
|
164
|
+
return null;
|
|
162
165
|
const roadmapPath = planningPaths(cwd).roadmap;
|
|
163
166
|
if (!node_fs_1.default.existsSync(roadmapPath))
|
|
164
167
|
return null;
|
|
@@ -185,6 +188,10 @@ function getRoadmapPhaseWithFallback(cwd, phaseNum) {
|
|
|
185
188
|
}
|
|
186
189
|
// ─── cmdRoadmapGetPhase ───────────────────────────────────────────────────────
|
|
187
190
|
function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
191
|
+
if (/^999(?:\.|$)/.test(stripProjectCodePrefix(phaseNum))) {
|
|
192
|
+
output({ found: false, phase_number: phaseNum }, raw, '');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
188
195
|
const roadmapPath = planningPaths(cwd).roadmap;
|
|
189
196
|
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
190
197
|
output({ found: false, error: 'ROADMAP.md not found' }, raw, '');
|
|
@@ -249,7 +256,8 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
249
256
|
const content = extractCurrentMilestone(rawContent, cwd);
|
|
250
257
|
const phasesDir = planningPaths(cwd).phases;
|
|
251
258
|
// Extract all phase headings: ## Phase N: Name or ### Phase N: Name
|
|
252
|
-
|
|
259
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
260
|
+
const phasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+(\d+[A-Z]?(?:[.-]\d+)*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n]+)/gi;
|
|
253
261
|
const phases = [];
|
|
254
262
|
let match;
|
|
255
263
|
// Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not real
|
|
@@ -325,7 +333,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
325
333
|
// #3537: padding-tolerant fragment — the heading discovered above may use
|
|
326
334
|
// a different padding than the summary-bullet checkbox below it (mixed
|
|
327
335
|
// padding inside one ROADMAP is legal and seen in real projects).
|
|
328
|
-
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseMarkdownRegexSource(phaseNum)}[:\\s]`, 'i');
|
|
336
|
+
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseMarkdownRegexSource(phaseNum)}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
|
|
329
337
|
const checkboxMatch = content.match(checkboxPattern);
|
|
330
338
|
const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
|
|
331
339
|
// If roadmap marks phase complete, trust that over disk file structure.
|
|
@@ -450,14 +458,14 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
450
458
|
// `**Plans**: N plans` — bold word + outer colon (gsd-core/templates/roadmap.md)
|
|
451
459
|
// `**Plans:** N plans` — bold "Plans:" (colon inside bold)
|
|
452
460
|
// `Plans: N plans` — plain text header
|
|
453
|
-
const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}(?=[:\\s])[\\s\\S]*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*|(?:^|\\n)Plans:)\\s*)[^\\n]+`, 'i');
|
|
461
|
+
const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])[\\s\\S]*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*|(?:^|\\n)Plans:)\\s*)[^\\n]+`, 'i');
|
|
454
462
|
const planCountText = isComplete
|
|
455
463
|
? `${summaryCount}/${planCount} plans complete`
|
|
456
464
|
: `${summaryCount}/${planCount} plans executed`;
|
|
457
465
|
roadmapContent = replaceInCurrentMilestone(roadmapContent, planCountPattern, `$1${planCountText}`);
|
|
458
466
|
// If complete: check checkbox
|
|
459
467
|
if (isComplete) {
|
|
460
|
-
const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phasePattern}[:\\s][^\\n]*)`, 'i');
|
|
468
|
+
const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][^\\n]*)`, 'i');
|
|
461
469
|
roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
|
|
462
470
|
}
|
|
463
471
|
// Mark completed plan checkboxes (e.g. "- [ ] 50-01-PLAN.md", "- [ ] 50-01:", or "- [ ] **50-01**")
|
|
@@ -507,8 +515,8 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
507
515
|
//
|
|
508
516
|
// Pattern A: anchor to bare `Plans:` header (preferred).
|
|
509
517
|
// Pattern B: fallback to bold summary when no bare header exists.
|
|
510
|
-
const insertRowsPatternA = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}(?=[:\\s])[\\s\\S]*?(?:^|\\n)(?:Plans:)[^\\n]*)`, 'i');
|
|
511
|
-
const insertRowsPatternB = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}(?=[:\\s])[\\s\\S]*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*)[^\\n]*)`, 'i');
|
|
518
|
+
const insertRowsPatternA = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])[\\s\\S]*?(?:^|\\n)(?:Plans:)[^\\n]*)`, 'i');
|
|
519
|
+
const insertRowsPatternB = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])[\\s\\S]*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*)[^\\n]*)`, 'i');
|
|
512
520
|
const sortedMissing = [...missingPlans].sort();
|
|
513
521
|
const newRows = sortedMissing.map(p => `- [ ] ${p}`).join('\n');
|
|
514
522
|
const inserter = (match) => `${match}\n${newRows}`;
|
|
@@ -645,7 +653,7 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
|
|
|
645
653
|
// #3537: padding-tolerant fragment so the caller's resolved padded id
|
|
646
654
|
// matches un-padded ROADMAP headings.
|
|
647
655
|
const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
|
|
648
|
-
const phaseHeaderPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEscaped}:[^\\n]*)`, 'i');
|
|
656
|
+
const phaseHeaderPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:[^\\n]*)`, 'i');
|
|
649
657
|
const phaseMatch = content.match(phaseHeaderPattern);
|
|
650
658
|
if (!phaseMatch)
|
|
651
659
|
return;
|
|
@@ -19,6 +19,7 @@ exports.STATE_MD_SECTIONS = exports.FIELD_CLASSIFICATION = void 0;
|
|
|
19
19
|
exports.getFieldClassification = getFieldClassification;
|
|
20
20
|
exports.applyStatePreservation = applyStatePreservation;
|
|
21
21
|
exports.transitionCore = transitionCore;
|
|
22
|
+
exports.sliceCurrentPositionSection = sliceCurrentPositionSection;
|
|
22
23
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
23
24
|
const frontmatter = require("./frontmatter.cjs");
|
|
24
25
|
const state_document_cjs_1 = require("./state-document.cjs");
|
|
@@ -319,6 +320,20 @@ function locateCurrentPosition(body) {
|
|
|
319
320
|
}
|
|
320
321
|
return { start, end };
|
|
321
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Return the body text of the `## Current Position` section, or `null` when it
|
|
325
|
+
* is absent. Reuses the fence-aware `locateCurrentPosition` locator (ADR-1372).
|
|
326
|
+
*
|
|
327
|
+
* Exposed so callers that must read a position field (e.g. `cmdStatePrune`,
|
|
328
|
+
* #1776) can scope extraction to the canonical section instead of the whole
|
|
329
|
+
* document — where `stateExtractField`'s pipe-table fallback could otherwise
|
|
330
|
+
* latch onto an unrelated `| Phase | N |` row elsewhere in STATE.md. This
|
|
331
|
+
* scopes the *caller*; the shared extractor is left broad for every other use.
|
|
332
|
+
*/
|
|
333
|
+
function sliceCurrentPositionSection(body) {
|
|
334
|
+
const span = locateCurrentPosition(body);
|
|
335
|
+
return span === null ? null : body.slice(span.start, span.end);
|
|
336
|
+
}
|
|
322
337
|
/**
|
|
323
338
|
* First-time ## Current Position mutation: update Phase / Plan / Status /
|
|
324
339
|
* Last activity lines. Mirrors state.cts:2261-2324 byte-for-behaviour
|
|
@@ -35,7 +35,7 @@ const { extractFrontmatter, reconstructFrontmatter } = frontmatter;
|
|
|
35
35
|
const scanPhasePlans = require("./plan-scan.cjs");
|
|
36
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
37
|
const stateTransitionMod = require("./state-transition.cjs");
|
|
38
|
-
const { transitionCore, applyStatePreservation } = stateTransitionMod;
|
|
38
|
+
const { transitionCore, applyStatePreservation, sliceCurrentPositionSection } = stateTransitionMod;
|
|
39
39
|
const state_document_cjs_1 = require("./state-document.cjs");
|
|
40
40
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
41
41
|
const STATE_PROGRESS_RESYNC_FIELDS = new Set([
|
|
@@ -1293,7 +1293,8 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
1293
1293
|
// truth for total_phases (#549).
|
|
1294
1294
|
let roadmapPhaseCount = 0;
|
|
1295
1295
|
if (roadmapScope !== null) {
|
|
1296
|
-
|
|
1296
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
1297
|
+
const phaseHeadingPattern = /#{2,4}\s*Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]*\))?\s*:/gi;
|
|
1297
1298
|
let m;
|
|
1298
1299
|
while ((m = phaseHeadingPattern.exec(roadmapScope)) !== null) {
|
|
1299
1300
|
// Only count tokens that contain at least one digit — excludes
|
|
@@ -2245,7 +2246,8 @@ function cmdStateSync(cwd, options, raw) {
|
|
|
2245
2246
|
try {
|
|
2246
2247
|
let roadmapPhaseCount = 0;
|
|
2247
2248
|
if (syncRoadmapScope !== null) {
|
|
2248
|
-
|
|
2249
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
2250
|
+
const phaseHeadingPattern = /#{2,4}\s*Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]*\))?\s*:/gi;
|
|
2249
2251
|
let m;
|
|
2250
2252
|
while ((m = phaseHeadingPattern.exec(syncRoadmapScope)) !== null) {
|
|
2251
2253
|
// Only count tokens that contain at least one digit — excludes
|
|
@@ -2321,12 +2323,29 @@ function cmdStatePrune(cwd, options, raw) {
|
|
|
2321
2323
|
}
|
|
2322
2324
|
const keepRecent = parseInt(String(options.keepRecent), 10) || 3;
|
|
2323
2325
|
const dryRun = !!options.dryRun;
|
|
2324
|
-
//
|
|
2325
|
-
// `Current Phase
|
|
2326
|
-
//
|
|
2327
|
-
//
|
|
2326
|
+
// Resolve the current phase via the same canonical chain buildStateFrontmatter
|
|
2327
|
+
// uses (frontmatter `current_phase` → `Current Phase` field → prose `Phase: X
|
|
2328
|
+
// of Y`), so prune engages on template-conformant STATE.md instead of bailing
|
|
2329
|
+
// "Only 0 phases" (#1760).
|
|
2330
|
+
// #1776: scope ONLY the prose `Phase:` term to the canonical `## Current
|
|
2331
|
+
// Position` section. Over the whole body, `stateExtractField`'s pipe-table
|
|
2332
|
+
// fallback matches any `| Phase | N |` row (e.g. a historical verification
|
|
2333
|
+
// table), resolving a stale phase and computing a wrong cutoff. Frontmatter and
|
|
2334
|
+
// the explicit `Current Phase` field are unambiguous, so they stay document-wide;
|
|
2335
|
+
// the shared extractor is not narrowed for any other caller.
|
|
2328
2336
|
const rawState = node_fs_1.default.readFileSync(statePath, 'utf-8');
|
|
2329
|
-
const
|
|
2337
|
+
const fm = extractFrontmatter(rawState);
|
|
2338
|
+
const body = stripFrontmatter(rawState);
|
|
2339
|
+
// Mirror buildStateFrontmatter's fmScalar: only string/number/boolean
|
|
2340
|
+
// frontmatter scalars are usable (an object/array `current_phase` is ignored,
|
|
2341
|
+
// which also avoids a base-to-string on a non-primitive).
|
|
2342
|
+
const fmRawPhase = fm.current_phase;
|
|
2343
|
+
const fmCurrentPhase = typeof fmRawPhase === 'string' ? (fmRawPhase.trim() || null)
|
|
2344
|
+
: typeof fmRawPhase === 'number' || typeof fmRawPhase === 'boolean' ? String(fmRawPhase)
|
|
2345
|
+
: null;
|
|
2346
|
+
const positionSection = sliceCurrentPositionSection(body);
|
|
2347
|
+
const prosePhase = positionSection !== null ? parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(positionSection, 'Phase')).phase : null;
|
|
2348
|
+
const currentPhaseRaw = fmCurrentPhase ?? (0, state_document_cjs_1.stateExtractField)(body, 'Current Phase') ?? prosePhase;
|
|
2330
2349
|
const currentPhase = parseInt(String(currentPhaseRaw), 10) || 0;
|
|
2331
2350
|
const cutoff = currentPhase - keepRecent;
|
|
2332
2351
|
if (cutoff <= 0) {
|
|
@@ -93,7 +93,8 @@ function buildRoadmapPhaseVariants(roadmapContent) {
|
|
|
93
93
|
const roadmapPhaseVariants = new Set();
|
|
94
94
|
// Matches both legacy numeric (Phase 1:), decimal (Phase 2.1:), milestone-prefixed (Phase 2-01:),
|
|
95
95
|
// and bracket-prefixed (### [GSD] Phase 2-01:) headings.
|
|
96
|
-
|
|
96
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
97
|
+
const phasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]*\))?\s*:/gi;
|
|
97
98
|
let m;
|
|
98
99
|
while ((m = phasePattern.exec(roadmapContent)) !== null) {
|
|
99
100
|
roadmapPhases.add(m[1]);
|
|
@@ -40,7 +40,7 @@ const configLoaderMod = require("./config-loader.cjs");
|
|
|
40
40
|
const { loadConfig, CONFIG_DEFAULTS } = configLoaderMod;
|
|
41
41
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
42
|
const phaseIdMod = require("./phase-id.cjs");
|
|
43
|
-
const { normalizePhaseName, phaseTokenMatches, escapeRegex, getMilestoneFromPhaseId } = phaseIdMod;
|
|
43
|
+
const { normalizePhaseName, phaseTokenMatches, escapeRegex, getMilestoneFromPhaseId, OPTIONAL_PHASE_TAG_SOURCE } = phaseIdMod;
|
|
44
44
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
45
45
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
46
46
|
const { findPhaseInternal } = phaseLocatorMod;
|
|
@@ -992,7 +992,8 @@ function checkMilestonePrefixMismatches(roadmapContent, { getMilestoneFromPhaseI
|
|
|
992
992
|
}
|
|
993
993
|
for (const section of sections) {
|
|
994
994
|
const content = roadmapContent.slice(section.start, section.end);
|
|
995
|
-
|
|
995
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
996
|
+
const phaseRx = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]*\))?\s*:/gi;
|
|
996
997
|
let pm;
|
|
997
998
|
while ((pm = phaseRx.exec(content)) !== null) {
|
|
998
999
|
const phaseId = pm[1];
|
|
@@ -1351,7 +1352,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1351
1352
|
stateContent.match(/Current Phase:\s*(\S+)/i);
|
|
1352
1353
|
if (currentPhaseMatch) {
|
|
1353
1354
|
const statePhase = currentPhaseMatch[1].replace(/^0+/, '');
|
|
1354
|
-
const phaseCheckboxRe = new RegExp(`-\\s*\\[x\\].*Phase\\s+0*${escapeRegex(statePhase)}[:\\s]`, 'i');
|
|
1355
|
+
const phaseCheckboxRe = new RegExp(`-\\s*\\[x\\].*Phase\\s+0*${escapeRegex(statePhase)}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
|
|
1355
1356
|
if (phaseCheckboxRe.test(roadmapContentFull)) {
|
|
1356
1357
|
const stateStatus = stateContent.match(/\*\*Status:\*\*\s*(.+)/i);
|
|
1357
1358
|
const statusVal = stateStatus ? stateStatus[1].trim().toLowerCase() : '';
|
|
@@ -1509,7 +1510,8 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1509
1510
|
if (isMarkedComplete) {
|
|
1510
1511
|
const roadmapRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1511
1512
|
const scopedContent = extractCurrentMilestone(roadmapRaw, cwd);
|
|
1512
|
-
|
|
1513
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
1514
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n]+)/gi;
|
|
1513
1515
|
const unstarted = [];
|
|
1514
1516
|
let pm;
|
|
1515
1517
|
// Non-hoisted: load-order matters (circular dep guard)
|
|
@@ -28,7 +28,7 @@ function isCompletedInventory(status) {
|
|
|
28
28
|
return /\bmilestone\s+complete\b/.test(s) || /\barchived\b/.test(s);
|
|
29
29
|
}
|
|
30
30
|
function buildWorkstreamInventory(inputs) {
|
|
31
|
-
const { name, projectDir, workstreamDir, phaseDirNames, activeWorkstreamName, phaseFilesCounts, roadmapPhaseCount, stateProjection, filesExist, } = inputs;
|
|
31
|
+
const { name, projectDir, workstreamDir, phaseDirNames, activeWorkstreamName, phaseFilesCounts, roadmapPhaseCount, stateProjection, filesExist, milestoneShipped, } = inputs;
|
|
32
32
|
// Index counts by directory for O(1) lookup during sort/iteration
|
|
33
33
|
const countsMap = new Map();
|
|
34
34
|
for (const entry of phaseFilesCounts) {
|
|
@@ -56,6 +56,14 @@ function buildWorkstreamInventory(inputs) {
|
|
|
56
56
|
summary_count: counts.summaryCount,
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
+
// #1913: derive status from authoritative shipped signals rather than trusting
|
|
60
|
+
// the mutable STATE.md `Status` field. When a shipped signal is present, the
|
|
61
|
+
// workstream is "milestone complete" regardless of a stale field value.
|
|
62
|
+
const fieldStatus = stateProjection.status;
|
|
63
|
+
const useDerived = milestoneShipped;
|
|
64
|
+
const status = useDerived ? 'milestone complete' : fieldStatus;
|
|
65
|
+
const status_source = useDerived ? 'derived' : 'field';
|
|
66
|
+
const status_conflict = useDerived && !isCompletedInventory(fieldStatus);
|
|
59
67
|
return {
|
|
60
68
|
name,
|
|
61
69
|
path: toPosixPath(node_path_1.default.relative(projectDir, workstreamDir)),
|
|
@@ -65,7 +73,9 @@ function buildWorkstreamInventory(inputs) {
|
|
|
65
73
|
state: filesExist.state,
|
|
66
74
|
requirements: filesExist.requirements,
|
|
67
75
|
},
|
|
68
|
-
status
|
|
76
|
+
status,
|
|
77
|
+
status_source,
|
|
78
|
+
status_conflict,
|
|
69
79
|
current_phase: stateProjection.current_phase,
|
|
70
80
|
last_activity: stateProjection.last_activity,
|
|
71
81
|
phases,
|
|
@@ -63,6 +63,33 @@ function readStateProjection(statePath) {
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* #1913: detect an authoritative shipped signal for a workstream so the
|
|
68
|
+
* inventory status is never trusted from the mutable STATE.md `Status` field
|
|
69
|
+
* alone. Returns true when EITHER an archived milestone snapshot is present
|
|
70
|
+
* under `<planningBase>/milestones/` OR the workstream ROADMAP carries a
|
|
71
|
+
* SHIPPED marker — both are hard to desync, unlike the hand-maintained field.
|
|
72
|
+
*/
|
|
73
|
+
function workstreamMilestoneShipped(roadmapPath, planningBase) {
|
|
74
|
+
try {
|
|
75
|
+
const milestonesDir = node_path_1.default.join(planningBase, 'milestones');
|
|
76
|
+
for (const entry of node_fs_1.default.readdirSync(milestonesDir, { withFileTypes: true })) {
|
|
77
|
+
if (entry.isFile() && /-ROADMAP\.md$/i.test(entry.name))
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* no milestones archive dir */
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
if (/SHIPPED/i.test(node_fs_1.default.readFileSync(roadmapPath, 'utf-8')))
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* no roadmap */
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
66
93
|
function sortWorkstreamInventories(inventories, activeWorkstreamName) {
|
|
67
94
|
return [...inventories].sort((a, b) => {
|
|
68
95
|
const aActive = a.name === activeWorkstreamName ? 1 : 0;
|
|
@@ -99,6 +126,7 @@ function inspectWorkstream(cwd, name, options = {}) {
|
|
|
99
126
|
state: node_fs_1.default.existsSync(p.state),
|
|
100
127
|
requirements: node_fs_1.default.existsSync(p.requirements),
|
|
101
128
|
},
|
|
129
|
+
milestoneShipped: workstreamMilestoneShipped(p.roadmap, p.planning),
|
|
102
130
|
});
|
|
103
131
|
}
|
|
104
132
|
function listWorkstreamInventories(cwd) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"runtimeTierDefaults": {
|
|
10
10
|
"claude": {
|
|
11
11
|
"opus": { "model": "claude-opus-4-8" },
|
|
12
|
-
"sonnet": { "model": "claude-sonnet-
|
|
12
|
+
"sonnet": { "model": "claude-sonnet-5" },
|
|
13
13
|
"haiku": { "model": "claude-haiku-4-5" }
|
|
14
14
|
},
|
|
15
15
|
"codex": {
|
|
@@ -29,17 +29,17 @@
|
|
|
29
29
|
},
|
|
30
30
|
"opencode": {
|
|
31
31
|
"opus": { "model": "anthropic/claude-opus-4-8" },
|
|
32
|
-
"sonnet": { "model": "anthropic/claude-sonnet-
|
|
32
|
+
"sonnet": { "model": "anthropic/claude-sonnet-5" },
|
|
33
33
|
"haiku": { "model": "anthropic/claude-haiku-4-5" }
|
|
34
34
|
},
|
|
35
35
|
"copilot": {
|
|
36
36
|
"opus": { "model": "claude-opus-4-8" },
|
|
37
|
-
"sonnet": { "model": "claude-sonnet-
|
|
37
|
+
"sonnet": { "model": "claude-sonnet-5" },
|
|
38
38
|
"haiku": { "model": "claude-haiku-4-5" }
|
|
39
39
|
},
|
|
40
40
|
"hermes": {
|
|
41
41
|
"opus": { "model": "anthropic/claude-opus-4-8" },
|
|
42
|
-
"sonnet": { "model": "anthropic/claude-sonnet-
|
|
42
|
+
"sonnet": { "model": "anthropic/claude-sonnet-5" },
|
|
43
43
|
"haiku": { "model": "anthropic/claude-haiku-4-5" }
|
|
44
44
|
},
|
|
45
45
|
"kilo": {
|
|
@@ -91,13 +91,13 @@
|
|
|
91
91
|
"providerPresets": {
|
|
92
92
|
"anthropic": {
|
|
93
93
|
"opus": { "low": { "model": "claude-opus-4-5" }, "medium": { "model": "claude-opus-4-8" }, "high": { "model": "claude-opus-4-8" } },
|
|
94
|
-
"sonnet": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-sonnet-
|
|
95
|
-
"haiku": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-haiku-4-5" }, "high": { "model": "claude-sonnet-
|
|
94
|
+
"sonnet": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-sonnet-5" }, "high": { "model": "claude-opus-4-8" } },
|
|
95
|
+
"haiku": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-haiku-4-5" }, "high": { "model": "claude-sonnet-5" } }
|
|
96
96
|
},
|
|
97
97
|
"anthropic-fable": {
|
|
98
98
|
"opus": { "low": { "model": "claude-opus-4-5" }, "medium": { "model": "claude-opus-4-8" }, "high": { "model": "claude-fable-5" } },
|
|
99
|
-
"sonnet": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-sonnet-
|
|
100
|
-
"haiku": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-haiku-4-5" }, "high": { "model": "claude-sonnet-
|
|
99
|
+
"sonnet": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-sonnet-5" }, "high": { "model": "claude-fable-5" } },
|
|
100
|
+
"haiku": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-haiku-4-5" }, "high": { "model": "claude-sonnet-5" } }
|
|
101
101
|
},
|
|
102
102
|
"openai": {
|
|
103
103
|
"opus": { "low": { "model": "gpt-5.4", "reasoning_effort": "medium" }, "medium": { "model": "gpt-5.5", "reasoning_effort": "high" }, "high": { "model": "gpt-5.5", "reasoning_effort": "xhigh" } },
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Agent Skills Self-Load (Bootstrap)
|
|
2
|
+
|
|
3
|
+
> **Shared contract.** Every `agent_skills` consumer agent self-loads its configured
|
|
4
|
+
> skills in its mandatory init step, so a project's `.planning/config.json`
|
|
5
|
+
> `agent_skills.<agent-type>` mapping reaches the agent that actually does the work —
|
|
6
|
+
> even when the orchestrator did not run bash init (e.g. a runtime whose `Skill()`
|
|
7
|
+
> delegation does not reliably execute the delegated workflow's bash, such as Cursor;
|
|
8
|
+
> see open-gsd/gsd-core#1600 / #1601). This is the durable counterpart to the
|
|
9
|
+
> orchestrator-side injection documented under
|
|
10
|
+
> [Agent Skills Injection](../../docs/CONFIGURATION.md#agent-skills-injection).
|
|
11
|
+
|
|
12
|
+
## When to run
|
|
13
|
+
|
|
14
|
+
In your mandatory init step — right after `mandatory-initial-read.md` / the
|
|
15
|
+
`Project skills` discovery, before any other work.
|
|
16
|
+
|
|
17
|
+
## Steps
|
|
18
|
+
|
|
19
|
+
1. **Dedup guard (MANDATORY).** Look at your own prompt. If it already contains an
|
|
20
|
+
`<agent_skills>` block, the orchestrator already injected one — **skip self-load
|
|
21
|
+
entirely.** Loading a second copy wastes context on runtimes where orchestrator-side
|
|
22
|
+
injection also runs (e.g. Claude Code). The guard is what keeps the two seams from
|
|
23
|
+
doubling the block.
|
|
24
|
+
|
|
25
|
+
2. **Query your configured skills.** Use **your own agent type** — the `name:` value in
|
|
26
|
+
your frontmatter (e.g. an agent whose frontmatter says `name: gsd-executor` queries
|
|
27
|
+
`gsd-executor`). The query is read-only and idempotent — it exits 0 with an empty
|
|
28
|
+
block when nothing is configured for your type:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
_AGENT_SKILLS=$(gsd_run query agent-skills <YOUR-FRONTMATTER-NAME> 2>/dev/null || true)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The runtime `gsd_run` resolver is the standard one from
|
|
35
|
+
`_runtime-launcher.snippet.sh`; your own init already defines it.
|
|
36
|
+
|
|
37
|
+
3. **Read every listed skill.** The block emits entries as `@<path>/SKILL.md`
|
|
38
|
+
includes — `Read` each one before starting work. If the block is empty, there is
|
|
39
|
+
nothing to do (zero overhead).
|
|
40
|
+
|
|
41
|
+
## What self-load does and does not cover
|
|
42
|
+
|
|
43
|
+
| Skill form | Self-loads? | Notes |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| Project-relative path (`skills/my-skill`) | ✅ everywhere | `Read` the `@`-include |
|
|
46
|
+
| Global personal (`global:<name>`) | ✅ everywhere | resolves to the runtime global skills dir, then `Read` |
|
|
47
|
+
| Plugin-provided (`global:<plugin>:<skill>`) | Claude only | emitted as a Skill-tool directive on Claude; **skipped with a warning on all other runtimes** — the plugin/Skill-tool model has no equivalent elsewhere (#1601, #1258). Not closeable on Cursor. |
|
|
48
|
+
|
|
49
|
+
## Notes
|
|
50
|
+
|
|
51
|
+
- **Idempotent and read-only.** `query agent-skills` never mutates state; calling it
|
|
52
|
+
twice (once by the orchestrator, once by the agent) is harmless because the dedup
|
|
53
|
+
guard suppresses the second load.
|
|
54
|
+
- **No new config keys.** This reuses the existing `agent_skills` map and the existing
|
|
55
|
+
`buildAgentSkillsBlock` / `cmdAgentSkills` machinery (`src/init.cts`). The 22 consumer
|
|
56
|
+
agent types are mirrored in `tests/agent-skills.test.cjs` (`CONSUMER_AGENTS`) and
|
|
57
|
+
guarded against drift by `tests/agent-skills-bootstrap.test.cjs`.
|
|
58
|
+
- **Checkers / read-only agents.** Bash is universal across consumer agents, so
|
|
59
|
+
self-load works for plan-checkers, verifiers, and auditors too; the bootstrap assumes
|
|
60
|
+
no tool an agent lacks.
|