@opengsd/gsd-core 1.6.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 +5 -2
- package/bin/gsd-mcp-server.js +31 -0
- package/bin/install.js +411 -1146
- package/commands/gsd/review.md +6 -0
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +134 -8
- package/gsd-core/bin/lib/adapter-declarative.cjs +35 -0
- package/gsd-core/bin/lib/adapter-imperative.cjs +52 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +231 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +7 -7
- package/gsd-core/bin/lib/capability-loader.cjs +45 -9
- package/gsd-core/bin/lib/capability-lock.cjs +2 -2
- package/gsd-core/bin/lib/capability-registry.cjs +891 -82
- package/gsd-core/bin/lib/capability-source.cjs +26 -11
- package/gsd-core/bin/lib/capability-validator.cjs +222 -2
- package/gsd-core/bin/lib/cli-skew-check.cjs +44 -0
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- package/gsd-core/bin/lib/commands.cjs +2 -1
- package/gsd-core/bin/lib/config.cjs +27 -0
- package/gsd-core/bin/lib/embedding-adapter.cjs +27 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +70 -0
- package/gsd-core/bin/lib/frontmatter.cjs +53 -6
- package/gsd-core/bin/lib/handshake-serialized.cjs +70 -0
- package/gsd-core/bin/lib/hook-bus.cjs +81 -0
- package/gsd-core/bin/lib/host-integration-sdk.cjs +53 -0
- package/gsd-core/bin/lib/host-integration.cjs +469 -0
- package/gsd-core/bin/lib/init.cjs +35 -7
- package/gsd-core/bin/lib/install-engine.cjs +755 -0
- package/gsd-core/bin/lib/install-profiles.cjs +35 -4
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/mcp-server.cjs +194 -0
- package/gsd-core/bin/lib/milestone.cjs +68 -40
- package/gsd-core/bin/lib/model-adapter.cjs +50 -0
- package/gsd-core/bin/lib/phase-id.cjs +18 -0
- package/gsd-core/bin/lib/phase.cjs +57 -90
- package/gsd-core/bin/lib/phases-command-router.cjs +4 -3
- package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
- package/gsd-core/bin/lib/probe-core.cjs +132 -2
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
- 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-upgrade.cjs +3 -2
- package/gsd-core/bin/lib/roadmap.cjs +33 -22
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +65 -9
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +54 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +5 -2
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1 -1
- package/gsd-core/bin/lib/runtime-name-policy.cjs +160 -30
- package/gsd-core/bin/lib/shell-command-projection.cjs +16 -0
- package/gsd-core/bin/lib/stale-bake-guard.cjs +254 -0
- package/gsd-core/bin/lib/state-command-router.cjs +4 -0
- package/gsd-core/bin/lib/state-io.cjs +55 -0
- package/gsd-core/bin/lib/state-transition.cjs +1603 -0
- package/gsd-core/bin/lib/state.cjs +327 -683
- package/gsd-core/bin/lib/surface.cjs +4 -1
- 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/lib/workstream.cjs +4 -4
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -0
- package/gsd-core/references/agent-skills-bootstrap.md +60 -0
- package/gsd-core/references/honest-verifier.md +105 -0
- package/gsd-core/references/model-profiles.md +27 -0
- package/gsd-core/references/reviewer-instances.md +99 -0
- package/gsd-core/workflows/autonomous.md +30 -32
- 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/manager.md +15 -15
- 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 +4 -159
- package/gsd-core/workflows/review.md +33 -2
- package/gsd-core/workflows/thread.md +4 -4
- package/gsd-core/workflows/verify-phase.md +11 -4
- package/gsd-core/workflows/verify-work.md +1 -2
- package/hooks/dist/gsd-graphify-update.sh +7 -1
- package/hooks/gsd-graphify-update.sh +7 -1
- package/package.json +6 -4
- package/scripts/ci-test-scope.cjs +38 -9
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-regression-test-names.allowlist.json +3 -0
- package/scripts/lint-test-file-count.allowlist.json +19 -5
- package/scripts/mutation-matrix.cjs +45 -3
- package/scripts/prompt-injection-scan.sh +8 -0
- package/scripts/run-tests.cjs +51 -1
- package/scripts/sync-manifest-versions.cjs +66 -14
- package/skills/gsd-review/SKILL.md +6 -0
- package/scripts/lint-windows-test-portability.cjs +0 -178
|
@@ -7,10 +7,19 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Owns reviewer-selection policy projection for /gsd:review:
|
|
9
9
|
* explicit flags > --all > review.default_reviewers > all detected.
|
|
10
|
+
*
|
|
11
|
+
* Reviewer instances (#1517): a bounded config surface
|
|
12
|
+
* `review.reviewer_instances.<name> = {cli, model?, agent?}` lets one
|
|
13
|
+
* model-capable adapter (e.g. opencode) run as several independent reviewer
|
|
14
|
+
* identities. Instances participate ONLY in the config_default branch (no
|
|
15
|
+
* per-instance CLI flags). An instance is available iff its base `cli` is
|
|
16
|
+
* detected. The instance→cli mapping lives HERE (single source; see the parity
|
|
17
|
+
* test in tests/review-reviewer-instances.test.cjs — DEFECT.GENERATIVE-FIX).
|
|
10
18
|
*/
|
|
11
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.KNOWN_REVIEWER_SLUGS = void 0;
|
|
20
|
+
exports.INSTANCE_NAME_PATTERN = exports.KNOWN_REVIEWER_SLUGS = void 0;
|
|
13
21
|
exports.normalizeConfiguredDefaultReviewers = normalizeConfiguredDefaultReviewers;
|
|
22
|
+
exports.normalizeReviewerInstances = normalizeReviewerInstances;
|
|
14
23
|
exports.resolveReviewerSelection = resolveReviewerSelection;
|
|
15
24
|
exports.KNOWN_REVIEWER_SLUGS = [
|
|
16
25
|
'gemini',
|
|
@@ -25,6 +34,8 @@ exports.KNOWN_REVIEWER_SLUGS = [
|
|
|
25
34
|
'lm_studio',
|
|
26
35
|
'llama_cpp',
|
|
27
36
|
];
|
|
37
|
+
/** Instance names are lowercase slugs that must not shadow a built-in slug. */
|
|
38
|
+
exports.INSTANCE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
28
39
|
function normalizeConfiguredDefaultReviewers(rawValue) {
|
|
29
40
|
if (rawValue === undefined || rawValue === null) {
|
|
30
41
|
return { absent: true, values: [], errors: [] };
|
|
@@ -63,14 +74,75 @@ function normalizeConfiguredDefaultReviewers(rawValue) {
|
|
|
63
74
|
}
|
|
64
75
|
return { absent: false, values: normalized, errors };
|
|
65
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Validate the `review.reviewer_instances` config object (#1517).
|
|
79
|
+
* `cli` MUST be a known adapter (never an arbitrary shell command — Kerckhoffs /
|
|
80
|
+
* Postel: strict at the invocation boundary). `model`/`agent` are opaque
|
|
81
|
+
* pass-through strings; they are never interpolated into shell strings by this
|
|
82
|
+
* module. Instance names must not collide with a built-in slug.
|
|
83
|
+
*/
|
|
84
|
+
function normalizeReviewerInstances(rawValue) {
|
|
85
|
+
if (rawValue === undefined || rawValue === null) {
|
|
86
|
+
return { instances: {}, errors: [] };
|
|
87
|
+
}
|
|
88
|
+
if (typeof rawValue !== 'object' || Array.isArray(rawValue)) {
|
|
89
|
+
return {
|
|
90
|
+
instances: {},
|
|
91
|
+
errors: ['review.reviewer_instances must be a JSON object mapping instance names to {cli,model,agent}'],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const obj = rawValue;
|
|
95
|
+
const instances = {};
|
|
96
|
+
const errors = [];
|
|
97
|
+
for (const [name, spec] of Object.entries(obj)) {
|
|
98
|
+
if (!exports.INSTANCE_NAME_PATTERN.test(name)) {
|
|
99
|
+
errors.push(`invalid reviewer instance name '${name}': must match ^[a-z0-9][a-z0-9-]*$`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (exports.KNOWN_REVIEWER_SLUGS.includes(name)) {
|
|
103
|
+
errors.push(`reviewer instance name '${name}' must not equal a built-in reviewer slug`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
107
|
+
errors.push(`reviewer_instances.${name} must be an object with at least {cli}`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const s = spec;
|
|
111
|
+
const cli = s.cli;
|
|
112
|
+
if (typeof cli !== 'string' || !exports.KNOWN_REVIEWER_SLUGS.includes(cli)) {
|
|
113
|
+
errors.push(`reviewer_instances.${name}.cli must be a known reviewer adapter (got: ${JSON.stringify(cli)})`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const instance = { cli };
|
|
117
|
+
if (s.model !== undefined && s.model !== null) {
|
|
118
|
+
if (typeof s.model !== 'string') {
|
|
119
|
+
errors.push(`reviewer_instances.${name}.model must be a string`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
instance.model = s.model;
|
|
123
|
+
}
|
|
124
|
+
if (s.agent !== undefined && s.agent !== null) {
|
|
125
|
+
if (typeof s.agent !== 'string') {
|
|
126
|
+
errors.push(`reviewer_instances.${name}.agent must be a string`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
instance.agent = s.agent;
|
|
130
|
+
}
|
|
131
|
+
instances[name] = instance;
|
|
132
|
+
}
|
|
133
|
+
return { instances, errors };
|
|
134
|
+
}
|
|
66
135
|
function resolveReviewerSelection(input) {
|
|
67
136
|
const detected = new Set((input.detected ?? []).map((v) => String(v).toLowerCase()));
|
|
68
137
|
const explicitFlags = new Set((input.explicitFlags ?? []).map((v) => String(v).toLowerCase()));
|
|
69
138
|
const allFlag = !!input.allFlag;
|
|
70
139
|
const normalizedDefaults = normalizeConfiguredDefaultReviewers(input.configuredDefaultReviewers);
|
|
140
|
+
const normalizedInstances = normalizeReviewerInstances(input.reviewerInstances);
|
|
141
|
+
const instances = normalizedInstances.instances;
|
|
142
|
+
const instancesConfigured = Object.keys(instances).length > 0;
|
|
71
143
|
const warnings = [];
|
|
72
144
|
const infos = [];
|
|
73
|
-
const errors = [...normalizedDefaults.errors];
|
|
145
|
+
const errors = [...normalizedDefaults.errors, ...normalizedInstances.errors];
|
|
74
146
|
let source = 'no_config_all_detected';
|
|
75
147
|
let selected = [];
|
|
76
148
|
if (explicitFlags.size > 0) {
|
|
@@ -90,20 +162,40 @@ function resolveReviewerSelection(input) {
|
|
|
90
162
|
}
|
|
91
163
|
else if (!normalizedDefaults.absent) {
|
|
92
164
|
source = 'config_default';
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
165
|
+
// #1517: expand instance references BEFORE the built-in-slug check. An
|
|
166
|
+
// instance name and a built-in slug are the two legal kinds of entry.
|
|
167
|
+
for (const entry of normalizedDefaults.values) {
|
|
168
|
+
if (instances[entry]) {
|
|
169
|
+
// Instance reference — available iff its base cli is detected.
|
|
170
|
+
const cli = instances[entry].cli;
|
|
171
|
+
if (!detected.has(cli)) {
|
|
172
|
+
infos.push(`configured instance ${entry} not detected (cli ${cli} missing on this host)`);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
selected.push(entry);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (exports.KNOWN_REVIEWER_SLUGS.includes(entry)) {
|
|
179
|
+
if (!detected.has(entry)) {
|
|
180
|
+
infos.push(`configured reviewers not detected on this host: ${entry}`);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
selected.push(entry);
|
|
184
|
+
}
|
|
97
185
|
}
|
|
98
186
|
else {
|
|
99
|
-
|
|
187
|
+
// Neither a defined instance nor a built-in slug.
|
|
188
|
+
if (instancesConfigured) {
|
|
189
|
+
// Most likely a typo'd instance name — must be loud (#1517 design Q2).
|
|
190
|
+
errors.push(`reviewer instance '${entry}' referenced in review.default_reviewers is not defined in review.reviewer_instances`);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// Backward-compatible behaviour: unknown slug with no instances
|
|
194
|
+
// configured warns and is dropped.
|
|
195
|
+
warnings.push(`unknown reviewer slug in review.default_reviewers: ${entry}`);
|
|
196
|
+
}
|
|
100
197
|
}
|
|
101
198
|
}
|
|
102
|
-
const undetected = knownDefaults.filter((slug) => !detected.has(slug));
|
|
103
|
-
if (undetected.length > 0) {
|
|
104
|
-
infos.push(`configured reviewers not detected on this host: ${undetected.join(', ')}`);
|
|
105
|
-
}
|
|
106
|
-
selected = knownDefaults.filter((slug) => detected.has(slug));
|
|
107
199
|
if (selected.length === 0 && errors.length === 0) {
|
|
108
200
|
errors.push('all configured default reviewers are unavailable on this host');
|
|
109
201
|
}
|
|
@@ -111,11 +203,35 @@ function resolveReviewerSelection(input) {
|
|
|
111
203
|
else {
|
|
112
204
|
selected = [...detected];
|
|
113
205
|
}
|
|
206
|
+
const selectedSorted = selected.sort();
|
|
207
|
+
// Single-source instance→cli resolution projected onto the selected set.
|
|
208
|
+
const resolvedInstances = selectedSorted.map((identity) => {
|
|
209
|
+
const inst = instances[identity];
|
|
210
|
+
if (inst) {
|
|
211
|
+
return {
|
|
212
|
+
identity,
|
|
213
|
+
kind: 'instance',
|
|
214
|
+
cli: inst.cli,
|
|
215
|
+
model: inst.model,
|
|
216
|
+
agent: inst.agent,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return { identity, kind: 'builtin', cli: identity };
|
|
220
|
+
});
|
|
221
|
+
const cliCounts = {};
|
|
222
|
+
for (const r of resolvedInstances) {
|
|
223
|
+
if (r.kind === 'instance') {
|
|
224
|
+
cliCounts[r.cli] = (cliCounts[r.cli] ?? 0) + 1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const sharedAdapterCaveat = Object.values(cliCounts).some((c) => c >= 2);
|
|
114
228
|
return {
|
|
115
229
|
source,
|
|
116
|
-
selected:
|
|
230
|
+
selected: selectedSorted,
|
|
117
231
|
warnings,
|
|
118
232
|
infos,
|
|
119
233
|
errors,
|
|
234
|
+
resolvedInstances,
|
|
235
|
+
sharedAdapterCaveat,
|
|
120
236
|
};
|
|
121
237
|
}
|
|
@@ -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;
|
|
@@ -13,6 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
14
|
const node_path_1 = __importDefault(require("node:path"));
|
|
15
15
|
const node_child_process_1 = require("node:child_process");
|
|
16
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
16
17
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
17
18
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
18
19
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -416,7 +417,7 @@ function applyMigration(cwd, plan, options = {}) {
|
|
|
416
417
|
const oldPath = node_path_1.default.join(phasesDir, phaseEntry.oldDir);
|
|
417
418
|
const newPath = node_path_1.default.join(phasesDir, phaseEntry.newDir);
|
|
418
419
|
if (node_fs_1.default.existsSync(oldPath)) {
|
|
419
|
-
|
|
420
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(oldPath, newPath);
|
|
420
421
|
performedRenames.push({ oldPath, newPath });
|
|
421
422
|
renamedDirs.push(`${phaseEntry.oldDir} → ${phaseEntry.newDir}`);
|
|
422
423
|
}
|
|
@@ -483,7 +484,7 @@ function applyMigration(cwd, plan, options = {}) {
|
|
|
483
484
|
const { oldPath, newPath } = performedRenames[i];
|
|
484
485
|
try {
|
|
485
486
|
if (node_fs_1.default.existsSync(newPath))
|
|
486
|
-
|
|
487
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(newPath, oldPath);
|
|
487
488
|
}
|
|
488
489
|
catch { /* best-effort */ }
|
|
489
490
|
}
|
|
@@ -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");
|
|
@@ -55,8 +56,11 @@ function coerceTruthToString(t) {
|
|
|
55
56
|
return String(t);
|
|
56
57
|
}
|
|
57
58
|
if (typeof t === 'object') {
|
|
58
|
-
// Prefer common title-bearing keys produced by parseMustHavesBlock
|
|
59
|
-
|
|
59
|
+
// Prefer common title-bearing keys produced by parseMustHavesBlock. `statement` is the canonical
|
|
60
|
+
// truth/prohibition payload field — and the carrier of #1154's object-form backstop truth
|
|
61
|
+
// `{ statement, verification: backstop }`, so it leads (a non-inferable truth must be coerced by
|
|
62
|
+
// its statement, never dropped — the Hyrum backward-compat guard for the new marker).
|
|
63
|
+
for (const k of ['statement', 'title', 'text', 'name', 'rule', 'path', 'provides']) {
|
|
60
64
|
const v = t[k];
|
|
61
65
|
if (typeof v === 'string' && v.trim())
|
|
62
66
|
return v;
|
|
@@ -91,12 +95,14 @@ function countPhasePlansAndSummaries(phaseDir) {
|
|
|
91
95
|
* checklist-only match), or null if the phase is not present at all.
|
|
92
96
|
*/
|
|
93
97
|
function searchPhaseInContent(content, escapedPhase, phaseNum) {
|
|
94
|
-
//
|
|
95
|
-
const
|
|
96
|
-
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);
|
|
97
103
|
if (!headerMatch) {
|
|
98
104
|
// Fallback: check if phase exists in summary list but missing detail section
|
|
99
|
-
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');
|
|
100
106
|
const checklistMatch = content.match(checklistPattern);
|
|
101
107
|
if (checklistMatch) {
|
|
102
108
|
return {
|
|
@@ -110,14 +116,12 @@ function searchPhaseInContent(content, escapedPhase, phaseNum) {
|
|
|
110
116
|
return null;
|
|
111
117
|
}
|
|
112
118
|
const phaseName = headerMatch[1].trim();
|
|
113
|
-
const headerIndex =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const sectionEnd =
|
|
119
|
-
? headerIndex + nextHeaderMatch.index
|
|
120
|
-
: 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;
|
|
121
125
|
const section = content.slice(headerIndex, sectionEnd).trim();
|
|
122
126
|
// Extract goal if present (supports both **Goal:** and **Goal**: formats)
|
|
123
127
|
const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
|
@@ -156,6 +160,8 @@ function searchPhaseInContent(content, escapedPhase, phaseNum) {
|
|
|
156
160
|
* phase resolution as `roadmap.get-phase` — not a milestone-only subset.
|
|
157
161
|
*/
|
|
158
162
|
function getRoadmapPhaseWithFallback(cwd, phaseNum) {
|
|
163
|
+
if (/^999(?:\.|$)/.test(stripProjectCodePrefix(phaseNum)))
|
|
164
|
+
return null;
|
|
159
165
|
const roadmapPath = planningPaths(cwd).roadmap;
|
|
160
166
|
if (!node_fs_1.default.existsSync(roadmapPath))
|
|
161
167
|
return null;
|
|
@@ -182,6 +188,10 @@ function getRoadmapPhaseWithFallback(cwd, phaseNum) {
|
|
|
182
188
|
}
|
|
183
189
|
// ─── cmdRoadmapGetPhase ───────────────────────────────────────────────────────
|
|
184
190
|
function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
191
|
+
if (/^999(?:\.|$)/.test(stripProjectCodePrefix(phaseNum))) {
|
|
192
|
+
output({ found: false, phase_number: phaseNum }, raw, '');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
185
195
|
const roadmapPath = planningPaths(cwd).roadmap;
|
|
186
196
|
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
187
197
|
output({ found: false, error: 'ROADMAP.md not found' }, raw, '');
|
|
@@ -246,7 +256,8 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
246
256
|
const content = extractCurrentMilestone(rawContent, cwd);
|
|
247
257
|
const phasesDir = planningPaths(cwd).phases;
|
|
248
258
|
// Extract all phase headings: ## Phase N: Name or ### Phase N: Name
|
|
249
|
-
|
|
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;
|
|
250
261
|
const phases = [];
|
|
251
262
|
let match;
|
|
252
263
|
// Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not real
|
|
@@ -322,7 +333,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
322
333
|
// #3537: padding-tolerant fragment — the heading discovered above may use
|
|
323
334
|
// a different padding than the summary-bullet checkbox below it (mixed
|
|
324
335
|
// padding inside one ROADMAP is legal and seen in real projects).
|
|
325
|
-
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');
|
|
326
337
|
const checkboxMatch = content.match(checkboxPattern);
|
|
327
338
|
const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
|
|
328
339
|
// If roadmap marks phase complete, trust that over disk file structure.
|
|
@@ -447,14 +458,14 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
447
458
|
// `**Plans**: N plans` — bold word + outer colon (gsd-core/templates/roadmap.md)
|
|
448
459
|
// `**Plans:** N plans` — bold "Plans:" (colon inside bold)
|
|
449
460
|
// `Plans: N plans` — plain text header
|
|
450
|
-
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');
|
|
451
462
|
const planCountText = isComplete
|
|
452
463
|
? `${summaryCount}/${planCount} plans complete`
|
|
453
464
|
: `${summaryCount}/${planCount} plans executed`;
|
|
454
465
|
roadmapContent = replaceInCurrentMilestone(roadmapContent, planCountPattern, `$1${planCountText}`);
|
|
455
466
|
// If complete: check checkbox
|
|
456
467
|
if (isComplete) {
|
|
457
|
-
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');
|
|
458
469
|
roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
|
|
459
470
|
}
|
|
460
471
|
// Mark completed plan checkboxes (e.g. "- [ ] 50-01-PLAN.md", "- [ ] 50-01:", or "- [ ] **50-01**")
|
|
@@ -504,8 +515,8 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
504
515
|
//
|
|
505
516
|
// Pattern A: anchor to bare `Plans:` header (preferred).
|
|
506
517
|
// Pattern B: fallback to bold summary when no bare header exists.
|
|
507
|
-
const insertRowsPatternA = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}(?=[:\\s])[\\s\\S]*?(?:^|\\n)(?:Plans:)[^\\n]*)`, 'i');
|
|
508
|
-
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');
|
|
509
520
|
const sortedMissing = [...missingPlans].sort();
|
|
510
521
|
const newRows = sortedMissing.map(p => `- [ ] ${p}`).join('\n');
|
|
511
522
|
const inserter = (match) => `${match}\n${newRows}`;
|
|
@@ -642,7 +653,7 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
|
|
|
642
653
|
// #3537: padding-tolerant fragment so the caller's resolved padded id
|
|
643
654
|
// matches un-padded ROADMAP headings.
|
|
644
655
|
const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
|
|
645
|
-
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');
|
|
646
657
|
const phaseMatch = content.match(phaseHeaderPattern);
|
|
647
658
|
if (!phaseMatch)
|
|
648
659
|
return;
|
|
@@ -25,6 +25,7 @@ const commandRoster = require("./command-roster.cjs");
|
|
|
25
25
|
const { readGsdCommandNames, transformContentToHyphen } = commandRoster;
|
|
26
26
|
const runtimeNamePolicy = require("./runtime-name-policy.cjs");
|
|
27
27
|
const { getDirName } = runtimeNamePolicy;
|
|
28
|
+
const capabilityRegistry = require("./capability-registry.cjs");
|
|
28
29
|
// #1383: resolve GSD's version WITHOUT a top-level
|
|
29
30
|
// `require('../../../package.json')`. That require ran at module load on every
|
|
30
31
|
// gsd-tools invocation (this module sits in the gsd-tools loader chain) and
|
|
@@ -1997,16 +1998,15 @@ function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost
|
|
|
1997
1998
|
}
|
|
1998
1999
|
/**
|
|
1999
2000
|
* Canonical list of every non-Claude runtime that gsd-core emits artifacts for.
|
|
2000
|
-
*
|
|
2001
|
-
*
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2001
|
+
* DERIVED from the capability registry (ADR-1239 Phase B, #1679) — the registry's
|
|
2002
|
+
* `runtimes` map is the single source of truth for runtime identity, so the
|
|
2003
|
+
* non-Claude set is its key set minus 'claude'. This replaces a hand-maintained
|
|
2004
|
+
* literal that had to be kept in sync with bin/install.js and getDirName(), and
|
|
2005
|
+
* can no longer drift from the registry. Exported so tests import one source (#1521).
|
|
2004
2006
|
*/
|
|
2005
|
-
const NON_CLAUDE_RUNTIMES =
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
'codebuddy', 'cline',
|
|
2009
|
-
];
|
|
2007
|
+
const NON_CLAUDE_RUNTIMES = Object.keys(capabilityRegistry.runtimes)
|
|
2008
|
+
.filter((id) => id !== 'claude')
|
|
2009
|
+
.sort();
|
|
2010
2010
|
/**
|
|
2011
2011
|
* #1521: Every non-Claude runtime resolves its own runtime identity from a
|
|
2012
2012
|
* runtime-neutral config, and defaults workflow.use_worktrees to false —
|
|
@@ -2310,6 +2310,59 @@ function rewriteStagedCommandBodies(stagedDir, opts) {
|
|
|
2310
2310
|
const attribution = resolveAttribution ? resolveAttribution(runtime) : undefined;
|
|
2311
2311
|
return applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathPrefix, isGlobal, attribution);
|
|
2312
2312
|
}
|
|
2313
|
+
/**
|
|
2314
|
+
* Runtimes that use the hyphen-namespace form `/gsd-<cmd>` in agent bodies.
|
|
2315
|
+
* claude/qwen/hermes use hyphen-name:`...` frontmatter; cursor/windsurf/etc
|
|
2316
|
+
* self-convert. Mirrors the `HYPHEN_NAME_AGENT_RUNTIMES` set in bin/install.js.
|
|
2317
|
+
*
|
|
2318
|
+
* @private — export normalizeAgentBodyForRuntime for callers.
|
|
2319
|
+
*/
|
|
2320
|
+
const HYPHEN_NAME_AGENT_RUNTIMES = new Set(['claude', 'qwen', 'hermes']);
|
|
2321
|
+
/**
|
|
2322
|
+
* Normalize `/gsd:<cmd>` colon refs in the agent body to `/gsd-<cmd>` for
|
|
2323
|
+
* hyphen-`name:` runtimes (claude / qwen / hermes). No-op for all other
|
|
2324
|
+
* runtimes. Mirrors the per-file call in bin/install.js line 9400.
|
|
2325
|
+
*
|
|
2326
|
+
* @param content raw agent file content (post-converter)
|
|
2327
|
+
* @param runtime canonical runtime ID
|
|
2328
|
+
* @param cmdNames gsd command names from readGsdCommandNames()
|
|
2329
|
+
*/
|
|
2330
|
+
function normalizeAgentBodyForRuntime(content, runtime, cmdNames) {
|
|
2331
|
+
if (!HYPHEN_NAME_AGENT_RUNTIMES.has(runtime))
|
|
2332
|
+
return content;
|
|
2333
|
+
return transformContentToHyphen(content, cmdNames);
|
|
2334
|
+
}
|
|
2335
|
+
/**
|
|
2336
|
+
* Apply the 4 base `~/.claude/` path-prefix rewrites to a single agent content
|
|
2337
|
+
* string. Mirrors the inline agent loop in bin/install.js lines 9330-9340:
|
|
2338
|
+
* ~/\.claude/ → pathPrefix
|
|
2339
|
+
* $HOME/\.claude/ → pathPrefix
|
|
2340
|
+
* ~/\.claude\b → normalizedPathPrefix
|
|
2341
|
+
* $HOME/\.claude\b → normalizedPathPrefix
|
|
2342
|
+
*
|
|
2343
|
+
* Skipped for copilot and antigravity (which do NOT do path rewrites in the
|
|
2344
|
+
* inline loop). NO stamp (_stampNonClaudeRuntimeDefaults) — agents are NOT
|
|
2345
|
+
* stamped in the inline loop.
|
|
2346
|
+
*
|
|
2347
|
+
* ADR-1235 §1: pre-converter cross-cutting for descriptor-driven agent pipeline.
|
|
2348
|
+
* Exported as `applyAgentPathRewrites` for testing and for injection into
|
|
2349
|
+
* stageAgentsForRuntimeWithConverter via agentCtx.
|
|
2350
|
+
*
|
|
2351
|
+
* @param content raw agent file content
|
|
2352
|
+
* @param runtime canonical runtime ID
|
|
2353
|
+
* @param pathPrefix trailing-slash path prefix (e.g. '$HOME/.cursor/')
|
|
2354
|
+
* @returns content with path-prefix rewrites applied (or unchanged for copilot/antigravity)
|
|
2355
|
+
*/
|
|
2356
|
+
function applyAgentPathRewrites(content, runtime, pathPrefix) {
|
|
2357
|
+
if (runtime === 'copilot' || runtime === 'antigravity')
|
|
2358
|
+
return content;
|
|
2359
|
+
const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
|
|
2360
|
+
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
2361
|
+
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
2362
|
+
content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
|
|
2363
|
+
content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
|
|
2364
|
+
return content;
|
|
2365
|
+
}
|
|
2313
2366
|
// ── End rewrite engine ────────────────────────────────────────────────────────
|
|
2314
2367
|
/**
|
|
2315
2368
|
* Apply Co-Authored-By attribution policy to file content.
|
|
@@ -2398,6 +2451,9 @@ module.exports = {
|
|
|
2398
2451
|
// High-level wrappers (derive pathPrefix + attribution from opts):
|
|
2399
2452
|
rewriteStagedSkillBodies,
|
|
2400
2453
|
rewriteStagedCommandBodies,
|
|
2454
|
+
// ADR-1235 §1: descriptor-driven agent cross-cutting
|
|
2455
|
+
applyAgentPathRewrites,
|
|
2456
|
+
normalizeAgentBodyForRuntime,
|
|
2401
2457
|
_computePathPrefix: computePathPrefix,
|
|
2402
2458
|
_applyRuntimeRewrites,
|
|
2403
2459
|
_stampNonClaudeRuntimeDefaults,
|