@opengsd/gsd-core 1.6.0 → 1.7.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-verifier.md +1 -0
- package/bin/gsd-mcp-server.js +31 -0
- package/bin/install.js +293 -1145
- package/commands/gsd/review.md +6 -0
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +116 -1
- 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 +18 -0
- package/gsd-core/bin/lib/capability-lock.cjs +2 -2
- package/gsd-core/bin/lib/capability-registry.cjs +889 -82
- package/gsd-core/bin/lib/capability-source.cjs +4 -4
- package/gsd-core/bin/lib/capability-validator.cjs +198 -0
- 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/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/hook-bus.cjs +81 -0
- package/gsd-core/bin/lib/host-integration.cjs +408 -0
- package/gsd-core/bin/lib/init.cjs +1 -1
- 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 +27 -30
- package/gsd-core/bin/lib/model-adapter.cjs +50 -0
- package/gsd-core/bin/lib/phase.cjs +41 -72
- package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
- package/gsd-core/bin/lib/probe-core.cjs +91 -1
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +3 -2
- package/gsd-core/bin/lib/roadmap.cjs +17 -3
- 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 +37 -1
- 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 +1588 -0
- package/gsd-core/bin/lib/state.cjs +306 -681
- package/gsd-core/bin/lib/surface.cjs +4 -1
- package/gsd-core/bin/lib/workstream.cjs +4 -4
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -0
- package/gsd-core/references/honest-verifier.md +105 -0
- package/gsd-core/references/reviewer-instances.md +99 -0
- package/gsd-core/workflows/autonomous.md +9 -9
- package/gsd-core/workflows/manager.md +15 -15
- package/gsd-core/workflows/plan-phase.md +1 -1
- package/gsd-core/workflows/review.md +26 -0
- package/gsd-core/workflows/thread.md +4 -4
- package/gsd-core/workflows/verify-phase.md +11 -4
- package/hooks/dist/gsd-graphify-update.sh +7 -1
- package/hooks/gsd-graphify-update.sh +7 -1
- package/package.json +4 -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/lint-windows-test-portability.cjs +0 -178
|
@@ -30,13 +30,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
30
30
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
31
31
|
};
|
|
32
32
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
-
exports.PROHIBITION_VALIDATORS = exports.VALID_STATUS = void 0;
|
|
33
|
+
exports.INSUFFICIENT_SPEC = exports.PROHIBITION_VALIDATORS = exports.VALID_STATUS = void 0;
|
|
34
34
|
exports.validateRequirement = validateRequirement;
|
|
35
35
|
exports.validateResolution = validateResolution;
|
|
36
36
|
exports.analyzeCoverage = analyzeCoverage;
|
|
37
37
|
exports.validateProhibitionResolution = validateProhibitionResolution;
|
|
38
38
|
exports.projectProhibitions = projectProhibitions;
|
|
39
39
|
exports.dispositionForProhibition = dispositionForProhibition;
|
|
40
|
+
exports.truthStatement = truthStatement;
|
|
41
|
+
exports.truthVerification = truthVerification;
|
|
42
|
+
exports.projectTruths = projectTruths;
|
|
43
|
+
exports.dispositionForUnverifiableTruth = dispositionForUnverifiableTruth;
|
|
40
44
|
exports.runProbeCli = runProbeCli;
|
|
41
45
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
42
46
|
/** The LOCKED set of valid lifecycle statuses (the re-cut: no covered/backstop). */
|
|
@@ -355,6 +359,92 @@ function dispositionForProhibition(prohibition, context = {}) {
|
|
|
355
359
|
reason: 'judgment-tier prohibition routes to judgment review — never a silent green (ADR-550 D4)',
|
|
356
360
|
};
|
|
357
361
|
}
|
|
362
|
+
/** Extract a truth's statement text from either the string or the object form (the Hyrum normalizer). */
|
|
363
|
+
function truthStatement(truth) {
|
|
364
|
+
if (typeof truth === 'string')
|
|
365
|
+
return truth;
|
|
366
|
+
if (truth != null && typeof truth === 'object') {
|
|
367
|
+
const s = truth.statement;
|
|
368
|
+
if (typeof s === 'string')
|
|
369
|
+
return s;
|
|
370
|
+
}
|
|
371
|
+
return '';
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Extract a truth's verification tier, or `null` when it carries none (a plain string, or an object
|
|
375
|
+
* 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), and the marker is
|
|
377
|
+
* machine-emitted from validated edge data so garbling is not a live input path.
|
|
378
|
+
*/
|
|
379
|
+
function truthVerification(truth) {
|
|
380
|
+
if (truth == null || typeof truth !== 'object')
|
|
381
|
+
return null;
|
|
382
|
+
const v = truth.verification;
|
|
383
|
+
return v === 'explicit' || v === 'backstop' ? v : null;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Conservative serializer (Postel: "send well-formed, minimal data") for projecting truths into a
|
|
387
|
+
* `must_haves.truths` block — the truth-axis analogue of `projectProhibitions`. A `backstop` truth is
|
|
388
|
+
* emitted as a flat-scalar object `{ statement, verification: 'backstop' }` (ADR-550 #1278: flat
|
|
389
|
+
* scalars round-trip the existing `parseMustHavesBlock`; a nested object would mangle it). Every other
|
|
390
|
+
* truth collapses to a bare statement string — only the non-inferable tier needs a structured marker,
|
|
391
|
+
* so an `explicit`/inferable truth never carries one (no spurious markers). Empty statements are dropped.
|
|
392
|
+
*/
|
|
393
|
+
function projectTruths(items) {
|
|
394
|
+
if (!Array.isArray(items))
|
|
395
|
+
return [];
|
|
396
|
+
const out = [];
|
|
397
|
+
for (const item of items) {
|
|
398
|
+
const statement = truthStatement(item);
|
|
399
|
+
if (!statement)
|
|
400
|
+
continue;
|
|
401
|
+
if (truthVerification(item) === 'backstop') {
|
|
402
|
+
out.push({ statement, verification: 'backstop' });
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
out.push(statement);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
/** The stable, distinguishable verdict-reason token for an abstained non-inferable truth (review condition 1). */
|
|
411
|
+
exports.INSUFFICIENT_SPEC = 'insufficient_spec';
|
|
412
|
+
/**
|
|
413
|
+
* Deterministic verify-time disposition for a single truth (ADR-550 D4 truth-axis mirror, #1154).
|
|
414
|
+
* PURE — no LLM judgment (ADR-550 D5); the LLM verifier's only job is to decide whether `evidence`
|
|
415
|
+
* exists, this helper owns the routing once that is known.
|
|
416
|
+
*
|
|
417
|
+
* - A `backstop` (non-inferable) truth with NO explicit evidence → `{ unverified, flagged }`,
|
|
418
|
+
* reason `insufficient_spec`. NEVER green — the verify-time companion to D4's never-silent-pass.
|
|
419
|
+
* - A `backstop` truth WITH explicit evidence (a passing wired held-out/property test) → `green`.
|
|
420
|
+
* Abstention is for the *unconfirmable*, not for every non-inferable check.
|
|
421
|
+
* - Any non-`backstop` truth (explicit, or a plain inferable string) → `green`, never flagged.
|
|
422
|
+
* This is the over-abstention guard (AC#3): abstention fires ONLY on the exogenous backstop tag.
|
|
423
|
+
*/
|
|
424
|
+
function dispositionForUnverifiableTruth(truth, context = {}) {
|
|
425
|
+
const tier = truthVerification(truth);
|
|
426
|
+
// Over-abstention guard (AC#3): only a backstop (non-inferable) truth is ever a candidate to abstain.
|
|
427
|
+
if (tier !== 'backstop') {
|
|
428
|
+
return {
|
|
429
|
+
status: 'green',
|
|
430
|
+
flagged: false,
|
|
431
|
+
tier,
|
|
432
|
+
reason: 'inferable truth — verified normally (no abstention; ADR-550 D4 over-abstention guard)',
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const evidence = Array.isArray(context.evidence) ? context.evidence : [];
|
|
436
|
+
if (evidence.length === 0) {
|
|
437
|
+
// ABSTAIN: a non-inferable truth the verifier cannot confirm with explicit evidence. Routes to
|
|
438
|
+
// human_needed with the distinguishable insufficient_spec reason — never a silent pass (ADR-550 D4).
|
|
439
|
+
return { status: 'unverified', flagged: true, tier, reason: exports.INSUFFICIENT_SPEC };
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
status: 'green',
|
|
443
|
+
flagged: false,
|
|
444
|
+
tier,
|
|
445
|
+
reason: 'backstop truth confirmed by explicit evidence (a passing wired held-out/property test or directly-observed behavior)',
|
|
446
|
+
};
|
|
447
|
+
}
|
|
358
448
|
/**
|
|
359
449
|
* Read the requirements file (and optional resolutions file), run the adapter's `analyze`,
|
|
360
450
|
* and write the report as pretty JSON + newline. With no requirements path, writes the usage
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -55,8 +55,11 @@ function coerceTruthToString(t) {
|
|
|
55
55
|
return String(t);
|
|
56
56
|
}
|
|
57
57
|
if (typeof t === 'object') {
|
|
58
|
-
// Prefer common title-bearing keys produced by parseMustHavesBlock
|
|
59
|
-
|
|
58
|
+
// Prefer common title-bearing keys produced by parseMustHavesBlock. `statement` is the canonical
|
|
59
|
+
// truth/prohibition payload field — and the carrier of #1154's object-form backstop truth
|
|
60
|
+
// `{ statement, verification: backstop }`, so it leads (a non-inferable truth must be coerced by
|
|
61
|
+
// its statement, never dropped — the Hyrum backward-compat guard for the new marker).
|
|
62
|
+
for (const k of ['statement', 'title', 'text', 'name', 'rule', 'path', 'provides']) {
|
|
60
63
|
const v = t[k];
|
|
61
64
|
if (typeof v === 'string' && v.trim())
|
|
62
65
|
return v;
|
|
@@ -249,6 +252,15 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
249
252
|
const phasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+(\d+[A-Z]?(?:[.-]\d+)*)\s*:\s*([^\n]+)/gi;
|
|
250
253
|
const phases = [];
|
|
251
254
|
let match;
|
|
255
|
+
// Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not real
|
|
256
|
+
// phases. They legitimately have no directory and must never be surfaced as
|
|
257
|
+
// current/next phase or counted in phase_count. Mirrors the engine-wide
|
|
258
|
+
// sentinel convention (phase-id getMilestoneFromPhaseId, roadmap-command-router
|
|
259
|
+
// SENTINELS, the #1445 /^999/ progress filters). (#1580)
|
|
260
|
+
const isSentinelPhase = (num) => {
|
|
261
|
+
const major = parseInt(num, 10);
|
|
262
|
+
return major === 0 || major === 999;
|
|
263
|
+
};
|
|
252
264
|
// Build phase directory lookup once (O(1) readdir instead of O(N) per phase)
|
|
253
265
|
const _phaseDirNames = (() => {
|
|
254
266
|
try {
|
|
@@ -262,6 +274,8 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
262
274
|
})();
|
|
263
275
|
while ((match = phasePattern.exec(content)) !== null) {
|
|
264
276
|
const phaseNum = match[1];
|
|
277
|
+
if (isSentinelPhase(phaseNum))
|
|
278
|
+
continue;
|
|
265
279
|
const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
|
|
266
280
|
// Extract goal from the section
|
|
267
281
|
const sectionStart = match.index;
|
|
@@ -362,7 +376,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
362
376
|
checklistPhases.add(checklistMatch[1]);
|
|
363
377
|
}
|
|
364
378
|
const detailPhases = new Set(phases.map(p => p.number));
|
|
365
|
-
const missingDetails = [...checklistPhases].filter(p => !detailPhases.has(p));
|
|
379
|
+
const missingDetails = [...checklistPhases].filter(p => !detailPhases.has(p) && !isSentinelPhase(p));
|
|
366
380
|
const result = {
|
|
367
381
|
milestones,
|
|
368
382
|
phases,
|
|
@@ -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,
|
|
@@ -9,6 +9,30 @@
|
|
|
9
9
|
// In .cts (CommonJS output) files, `require` is available as a global.
|
|
10
10
|
const _require = require;
|
|
11
11
|
const path = _require('node:path');
|
|
12
|
+
/**
|
|
13
|
+
* Asserts that `destSubpath` resolves to a path inside `configDir`.
|
|
14
|
+
*
|
|
15
|
+
* Rejects any path that escapes the configDir root (e.g. "../../etc") and any
|
|
16
|
+
* path containing a NUL byte. This is a security gate for Phase B of
|
|
17
|
+
* ADR-1239: third-party descriptors must never be able to write outside the
|
|
18
|
+
* designated config home directory.
|
|
19
|
+
*
|
|
20
|
+
* @param configDir - The root config directory (e.g. ~/.claude).
|
|
21
|
+
* @param destSubpath - The relative path declared by the runtime descriptor.
|
|
22
|
+
* @returns The resolved absolute path under configDir.
|
|
23
|
+
* @throws {Error} if destSubpath escapes configDir or contains a NUL byte.
|
|
24
|
+
*/
|
|
25
|
+
function assertDestWithinConfigHome(configDir, destSubpath) {
|
|
26
|
+
if (destSubpath.includes('\0')) {
|
|
27
|
+
throw new Error(`destSubpath "${destSubpath}" contains a NUL byte and is not valid`);
|
|
28
|
+
}
|
|
29
|
+
const root = path.resolve(configDir);
|
|
30
|
+
const resolved = path.resolve(configDir, destSubpath);
|
|
31
|
+
if (resolved === root || !resolved.startsWith(root + path.sep)) {
|
|
32
|
+
throw new Error(`destSubpath "${destSubpath}" must be a strict subpath of configHome "${configDir}" — not configHome itself or outside it (escapes configHome)`);
|
|
33
|
+
}
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
12
36
|
function errorMessage(err) {
|
|
13
37
|
if (err instanceof Error)
|
|
14
38
|
return err.message;
|
|
@@ -36,10 +60,34 @@ function createRuntimeArtifactInstallPlan(args) {
|
|
|
36
60
|
platform,
|
|
37
61
|
resolveAttribution,
|
|
38
62
|
};
|
|
63
|
+
// ADR-1235 §1: build agentCtx once per plan so agents kind entries can apply
|
|
64
|
+
// the CORRECT pre-converter cross-cutting (path rewrites → attribution → converter
|
|
65
|
+
// → normalize). This mirrors the exact per-file order in the inline agent loop
|
|
66
|
+
// in bin/install.js (lines 9330-9415). agentCtx is passed as the second arg
|
|
67
|
+
// to kind.stage() for agents kind entries with a converter (convertedAgentsKind).
|
|
68
|
+
// NO _stampNonClaudeRuntimeDefaults — agents are NOT stamped in the inline loop.
|
|
69
|
+
const os = _require('node:os');
|
|
70
|
+
const homedirFn = homedir ?? (() => os.homedir());
|
|
71
|
+
const resolvedTarget = path.resolve(layout.configDir).replace(/\\/g, '/');
|
|
72
|
+
const homeDir = homedirFn().replace(/\\/g, '/');
|
|
73
|
+
const isGlobal = scope === 'global';
|
|
74
|
+
const isOpencode = layout.runtime === 'opencode';
|
|
75
|
+
const isWindowsHost = (platform ?? process.platform) === 'win32';
|
|
76
|
+
const pathPrefix = conversionExports._computePathPrefix({ isGlobal, isOpencode, isWindowsHost, resolvedTarget, homeDir });
|
|
77
|
+
const attribution = resolveAttribution ? resolveAttribution(layout.runtime) : undefined;
|
|
78
|
+
const agentCtx = { runtime: layout.runtime, pathPrefix, attribution };
|
|
39
79
|
for (const kind of layout.kinds) {
|
|
40
80
|
let stagedDir;
|
|
41
81
|
try {
|
|
42
|
-
|
|
82
|
+
if (kind.kind === 'agents') {
|
|
83
|
+
// ADR-1235 §1: pass agentCtx so stageAgentsForRuntimeWithConverter applies
|
|
84
|
+
// the full inline-loop order: pathRewrites → attribution → converter → normalize.
|
|
85
|
+
// The cross-cutting is now PRE-converter (inside staging), not POST.
|
|
86
|
+
stagedDir = kind.stage(resolvedProfile, agentCtx);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
stagedDir = kind.stage(resolvedProfile);
|
|
90
|
+
}
|
|
43
91
|
}
|
|
44
92
|
catch (err) {
|
|
45
93
|
return { ok: false, kind: 'stage_failed', message: errorMessage(err), cleanupDirs, failedKind: kind.kind };
|
|
@@ -54,6 +102,8 @@ function createRuntimeArtifactInstallPlan(args) {
|
|
|
54
102
|
const rewrittenDir = rewriteStagedSkillBodies(stagedDir, rewriteOpts);
|
|
55
103
|
sourceDir = addCleanupDir(cleanupDirs, stagedDir, rewrittenDir);
|
|
56
104
|
}
|
|
105
|
+
// agents kind: cross-cutting already applied INSIDE kind.stage() via agentCtx.
|
|
106
|
+
// No POST-step needed. sourceDir stays as stagedDir.
|
|
57
107
|
}
|
|
58
108
|
catch (err) {
|
|
59
109
|
return { ok: false, kind: 'rewrite_failed', message: errorMessage(err), cleanupDirs, failedKind: kind.kind };
|
|
@@ -61,7 +111,7 @@ function createRuntimeArtifactInstallPlan(args) {
|
|
|
61
111
|
items.push({
|
|
62
112
|
kind: kind.kind,
|
|
63
113
|
sourceDir,
|
|
64
|
-
destDir:
|
|
114
|
+
destDir: assertDestWithinConfigHome(layout.configDir, kind.destSubpath),
|
|
65
115
|
});
|
|
66
116
|
}
|
|
67
117
|
return { ok: true, plan: { items, cleanupDirs } };
|
|
@@ -70,8 +120,8 @@ function createRuntimeArtifactUninstallPlan(layout) {
|
|
|
70
120
|
return {
|
|
71
121
|
items: layout.kinds.map((kind) => ({
|
|
72
122
|
kind: kind.kind,
|
|
73
|
-
destDir:
|
|
123
|
+
destDir: assertDestWithinConfigHome(layout.configDir, kind.destSubpath),
|
|
74
124
|
})),
|
|
75
125
|
};
|
|
76
126
|
}
|
|
77
|
-
module.exports = { createRuntimeArtifactInstallPlan, createRuntimeArtifactUninstallPlan };
|
|
127
|
+
module.exports = { assertDestWithinConfigHome, createRuntimeArtifactInstallPlan, createRuntimeArtifactUninstallPlan };
|
|
@@ -156,13 +156,16 @@ function convertedAgentsKind(destSubpath, prefix, converterName, configDir, scop
|
|
|
156
156
|
kind: 'agents',
|
|
157
157
|
destSubpath,
|
|
158
158
|
prefix,
|
|
159
|
-
stage: (resolved) => {
|
|
159
|
+
stage: (resolved, agentCtx) => {
|
|
160
160
|
// isGlobal is threaded so scope-aware agent converters (copilot, antigravity)
|
|
161
161
|
// choose global-home vs workspace-relative paths; converters that only take
|
|
162
162
|
// (content) ignore the extra positional arg. Mirrors skillsKind's scope
|
|
163
163
|
// threading (#1173).
|
|
164
164
|
const converter = conversionExports[converterName];
|
|
165
|
-
|
|
165
|
+
// ADR-1235 §1: when agentCtx is provided (by createRuntimeArtifactInstallPlan
|
|
166
|
+
// for descriptor-driven runtimes), thread it through so stageAgentsForRuntimeWithConverter
|
|
167
|
+
// can apply the full pre-converter + post-converter sequence in the correct order.
|
|
168
|
+
return stageAgentsForRuntimeWithConverter(findAgentsSourceRoot(configDir), resolved, converter, scope === 'global', agentCtx);
|
|
166
169
|
},
|
|
167
170
|
};
|
|
168
171
|
}
|
|
@@ -84,7 +84,7 @@ function atomicWriteFileSync(target, data, options) {
|
|
|
84
84
|
__atomicWrittenTmps.add(tmp);
|
|
85
85
|
try {
|
|
86
86
|
node_fs_1.default.writeFileSync(tmp, data, options);
|
|
87
|
-
|
|
87
|
+
shellCmdProjection.retryRenameSync(tmp, target);
|
|
88
88
|
// Successful rename: the tmp path no longer exists, but leave it in the
|
|
89
89
|
// Set so _cleanTmpFiles can recognise it as installer-owned if it somehow
|
|
90
90
|
// lingers (e.g. a rename succeeded but left a stale entry on some FS).
|