@opengsd/gsd-core 1.7.0-rc.4 → 1.7.0-rc.5
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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +20 -0
- package/agents/gsd-doc-classifier.md +105 -0
- package/agents/gsd-doc-synthesizer.md +61 -0
- package/agents/gsd-ui-checker.md +28 -0
- package/bin/install.js +624 -456
- package/gsd-core/bin/gsd-tools.cjs +40 -1
- package/gsd-core/bin/lib/api-coverage.cjs +466 -0
- package/gsd-core/bin/lib/audit.cjs +6 -3
- package/gsd-core/bin/lib/capability-loader.cjs +11 -9
- package/gsd-core/bin/lib/capability-registry.cjs +325 -64
- package/gsd-core/bin/lib/capability-validator.cjs +1 -1
- package/gsd-core/bin/lib/capability-writer.cjs +10 -1
- package/gsd-core/bin/lib/check-command-router.cjs +242 -3
- package/gsd-core/bin/lib/commands.cjs +7 -5
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +96 -0
- package/gsd-core/bin/lib/core-utils.cjs +4 -1
- package/gsd-core/bin/lib/host-integration-adapters/cline-sdk-binding.cjs +234 -0
- package/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs +145 -0
- package/gsd-core/bin/lib/host-integration.cjs +16 -0
- package/gsd-core/bin/lib/init.cjs +74 -30
- package/gsd-core/bin/lib/install-effort-resolver.cjs +213 -0
- package/gsd-core/bin/lib/install-engine.cjs +151 -11
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +68 -17
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +50 -11
- package/gsd-core/bin/lib/milestone.cjs +3 -3
- package/gsd-core/bin/lib/normalize-test-command.cjs +187 -0
- package/gsd-core/bin/lib/phase-id.cjs +132 -3
- package/gsd-core/bin/lib/phase.cjs +78 -16
- package/gsd-core/bin/lib/planning-workspace.cjs +17 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +5 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +21 -30
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +9 -9
- package/gsd-core/bin/lib/roadmap.cjs +42 -56
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +13 -6
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +17 -10
- package/gsd-core/bin/lib/runtime-homes.cjs +8 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +33 -25
- package/gsd-core/bin/lib/spec-section.cjs +111 -0
- package/gsd-core/bin/lib/state-transition.cjs +1 -1
- package/gsd-core/bin/lib/state.cjs +24 -24
- package/gsd-core/bin/lib/surface.cjs +30 -0
- package/gsd-core/bin/lib/uat.cjs +4 -1
- package/gsd-core/bin/lib/validate.cjs +15 -6
- package/gsd-core/bin/lib/verify.cjs +33 -37
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/bin/shared/model-catalog.json +6 -6
- package/gsd-core/references/api-coverage.md +104 -0
- package/gsd-core/references/model-profiles.md +2 -2
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/specless-probe-fallback.md +172 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/workflows/audit-fix.md +9 -1
- package/gsd-core/workflows/code-review-fix.md +7 -3
- package/gsd-core/workflows/code-review.md +4 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +8 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +42 -0
- package/gsd-core/workflows/execute-phase.md +1 -25
- package/gsd-core/workflows/plan-phase.md +31 -2
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/review.md +59 -13
- package/gsd-core/workflows/settings-advanced.md +5 -5
- package/gsd-core/workflows/settings.md +2 -2
- package/gsd-core/workflows/verify-phase.md +3 -2
- package/gsd-core/workflows/verify-work.md +38 -0
- package/hooks/dist/gsd-cursor-pre-tool.js +76 -0
- package/hooks/dist/gsd-cursor-stop.js +48 -0
- package/hooks/dist/gsd-cursor-subagent-start.js +50 -0
- package/hooks/dist/gsd-cursor-subagent-stop.js +40 -0
- package/hooks/dist/managed-hooks-registry.cjs +4 -0
- package/hooks/gsd-cursor-pre-tool.js +76 -0
- package/hooks/gsd-cursor-stop.js +48 -0
- package/hooks/gsd-cursor-subagent-start.js +50 -0
- package/hooks/gsd-cursor-subagent-stop.js +40 -0
- package/hooks/managed-hooks-registry.cjs +4 -0
- package/package.json +2 -1
- package/scripts/build-hooks.js +5 -1
- package/scripts/lint-phase-id-drift.cjs +150 -0
- package/scripts/run-tests.cjs +21 -1
|
@@ -22,7 +22,10 @@ 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,
|
|
25
|
+
const { escapeRegex, phaseMarkdownRegexSource, stripProjectCodePrefix, OPTIONAL_PHASE_TAG_SOURCE,
|
|
26
|
+
// #2121: roadmapPhaseLookupSources now lives in phase-id.cjs (single owner of
|
|
27
|
+
// the lookup-source ordering); imported here rather than defined locally.
|
|
28
|
+
roadmapPhaseLookupSources, } = phaseIdModule;
|
|
26
29
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
30
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
28
31
|
const { planningDir } = planningWorkspace;
|
|
@@ -33,7 +36,7 @@ const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
|
33
36
|
* Strip shipped milestone content wrapped in <details> blocks.
|
|
34
37
|
*/
|
|
35
38
|
function stripShippedMilestones(content) {
|
|
36
|
-
return
|
|
39
|
+
return (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(content, 'details');
|
|
37
40
|
}
|
|
38
41
|
/**
|
|
39
42
|
* Extract the current milestone section from ROADMAP.md by positive lookup.
|
|
@@ -80,10 +83,9 @@ function extractCurrentMilestone(content, cwd) {
|
|
|
80
83
|
const anyMilestoneOrDetails = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|✅|📋|🚧|🔄)|<details/im;
|
|
81
84
|
const firstMilestoneMatch = content.match(anyMilestoneOrDetails);
|
|
82
85
|
const preambleCutoff = firstMilestoneMatch ? firstMilestoneMatch.index : detailsOpenIdx;
|
|
83
|
-
const preamble = content.slice(0, preambleCutoff)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*(?:\s*\([^)\n]*\))?\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
|
|
86
|
+
const preamble = (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(content.slice(0, preambleCutoff), 'details')
|
|
87
|
+
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
88
|
+
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*(?:\s*\([^)\n]{0,200}\))?\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
|
|
87
89
|
.replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
|
|
88
90
|
return preamble + content.slice(detailsOpenIdx, detailsEnd);
|
|
89
91
|
}
|
|
@@ -150,10 +152,9 @@ function extractCurrentMilestone(content, cwd) {
|
|
|
150
152
|
const detailsStart = detailsMatch.index ?? 0;
|
|
151
153
|
detailsSection = content.slice(detailsStart, computeSectionEnd(detailsMatch[0], detailsStart));
|
|
152
154
|
}
|
|
153
|
-
const preamble = beforeMilestones
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*(?:\s*\([^)\n]*\))?\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
|
|
155
|
+
const preamble = (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(beforeMilestones, 'details')
|
|
156
|
+
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
157
|
+
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*(?:\s*\([^)\n]{0,200}\))?\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
|
|
157
158
|
.replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
|
|
158
159
|
return detailsSection
|
|
159
160
|
? preamble + currentSection + '\n' + detailsSection
|
|
@@ -174,7 +175,7 @@ function replaceInCurrentMilestone(content, pattern, replacement) {
|
|
|
174
175
|
}
|
|
175
176
|
function findRoadmapPhaseInContent(content, phaseNum, phaseSource) {
|
|
176
177
|
// #1729: OPTIONAL_PHASE_TAG_SOURCE after the number tolerates a pre-colon ( ) tag.
|
|
177
|
-
const headingPattern = new RegExp(`^(?:\\[[^\\]]
|
|
178
|
+
const headingPattern = new RegExp(`^(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+${phaseSource ?? phaseMarkdownRegexSource(phaseNum)}${OPTIONAL_PHASE_TAG_SOURCE}:\\s*(.+)$`, 'i');
|
|
178
179
|
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
179
180
|
const headingIndex = headings.findIndex((heading) => headingPattern.test(heading.text));
|
|
180
181
|
if (headingIndex === -1)
|
|
@@ -197,21 +198,6 @@ function findRoadmapPhaseInContent(content, phaseNum, phaseSource) {
|
|
|
197
198
|
section,
|
|
198
199
|
};
|
|
199
200
|
}
|
|
200
|
-
function roadmapPhaseLookupSources(phaseNum) {
|
|
201
|
-
const sources = [];
|
|
202
|
-
const exactSource = phaseMarkdownRegexSourceExact(phaseNum);
|
|
203
|
-
if (exactSource)
|
|
204
|
-
sources.push(exactSource);
|
|
205
|
-
const numericSource = phaseMarkdownRegexSource(phaseNum);
|
|
206
|
-
// Source order matters: the bare numeric source is tried before the
|
|
207
|
-
// prefix-tolerant form so that a canonical bare heading ("Phase 117:") is
|
|
208
|
-
// preferred over a drifted prefixed heading ("Phase MANIFOLD-117:") when
|
|
209
|
-
// both exist in the same ROADMAP. The prefix-tolerant form is the fallback
|
|
210
|
-
// that handles the drifted-only case.
|
|
211
|
-
sources.push(numericSource);
|
|
212
|
-
sources.push(`${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${numericSource}`);
|
|
213
|
-
return [...new Set(sources)];
|
|
214
|
-
}
|
|
215
201
|
function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
216
202
|
if (!phaseNum)
|
|
217
203
|
return null;
|
|
@@ -323,7 +309,7 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
323
309
|
throw new Error('missing');
|
|
324
310
|
let roadmap = extractCurrentMilestone(roadmapContent, cwd);
|
|
325
311
|
const hasVersionedMilestonesGlobal = /^#{1,3}\s+.*v\d+\.\d+/mi.test(roadmapContent);
|
|
326
|
-
const hasPhaseHeadings = /#{2,4}\s*(?:\[[^\]]
|
|
312
|
+
const hasPhaseHeadings = /#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+[\w]/i.test(roadmapContent);
|
|
327
313
|
if (!hasVersionedMilestonesGlobal && hasPhaseHeadings && phaseIdConvention === 'milestone-prefixed') {
|
|
328
314
|
console.warn('[gsd] Deprecated: free-form ROADMAP.md detected (no versioned milestone headings). ' +
|
|
329
315
|
'The project has phase_id_convention set to "milestone-prefixed" in config.json but the ' +
|
|
@@ -378,8 +364,8 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
378
364
|
}
|
|
379
365
|
// Use tokenizeHeadings (fence-aware) instead of stripFencedLines + regex.
|
|
380
366
|
// T4 seam migration: phase headings inside fences are excluded automatically.
|
|
381
|
-
// #1729: `(?:\s*\([^)\n]
|
|
382
|
-
const phaseHeadingPattern = /^(?:\[[^\]]
|
|
367
|
+
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
368
|
+
const phaseHeadingPattern = /^(?:\[[^\]]{1,200}\]\s*)?Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]{0,200}\))?\s*:/i;
|
|
383
369
|
for (const h of (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(roadmap)) {
|
|
384
370
|
if (h.level < 2 || h.level > 4)
|
|
385
371
|
continue;
|
|
@@ -401,8 +387,13 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
401
387
|
return id.split('-').map(seg => seg.replace(/^0+(?=\d)/, '') || '0').join('-');
|
|
402
388
|
}
|
|
403
389
|
const roadmapUsesHyphenedIds = [...normalized].some(n => n.includes('-'));
|
|
390
|
+
// #2043: milestone-prefixed sub-phase components must be zero-padded (≥2 digits)
|
|
391
|
+
// — "-\d{2,}" instead of "-0*\d+" — so a single-digit slug word after the phase
|
|
392
|
+
// number (e.g. dir "46-6-rs-…") captures "46" and is not silently excluded from
|
|
393
|
+
// the milestone as a bogus "46-6" id.
|
|
404
394
|
const numericRe = roadmapUsesHyphenedIds
|
|
405
|
-
? /^0*(\d+(
|
|
395
|
+
? /^0*(\d+(?:-\d{2,})*[A-Za-z]?(?:\.\d+)*)/
|
|
396
|
+
// phase-id-owner: the [A-Za-z] letter class does real case handling here — this regex carries NO /i flag; kept literal, not source-byte-equal to the canonical PHASE_NUMBER_TOKEN_SOURCE.
|
|
406
397
|
: /^0*(\d+[A-Za-z]?(?:\.\d+)*)/;
|
|
407
398
|
function isDirInMilestone(dirName) {
|
|
408
399
|
const m2 = dirName.match(numericRe);
|
|
@@ -19,16 +19,16 @@ const planningWorkspace = require("./planning-workspace.cjs");
|
|
|
19
19
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
20
20
|
const phaseIdMod = require("./phase-id.cjs");
|
|
21
21
|
const { planningDir } = planningWorkspace;
|
|
22
|
-
const { stripProjectCodePrefix } = phaseIdMod;
|
|
22
|
+
const { stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE } = phaseIdMod;
|
|
23
23
|
// ─── Regex helpers ────────────────────────────────────────────────────────────
|
|
24
24
|
// Matches legacy phase headings: ### Phase N: Name (also decimal: Phase 2.1:)
|
|
25
25
|
// Captures: (hashes)(spaces)(phase-number)(rest-of-line)
|
|
26
|
-
const LEGACY_PHASE_HEADING_RE =
|
|
26
|
+
const LEGACY_PHASE_HEADING_RE = new RegExp(`^(#{2,4})\\s*(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})\\s*:(.*)`, 'i');
|
|
27
27
|
// Matches already-migrated phase headings: ### Phase M-NN: Name
|
|
28
|
-
const MIGRATED_PHASE_HEADING_RE = /^#{2,4}\s*(?:\[[^\]]
|
|
28
|
+
const MIGRATED_PHASE_HEADING_RE = /^#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+\d+-\d{2}\s*:/i;
|
|
29
29
|
// Matches milestone section headings: ## v1.0, ## Roadmap v2.0, ## ✅ v1.0, ## [GSD] v1.0, etc.
|
|
30
30
|
// The optional bracket-token prefix (e.g., [GSD]) must be tested before the emoji group.
|
|
31
|
-
const MILESTONE_HEADING_RE = /^##\s+(?:\[[^\]]
|
|
31
|
+
const MILESTONE_HEADING_RE = /^##\s+(?:\[[^\]]{1,200}\]\s+|Roadmap\s+|[✅🚧]\s*)?v(\d+)\.(\d+)(?:\s|:)/iu;
|
|
32
32
|
// ─── Pure computation helpers ─────────────────────────────────────────────────
|
|
33
33
|
/**
|
|
34
34
|
* Parse the ROADMAP.md content and build a list of phase entries with their
|
|
@@ -106,7 +106,7 @@ function extractPhaseNumFromDir(dirName) {
|
|
|
106
106
|
const stripped = stripProjectCodePrefix(dirName);
|
|
107
107
|
// Matches: digits + optional letter + optional decimal suffix, followed by '-' or end.
|
|
108
108
|
// e.g. "02.1-hotfix" → "02.1", "01-setup" → "01"
|
|
109
|
-
const m = stripped.match(
|
|
109
|
+
const m = stripped.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})(?:-|$)`, 'i'));
|
|
110
110
|
return m ? m[1] : null;
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
@@ -119,7 +119,7 @@ function buildNewDirName(oldDirName, newId, projectCode) {
|
|
|
119
119
|
// Strip existing project_code prefix
|
|
120
120
|
const stripped = stripProjectCodePrefix(oldDirName);
|
|
121
121
|
// Extract slug: everything after "NN-" (the old phase num, including decimal like 02.1)
|
|
122
|
-
const slugMatch = stripped.match(
|
|
122
|
+
const slugMatch = stripped.match(new RegExp(`^${PHASE_NUMBER_TOKEN_SOURCE}-(.*)`, 'i'));
|
|
123
123
|
const slug = slugMatch ? slugMatch[1] : stripped;
|
|
124
124
|
// Build M-NN prefix (zero-pad both parts)
|
|
125
125
|
const [milestoneStr, subStr] = newId.split('-');
|
|
@@ -256,7 +256,7 @@ function computeMigrationPlan(cwd, options = {}) {
|
|
|
256
256
|
continue;
|
|
257
257
|
// Rewrite heading line: "### Phase N: Name" → "### Phase M-NN: Name"
|
|
258
258
|
const oldLine = lines[entry.lineIndex];
|
|
259
|
-
const newLine = oldLine.replace(
|
|
259
|
+
const newLine = oldLine.replace(new RegExp(`^(#{2,4}\\s*(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+)${PHASE_NUMBER_TOKEN_SOURCE}(\\s*:)`, 'i'), `$1${mapping.newId}$2`);
|
|
260
260
|
if (newLine !== oldLine) {
|
|
261
261
|
roadmapEdits.push({ lineIndex: entry.lineIndex, from: oldLine, to: newLine });
|
|
262
262
|
}
|
|
@@ -274,7 +274,7 @@ function computeMigrationPlan(cwd, options = {}) {
|
|
|
274
274
|
if (roadmapEdits.some(e => e.lineIndex === i))
|
|
275
275
|
continue;
|
|
276
276
|
// Match checklist items: "- [ ] **Phase N:**" or "- [x] Phase N:" (also decimal)
|
|
277
|
-
const checklistMatch = line.match(
|
|
277
|
+
const checklistMatch = line.match(new RegExp(`^(\\s*-\\s*\\[[ x]\\]\\s*\\*{0,2}Phase\\s+)(${PHASE_NUMBER_TOKEN_SOURCE})(\\s*[:\\s*])`, 'i'));
|
|
278
278
|
if (checklistMatch) {
|
|
279
279
|
const legacyNum = checklistMatch[2];
|
|
280
280
|
const cIntPart = parseInt(legacyNum, 10);
|
|
@@ -300,7 +300,7 @@ function computeMigrationPlan(cwd, options = {}) {
|
|
|
300
300
|
newId = found.newId;
|
|
301
301
|
}
|
|
302
302
|
if (newId) {
|
|
303
|
-
const newLine = line.replace(
|
|
303
|
+
const newLine = line.replace(new RegExp(`^(\\s*-\\s*\\[[ x]\\]\\s*\\*{0,2}Phase\\s+)${PHASE_NUMBER_TOKEN_SOURCE}(\\s*[:\\s*])`, 'i'), `$1${newId}$2`);
|
|
304
304
|
if (newLine !== line) {
|
|
305
305
|
roadmapEdits.push({ lineIndex: i, from: line, to: newLine });
|
|
306
306
|
}
|
|
@@ -17,7 +17,7 @@ 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,
|
|
20
|
+
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, phaseTokenMatches, stripProjectCodePrefix, OPTIONAL_PHASE_TAG_SOURCE, roadmapPhaseLookupSources } = 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;
|
|
@@ -102,7 +102,7 @@ function countPhasePlansAndSummaries(phaseDir) {
|
|
|
102
102
|
*/
|
|
103
103
|
function searchPhaseInContent(content, escapedPhase, phaseNum) {
|
|
104
104
|
// #1729: OPTIONAL_PHASE_TAG_SOURCE after the number tolerates a pre-colon ( ) tag.
|
|
105
|
-
const headingPattern = new RegExp(`^(?:\\[[^\\]]
|
|
105
|
+
const headingPattern = new RegExp(`^(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}:\\s*(.+)$`, 'i');
|
|
106
106
|
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
107
107
|
const headingIndex = headings.findIndex((heading) => headingPattern.test(heading.text));
|
|
108
108
|
const headerMatch = headingIndex === -1 ? null : headings[headingIndex].text.match(headingPattern);
|
|
@@ -174,23 +174,18 @@ function getRoadmapPhaseWithFallback(cwd, phaseNum) {
|
|
|
174
174
|
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
175
175
|
const milestoneContent = extractCurrentMilestone(rawContent, cwd);
|
|
176
176
|
const fullContent = stripShippedMilestones(rawContent);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
177
|
+
// #2121/#2114: iterate the shared lookup-source list (exact → numeric →
|
|
178
|
+
// prefix-tolerant) so this resolver matches getRoadmapPhaseInternal and a
|
|
179
|
+
// bare-number query resolves a drifted project-code-prefixed heading.
|
|
180
|
+
for (const source of roadmapPhaseLookupSources(phaseNum)) {
|
|
181
|
+
const milestoneResult = searchPhaseInContent(milestoneContent, source, phaseNum);
|
|
182
|
+
if (milestoneResult && !milestoneResult.error)
|
|
183
|
+
return milestoneResult.section ?? null;
|
|
184
|
+
const fullResult = searchPhaseInContent(fullContent, source, phaseNum);
|
|
185
|
+
if (fullResult && !fullResult.error)
|
|
186
|
+
return fullResult.section ?? null;
|
|
185
187
|
}
|
|
186
|
-
|
|
187
|
-
const milestoneResult = searchPhaseInContent(milestoneContent, escapedPhase, phaseNum);
|
|
188
|
-
const result = (milestoneResult && !milestoneResult.error)
|
|
189
|
-
? milestoneResult
|
|
190
|
-
: searchPhaseInContent(fullContent, escapedPhase, phaseNum) || milestoneResult;
|
|
191
|
-
if (!result || result.error)
|
|
192
|
-
return null;
|
|
193
|
-
return result.section ?? null;
|
|
188
|
+
return null;
|
|
194
189
|
}
|
|
195
190
|
// ─── cmdRoadmapGetPhase ───────────────────────────────────────────────────────
|
|
196
191
|
function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
@@ -206,46 +201,35 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
|
206
201
|
try {
|
|
207
202
|
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
208
203
|
const milestoneContent = extractCurrentMilestone(rawContent, cwd);
|
|
209
|
-
// #3599 two-pass: when the caller passes a project-code-prefixed ID like
|
|
210
|
-
// `PROJ-42`, try the exact-prefixed heading first (`### Phase PROJ-42:`).
|
|
211
|
-
// If no match, fall back to the #3537 padding-tolerant numeric form so
|
|
212
|
-
// a `CK-01` query still resolves to `### Phase 1:`. Doing this at the
|
|
213
|
-
// call site (instead of inside phaseMarkdownRegexSource) avoids the
|
|
214
|
-
// alternation-order ambiguity where a bare `### Phase 42:` heading in
|
|
215
|
-
// the same document would intercept the match for a `PROJ-42` query.
|
|
216
204
|
const fullContent = stripShippedMilestones(rawContent);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
205
|
+
// #2121/#2114: iterate the shared lookup-source list (exact → numeric →
|
|
206
|
+
// prefix-tolerant) so all three roadmap resolvers share one contract and a
|
|
207
|
+
// bare-number query resolves a drifted `### Phase AB-29:` heading. This
|
|
208
|
+
// preserves the #3599 exact-prefix-first and #3537 padding-tolerant behavior
|
|
209
|
+
// (both now encoded in roadmapPhaseLookupSources' ordering). A clean match
|
|
210
|
+
// (milestone or full, any source) wins immediately; a malformed_roadmap
|
|
211
|
+
// (checklist-only) candidate is surfaced only if no source finds a real
|
|
212
|
+
// heading — so a milestone checklist never blocks a full-roadmap header.
|
|
213
|
+
let malformed = null;
|
|
214
|
+
for (const source of roadmapPhaseLookupSources(phaseNum)) {
|
|
215
|
+
const milestoneResult = searchPhaseInContent(milestoneContent, source, phaseNum);
|
|
216
|
+
if (milestoneResult && !milestoneResult.error) {
|
|
217
|
+
output(milestoneResult, raw, milestoneResult.section);
|
|
222
218
|
return;
|
|
223
219
|
}
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
226
|
-
output(
|
|
220
|
+
const fullResult = searchPhaseInContent(fullContent, source, phaseNum);
|
|
221
|
+
if (fullResult && !fullResult.error) {
|
|
222
|
+
output(fullResult, raw, fullResult.section);
|
|
227
223
|
return;
|
|
228
224
|
}
|
|
225
|
+
if (!malformed)
|
|
226
|
+
malformed = (milestoneResult?.error ? milestoneResult : (fullResult?.error ? fullResult : null));
|
|
229
227
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const escapedPhase = phaseMarkdownRegexSource(phaseNum);
|
|
233
|
-
// Search the current milestone slice first, then fall back to full roadmap.
|
|
234
|
-
// A malformed_roadmap result (checklist-only) from the milestone should not
|
|
235
|
-
// block finding a full header match in the wider roadmap content.
|
|
236
|
-
const milestoneResult = searchPhaseInContent(milestoneContent, escapedPhase, phaseNum);
|
|
237
|
-
const result = (milestoneResult && !milestoneResult.error)
|
|
238
|
-
? milestoneResult
|
|
239
|
-
: searchPhaseInContent(fullContent, escapedPhase, phaseNum) || milestoneResult;
|
|
240
|
-
if (!result) {
|
|
241
|
-
output({ found: false, phase_number: phaseNum }, raw, '');
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
if (result.error) {
|
|
245
|
-
output(result, raw, '');
|
|
228
|
+
if (malformed) {
|
|
229
|
+
output(malformed, raw, '');
|
|
246
230
|
return;
|
|
247
231
|
}
|
|
248
|
-
output(
|
|
232
|
+
output({ found: false, phase_number: phaseNum }, raw, '');
|
|
249
233
|
}
|
|
250
234
|
catch (e) {
|
|
251
235
|
error('Failed to read ROADMAP.md: ' + e.message);
|
|
@@ -262,8 +246,9 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
262
246
|
const content = extractCurrentMilestone(rawContent, cwd);
|
|
263
247
|
const phasesDir = planningPaths(cwd).phases;
|
|
264
248
|
// Extract all phase headings: ## Phase N: Name or ### Phase N: Name
|
|
265
|
-
// #1729: `(?:\s*\([^)\n]
|
|
266
|
-
|
|
249
|
+
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
250
|
+
// phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
|
|
251
|
+
const phasePattern = /#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+(\d+[A-Z]?(?:[.-]\d+)*)(?:\s*\([^)\n]{0,200}\))?\s*:\s*([^\n]+)/gi;
|
|
267
252
|
const phases = [];
|
|
268
253
|
let match;
|
|
269
254
|
// Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not real
|
|
@@ -296,7 +281,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
296
281
|
const restOfContent = content.slice(sectionStart);
|
|
297
282
|
// #3691: `\d` → `\d[\d.]*` so decimal phase headings (e.g. `### Phase 02.3:`) are
|
|
298
283
|
// recognised as section boundaries.
|
|
299
|
-
const nextHeader = restOfContent.match(/\n#{2,4}\s+(?:\[[^\]]
|
|
284
|
+
const nextHeader = restOfContent.match(/\n#{2,4}\s+(?:\[[^\]]{1,200}\]\s*)?Phase\s+\d[\d.-]*/i);
|
|
300
285
|
const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
|
|
301
286
|
const section = content.slice(sectionStart, sectionEnd);
|
|
302
287
|
const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
|
@@ -383,6 +368,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
383
368
|
// The char class must allow `-` (not just `.`) so dash-separated milestone-prefixed
|
|
384
369
|
// IDs (e.g. `1-01`) match the detail-heading scanner above; otherwise they truncate
|
|
385
370
|
// at the dash (`1-01` -> `1`) and every such phase reports a phantom missing detail.
|
|
371
|
+
// phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
|
|
386
372
|
const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:[.-]\d+)*)/gi;
|
|
387
373
|
const checklistPhases = new Set();
|
|
388
374
|
let checklistMatch;
|
|
@@ -473,7 +459,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
473
459
|
// `**Plans**: N plans` — bold word + outer colon (gsd-core/templates/roadmap.md)
|
|
474
460
|
// `**Plans:** N plans` — bold "Plans:" (colon inside bold)
|
|
475
461
|
// `Plans: N plans` — plain text header
|
|
476
|
-
const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])[\\s\\S]*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*|(?:^|\\n)Plans:)\\s*)[^\\n]+`, 'i');
|
|
462
|
+
const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])(?:(?!\\n#{1,4}\\s)[\\s\\S])*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*|(?:^|\\n)Plans:)\\s*)[^\\n]+`, 'i');
|
|
477
463
|
const planCountText = isComplete
|
|
478
464
|
? `${summaryCount}/${planCount} plans complete`
|
|
479
465
|
: `${summaryCount}/${planCount} plans executed`;
|
|
@@ -530,8 +516,8 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
530
516
|
//
|
|
531
517
|
// Pattern A: anchor to bare `Plans:` header (preferred).
|
|
532
518
|
// Pattern B: fallback to bold summary when no bare header exists.
|
|
533
|
-
const insertRowsPatternA = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])[\\s\\S]*?(?:^|\\n)(?:Plans:)[^\\n]*)`, 'i');
|
|
534
|
-
const insertRowsPatternB = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])[\\s\\S]*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*)[^\\n]*)`, 'i');
|
|
519
|
+
const insertRowsPatternA = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])(?:(?!\\n#{1,4}\\s)[\\s\\S])*?(?:^|\\n)(?:Plans:)[^\\n]*)`, 'i');
|
|
520
|
+
const insertRowsPatternB = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])(?:(?!\\n#{1,4}\\s)[\\s\\S])*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*)[^\\n]*)`, 'i');
|
|
535
521
|
const sortedMissing = [...missingPlans].sort();
|
|
536
522
|
const newRows = sortedMissing.map(p => `- [ ] ${p}`).join('\n');
|
|
537
523
|
const inserter = (match) => `${match}\n${newRows}`;
|
|
@@ -2277,7 +2277,7 @@ function rewriteStagedSkillBodies(stagedDir, opts) {
|
|
|
2277
2277
|
const resolvedTarget = node_path_1.default.resolve(configDir).replace(/\\/g, '/');
|
|
2278
2278
|
const homeDir = homedir().replace(/\\/g, '/');
|
|
2279
2279
|
const isGlobal = scope === 'global';
|
|
2280
|
-
const isOpencode =
|
|
2280
|
+
const isOpencode = false; // #2087: opencode installs via the combined-family engine path, never through the generic rewrite
|
|
2281
2281
|
const isWindowsHost = platform === 'win32';
|
|
2282
2282
|
const pathPrefix = computePathPrefix({ isGlobal, isOpencode, isWindowsHost, resolvedTarget, homeDir });
|
|
2283
2283
|
const attribution = resolveAttribution ? resolveAttribution(runtime) : undefined;
|
|
@@ -2290,10 +2290,12 @@ function rewriteStagedSkillBodies(stagedDir, opts) {
|
|
|
2290
2290
|
* attribution from opts, then delegates to applyRuntimeContentRewritesForCommandsInPlace
|
|
2291
2291
|
* (single copy+rewrite owner).
|
|
2292
2292
|
*
|
|
2293
|
-
* @internal — symmetric companion to rewriteStagedSkillBodies;
|
|
2294
|
-
*
|
|
2295
|
-
*
|
|
2296
|
-
*
|
|
2293
|
+
* @internal — symmetric companion to rewriteStagedSkillBodies; the deep-seam API for
|
|
2294
|
+
* command bodies. Production callers: applySurface (surface.cts) and the install path
|
|
2295
|
+
* in createRuntimeArtifactInstallPlan (runtime-artifact-install-plan.cts) — both keep
|
|
2296
|
+
* the returned temp dir alive until they have copied its contents out, then clean it up
|
|
2297
|
+
* in their own finally. (A test that treats this as a throwaway shared-tmp path will
|
|
2298
|
+
* race those live temp dirs under --test-concurrency; see #1575/#2090.)
|
|
2297
2299
|
*
|
|
2298
2300
|
* @returns {string} path to the temp dir (caller is responsible for cleanup)
|
|
2299
2301
|
*/
|
|
@@ -2304,7 +2306,7 @@ function rewriteStagedCommandBodies(stagedDir, opts) {
|
|
|
2304
2306
|
const resolvedTarget = node_path_1.default.resolve(configDir).replace(/\\/g, '/');
|
|
2305
2307
|
const homeDir = homedir().replace(/\\/g, '/');
|
|
2306
2308
|
const isGlobal = scope === 'global';
|
|
2307
|
-
const isOpencode =
|
|
2309
|
+
const isOpencode = false; // #2087: opencode installs via the combined-family engine path, never through the generic rewrite
|
|
2308
2310
|
const isWindowsHost = platform === 'win32';
|
|
2309
2311
|
const pathPrefix = computePathPrefix({ isGlobal, isOpencode, isWindowsHost, resolvedTarget, homeDir });
|
|
2310
2312
|
const attribution = resolveAttribution ? resolveAttribution(runtime) : undefined;
|
|
@@ -2425,6 +2427,11 @@ module.exports = {
|
|
|
2425
2427
|
neutralizeAgentReferences,
|
|
2426
2428
|
convertClaudeCommandToOpencodeSkill,
|
|
2427
2429
|
convertClaudeCommandToKiloSkill,
|
|
2430
|
+
// #2087 — opencode/kilo command-frontmatter converters, exported so the
|
|
2431
|
+
// layout-driven `convertedCommandsKind` can resolve them by name (routes the
|
|
2432
|
+
// opencode/kilo command install through the engine instead of the bespoke path).
|
|
2433
|
+
convertClaudeToOpencodeFrontmatter,
|
|
2434
|
+
convertClaudeToKiloFrontmatter,
|
|
2428
2435
|
readGsdCommandNames,
|
|
2429
2436
|
transformContentToHyphen,
|
|
2430
2437
|
// #1383: version resolver (exported for regression test of the Codex
|
|
@@ -111,7 +111,7 @@ function createRuntimeArtifactInstallPlan(args) {
|
|
|
111
111
|
items.push({
|
|
112
112
|
kind: kind.kind,
|
|
113
113
|
sourceDir,
|
|
114
|
-
destDir: assertDestWithinConfigHome(layout.configDir, kind.destSubpath),
|
|
114
|
+
destDir: assertDestWithinConfigHome(kind.home ?? layout.configDir, kind.destSubpath),
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
117
|
return { ok: true, plan: { items, cleanupDirs } };
|
|
@@ -120,7 +120,7 @@ function createRuntimeArtifactUninstallPlan(layout) {
|
|
|
120
120
|
return {
|
|
121
121
|
items: layout.kinds.map((kind) => ({
|
|
122
122
|
kind: kind.kind,
|
|
123
|
-
destDir: assertDestWithinConfigHome(layout.configDir, kind.destSubpath),
|
|
123
|
+
destDir: assertDestWithinConfigHome(kind.home ?? layout.configDir, kind.destSubpath),
|
|
124
124
|
})),
|
|
125
125
|
};
|
|
126
126
|
}
|
|
@@ -278,27 +278,34 @@ function getRegistry() {
|
|
|
278
278
|
function dispatchKindEntry(entry, runtime, configDir, scope) {
|
|
279
279
|
const { kind, destSubpath, prefix, nesting, converter } = entry;
|
|
280
280
|
const nested = nesting === 'nested';
|
|
281
|
+
let result;
|
|
281
282
|
switch (kind) {
|
|
282
283
|
case 'commands':
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
284
|
+
result = converter == null
|
|
285
|
+
? commandsKind(destSubpath, prefix, configDir)
|
|
286
|
+
: convertedCommandsKind(destSubpath, prefix, converter, configDir);
|
|
287
|
+
break;
|
|
287
288
|
case 'agents':
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
289
|
+
result = converter == null
|
|
290
|
+
? agentsKind(destSubpath, prefix, configDir)
|
|
291
|
+
: convertedAgentsKind(destSubpath, prefix, converter, configDir, scope);
|
|
292
|
+
break;
|
|
292
293
|
case 'skills':
|
|
293
294
|
if (converter == null) {
|
|
294
295
|
throw new TypeError(`resolveRuntimeArtifactLayout: skills entry for '${runtime}' has converter=null (converter is required for skills)`);
|
|
295
296
|
}
|
|
296
|
-
|
|
297
|
+
result = skillsKind(destSubpath, prefix, converter, runtime, configDir, nested, scope);
|
|
298
|
+
break;
|
|
297
299
|
case 'kimi-agents':
|
|
298
|
-
|
|
300
|
+
result = kimiAgentsKind(destSubpath, prefix, configDir);
|
|
301
|
+
break;
|
|
299
302
|
default:
|
|
300
303
|
throw new TypeError(`resolveRuntimeArtifactLayout: unknown kind '${kind}' in descriptor for runtime '${runtime}'`);
|
|
301
304
|
}
|
|
305
|
+
if (typeof entry.home === 'string' && entry.home !== '') {
|
|
306
|
+
result.home = node_path_1.default.join(node_os_1.default.homedir(), entry.home);
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
302
309
|
}
|
|
303
310
|
/**
|
|
304
311
|
* Resolve the artifact layout for a given runtime and config directory.
|
|
@@ -296,6 +296,14 @@ function getGlobalSkillsBase(runtime) {
|
|
|
296
296
|
const runtimeEntry = getRegistry().runtimes[runtime];
|
|
297
297
|
const descriptor = runtimeEntry?.runtime;
|
|
298
298
|
const globalSkillsKind = descriptor?.artifactLayout?.global?.find((entry) => entry.kind === 'skills');
|
|
299
|
+
// ADR-1239 upgrade 3 (#2088): honor a skills-kind `home` override (e.g. Codex
|
|
300
|
+
// → $HOME/.agents/skills, independent of $CODEX_HOME) so the reported skills
|
|
301
|
+
// root matches where the installer actually writes (the artifact layout /
|
|
302
|
+
// _resolveSkillsRootDir). Without this, `--skills-root` and the sync-skills
|
|
303
|
+
// workflow would look under configHome/skills while skills live under ~/.agents.
|
|
304
|
+
if (globalSkillsKind?.home && globalSkillsKind?.destSubpath) {
|
|
305
|
+
return node_path_1.default.join(node_os_1.default.homedir(), globalSkillsKind.home, globalSkillsKind.destSubpath);
|
|
306
|
+
}
|
|
299
307
|
if (descriptor?.configHome && globalSkillsKind?.destSubpath) {
|
|
300
308
|
return resolveSkillsBaseFromDescriptor(descriptor.configHome, { env: process.env, home: node_os_1.default.homedir(), existsSync: node_fs_1.default.existsSync }, globalSkillsKind.destSubpath);
|
|
301
309
|
}
|
|
@@ -28,6 +28,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
28
28
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
29
29
|
const node_path_1 = __importDefault(require("node:path"));
|
|
30
30
|
const node_os_1 = __importDefault(require("node:os"));
|
|
31
|
+
const imperative_hook_bus_cjs_1 = require("./host-integration-adapters/imperative-hook-bus.cjs");
|
|
31
32
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
33
|
const shellCmdProjection = require("./shell-command-projection.cjs");
|
|
33
34
|
const { isManagedHookBasename, isManagedHookCommand, projectLegacySettingsHookCommand, projectManagedHookCommand, projectPortableHookBaseDir, projectCodexHookTomlCommand, shellHookOmitsBashRunner, } = shellCmdProjection;
|
|
@@ -57,7 +58,18 @@ const GSD_COPILOT_SESSION_HOOK_PWSH = 'if (Test-Path .planning/STATE.md) ' +
|
|
|
57
58
|
// ---------------------------------------------------------------------------
|
|
58
59
|
const GSD_CURSOR_SESSION_HOOK_SCRIPT = 'gsd-cursor-session-start.js';
|
|
59
60
|
const GSD_CURSOR_POST_TOOL_HOOK_SCRIPT = 'gsd-cursor-post-tool.js';
|
|
61
|
+
const GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT = 'gsd-cursor-pre-tool.js';
|
|
62
|
+
const GSD_CURSOR_STOP_HOOK_SCRIPT = 'gsd-cursor-stop.js';
|
|
63
|
+
const GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT = 'gsd-cursor-subagent-start.js';
|
|
64
|
+
const GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT = 'gsd-cursor-subagent-stop.js';
|
|
60
65
|
const GSD_CURSOR_HOOK_MARKER = 'gsd-managed';
|
|
66
|
+
// The full set of Cursor hook events GSD manages — sourced from the adapter
|
|
67
|
+
// (src/host-integration-adapters/imperative-hook-bus.cts) so the vocabulary
|
|
68
|
+
// stays closed and first-party. Used by reconcileCursorHooksJson (the
|
|
69
|
+
// reconciliation scope is always the full set). The install path
|
|
70
|
+
// (writeCursorHooksJson) resolves a descriptor-driven subset via
|
|
71
|
+
// resolveManagedHookEvents(opts.managedHookEvents).
|
|
72
|
+
const CURSOR_MANAGED_EVENTS = imperative_hook_bus_cjs_1.CURSOR_HOOK_EVENTS;
|
|
61
73
|
// ---------------------------------------------------------------------------
|
|
62
74
|
// Cline / AGENTS.md constants
|
|
63
75
|
// ---------------------------------------------------------------------------
|
|
@@ -819,9 +831,8 @@ function reconcileCursorHooksJson(hooksJsonPath, managedEntries) {
|
|
|
819
831
|
parsed = {};
|
|
820
832
|
const hasNestedHooksObject = parsed['hooks'] && typeof parsed['hooks'] === 'object' && !Array.isArray(parsed['hooks']);
|
|
821
833
|
if (!hasNestedHooksObject) {
|
|
822
|
-
const eventKeys = ['sessionStart', 'postToolUse'];
|
|
823
834
|
const lifted = {};
|
|
824
|
-
for (const k of
|
|
835
|
+
for (const k of CURSOR_MANAGED_EVENTS) {
|
|
825
836
|
if (Array.isArray(parsed[k])) {
|
|
826
837
|
lifted[k] = parsed[k];
|
|
827
838
|
delete parsed[k];
|
|
@@ -832,9 +843,8 @@ function reconcileCursorHooksJson(hooksJsonPath, managedEntries) {
|
|
|
832
843
|
if (!parsed['version'])
|
|
833
844
|
parsed['version'] = 1;
|
|
834
845
|
const hookTable = parsed['hooks'];
|
|
835
|
-
const MANAGED_EVENTS = ['sessionStart', 'postToolUse'];
|
|
836
846
|
const entries = managedEntries || {};
|
|
837
|
-
for (const event of
|
|
847
|
+
for (const event of CURSOR_MANAGED_EVENTS) {
|
|
838
848
|
const existing = Array.isArray(hookTable[event]) ? hookTable[event] : [];
|
|
839
849
|
const userOwned = existing.filter((e) => !isManagedCursorHookEntry(e));
|
|
840
850
|
const newEntry = entries[event] || null;
|
|
@@ -862,7 +872,11 @@ function writeCursorHooksJson(targetDir, src, opts) {
|
|
|
862
872
|
opts = opts || {};
|
|
863
873
|
const hooksDir = node_path_1.default.join(targetDir, 'hooks');
|
|
864
874
|
node_fs_1.default.mkdirSync(hooksDir, { recursive: true });
|
|
865
|
-
|
|
875
|
+
// Descriptor-driven event resolution (#2089): the managed event set comes
|
|
876
|
+
// from the host descriptor's hostBehaviors.managedHookEvents via the pure
|
|
877
|
+
// adapter (resolveManagedHookEvents), NOT a hardcoded constant.
|
|
878
|
+
const events = (0, imperative_hook_bus_cjs_1.resolveManagedHookEvents)(opts.managedHookEvents);
|
|
879
|
+
const hookScripts = (0, imperative_hook_bus_cjs_1.resolveHookScripts)(events);
|
|
866
880
|
const srcHooksDir = node_path_1.default.join(src, 'hooks');
|
|
867
881
|
const installedScripts = new Set();
|
|
868
882
|
for (const script of hookScripts) {
|
|
@@ -880,27 +894,17 @@ function writeCursorHooksJson(targetDir, src, opts) {
|
|
|
880
894
|
}
|
|
881
895
|
}
|
|
882
896
|
const hookOpts = { runtime: 'cursor', platform: opts.platform || process.platform };
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
type: 'command',
|
|
893
|
-
command: sessionStartCmd,
|
|
894
|
-
[GSD_CURSOR_HOOK_MARKER]: true,
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
if (postToolCmd) {
|
|
898
|
-
managedEntries['postToolUse'] = {
|
|
899
|
-
type: 'command',
|
|
900
|
-
command: postToolCmd,
|
|
901
|
-
[GSD_CURSOR_HOOK_MARKER]: true,
|
|
902
|
-
};
|
|
897
|
+
const commands = {};
|
|
898
|
+
for (const ev of events) {
|
|
899
|
+
const script = imperative_hook_bus_cjs_1.CURSOR_EVENT_SCRIPT_MAP[ev];
|
|
900
|
+
if (script && installedScripts.has(script)) {
|
|
901
|
+
commands[ev] = buildHookCommand(targetDir, script, hookOpts);
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
commands[ev] = null;
|
|
905
|
+
}
|
|
903
906
|
}
|
|
907
|
+
const managedEntries = (0, imperative_hook_bus_cjs_1.buildHookBusEntries)(events, commands);
|
|
904
908
|
const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
|
|
905
909
|
const result = reconcileCursorHooksJson(hooksJsonPath, managedEntries);
|
|
906
910
|
return { hooksJsonPath, changed: result.changed };
|
|
@@ -1449,6 +1453,10 @@ module.exports = {
|
|
|
1449
1453
|
removeCursorHooksJson,
|
|
1450
1454
|
GSD_CURSOR_SESSION_HOOK_SCRIPT,
|
|
1451
1455
|
GSD_CURSOR_POST_TOOL_HOOK_SCRIPT,
|
|
1456
|
+
GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT,
|
|
1457
|
+
GSD_CURSOR_STOP_HOOK_SCRIPT,
|
|
1458
|
+
GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT,
|
|
1459
|
+
GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT,
|
|
1452
1460
|
GSD_CURSOR_HOOK_MARKER,
|
|
1453
1461
|
// Copilot
|
|
1454
1462
|
buildCopilotHookConfig,
|