@opengsd/gsd-core 1.7.0-rc.5 → 1.7.0-rc.6
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/agents/gsd-ui-checker.md +2 -0
- package/agents/gsd-ui-researcher.md +1 -0
- package/bin/install.js +970 -192
- package/gsd-core/bin/lib/capability-registry.cjs +501 -85
- package/gsd-core/bin/lib/capability-validator.cjs +56 -18
- package/gsd-core/bin/lib/host-integration.cjs +33 -8
- package/gsd-core/bin/lib/init.cjs +33 -40
- package/gsd-core/bin/lib/install-engine.cjs +90 -20
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/mcp-server.cjs +18 -7
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +235 -38
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +22 -13
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +22 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +501 -12
- package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
- package/gsd-core/bin/lib/security.cjs +6 -36
- package/gsd-core/bin/lib/shell-command-projection.cjs +115 -2
- package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
- package/gsd-core/bin/lib/surface.cjs +10 -6
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -3
- package/gsd-core/references/ui-consideration-probe.md +73 -0
- package/gsd-core/templates/UI-SPEC.md +25 -0
- package/gsd-core/templates/VALIDATION.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +7 -4
- package/gsd-core/workflows/plan-phase.md +6 -0
- package/gsd-core/workflows/settings-advanced.md +7 -4
- package/gsd-core/workflows/ui-phase.md +146 -1
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
- package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-windsurf-pre-command.js +275 -0
- package/hooks/gsd-windsurf-pre-write.js +132 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +8 -4
- package/pi/gsd.cjs +354 -0
- package/scripts/build-hooks.js +3 -0
- package/scripts/gen-golden-install-parity-zcode.cjs +11 -1
- package/scripts/gen-registry.cjs +128 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/registry-schema.cjs +565 -0
- package/scripts/validate-registry.cjs +117 -0
- package/vscode/browser.js +197 -0
- package/vscode/extension.js +383 -0
- package/vscode/host-binding.js +113 -0
- package/vscode/package.json +96 -0
|
@@ -15,6 +15,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
15
15
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
16
|
};
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.NO_LOCAL_CONFIG_DIR_SENTINEL = void 0;
|
|
18
19
|
exports.canonicalizeRuntimeName = canonicalizeRuntimeName;
|
|
19
20
|
exports.resolveRuntimeNameFromCandidates = resolveRuntimeNameFromCandidates;
|
|
20
21
|
exports.getProjectInstructionFile = getProjectInstructionFile;
|
|
@@ -132,7 +133,15 @@ function resolveRuntimeNameFromCandidates(...candidates) {
|
|
|
132
133
|
* Aliases are normalized via `canonicalizeRuntimeName` first, so inputs like
|
|
133
134
|
* `codex-cli` resolve to `codex` → `AGENTS.md`. Replaces the prior codex-only
|
|
134
135
|
* override in profile-output.cjs (#3163) which left AGENTS-native runtimes
|
|
135
|
-
* (opencode/kilo/kimi) incorrectly emitting `.claude/CLAUDE.md`. Pure: no I/O
|
|
136
|
+
* (opencode/kilo/kimi) incorrectly emitting `.claude/CLAUDE.md`. Pure: no I/O
|
|
137
|
+
* (the lazy `require` below reads a static generated module, not the disk).
|
|
138
|
+
*
|
|
139
|
+
* Descriptor-driven (ADR-1239 / #2096): antigravity's `GEMINI.md` is folded
|
|
140
|
+
* from a hardcoded `canonical === 'antigravity'` literal into a read of
|
|
141
|
+
* `runtime.hostBehaviors.projectInstructionFile`. claude/copilot stay
|
|
142
|
+
* hardcoded (out of scope here) mirroring `getDirName` below, which already
|
|
143
|
+
* lazy-`require`s `capability-registry.cjs` inside the function body to
|
|
144
|
+
* avoid a circular dependency at module load.
|
|
136
145
|
*/
|
|
137
146
|
function getProjectInstructionFile(runtime) {
|
|
138
147
|
const canonical = canonicalizeRuntimeName(runtime);
|
|
@@ -140,17 +149,46 @@ function getProjectInstructionFile(runtime) {
|
|
|
140
149
|
return '.claude/CLAUDE.md';
|
|
141
150
|
if (canonical === 'copilot')
|
|
142
151
|
return '.github/copilot-instructions.md';
|
|
143
|
-
|
|
144
|
-
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
153
|
+
const { runtimes } = require('./capability-registry.cjs');
|
|
154
|
+
const declared = canonical ? runtimes[canonical]?.runtime?.hostBehaviors?.projectInstructionFile : undefined;
|
|
155
|
+
if (typeof declared === 'string' && declared.length > 0)
|
|
156
|
+
return declared;
|
|
145
157
|
// codex, opencode, kilo, kimi, AND unknown/future runtimes all default to
|
|
146
158
|
// root AGENTS.md (the safe cross-agent instruction file).
|
|
147
159
|
return 'AGENTS.md';
|
|
148
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Sentinel returned by {@link getDirName} for a runtime whose
|
|
163
|
+
* `runtime.configHome.kind === 'none'` (#2103 — a Marketplace/VSIX-distributed
|
|
164
|
+
* host with NO file-projected config directory at all, e.g. VS Code).
|
|
165
|
+
*
|
|
166
|
+
* A plain fallback to `.claude` would be actively wrong here — it would read
|
|
167
|
+
* as "this runtime installs into .claude", which is false. This sentinel is
|
|
168
|
+
* a string (not `null`) so `getDirName`'s return type and every existing
|
|
169
|
+
* template-literal call site (`` `${getDirName(runtime)}` `` in bin/install.js
|
|
170
|
+
* / runtime-artifact-conversion.cjs / install-engine.cjs) are unaffected —
|
|
171
|
+
* widening the return type to `string | null` would require auditing every
|
|
172
|
+
* call site for a null-check, which is out of scope for a runtime that is
|
|
173
|
+
* never actually dispatched through those installer paths (vscode has no
|
|
174
|
+
* install surface — see capabilities/vscode/capability.json). The value is
|
|
175
|
+
* deliberately NOT a plausible dot-dir name (parens are not valid in a
|
|
176
|
+
* directory-name token GSD would ever generate) so a future caller that
|
|
177
|
+
* mistakenly interpolates it into a path fails obviously rather than
|
|
178
|
+
* silently colliding with a real directory.
|
|
179
|
+
*/
|
|
180
|
+
exports.NO_LOCAL_CONFIG_DIR_SENTINEL = '(no-local-config-dir)';
|
|
149
181
|
/**
|
|
150
182
|
* Map a canonical runtime id to its on-disk local config directory name
|
|
151
183
|
* (e.g. `cursor` -> `.cursor`, `windsurf` -> `.windsurf`). Unknown/empty inputs
|
|
152
184
|
* fall back to `.claude`.
|
|
153
185
|
*
|
|
186
|
+
* #2103: a runtime whose descriptor declares `configHome.kind === 'none'`
|
|
187
|
+
* (no file-projected config directory at all) returns
|
|
188
|
+
* {@link NO_LOCAL_CONFIG_DIR_SENTINEL} instead of falling through to
|
|
189
|
+
* `.claude` — it has no local config dir, and `.claude` would be a wrong
|
|
190
|
+
* answer, not just an imprecise one.
|
|
191
|
+
*
|
|
154
192
|
* Pure runtime-identity projection. Relocated from `bin/install.js` per
|
|
155
193
|
* ADR-1508 (epic #1507, #1510 Phase 1) so the Runtime Artifact Conversion
|
|
156
194
|
* Module's rewrite engine can consume it without importing the installer.
|
|
@@ -161,9 +199,12 @@ function getDirName(runtime) {
|
|
|
161
199
|
return '.claude';
|
|
162
200
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
163
201
|
const { runtimes } = require('./capability-registry.cjs');
|
|
164
|
-
const
|
|
202
|
+
const entry = runtimes[runtime]?.runtime;
|
|
203
|
+
const dir = entry?.localConfigDir;
|
|
165
204
|
if (typeof dir === 'string' && dir.length > 0)
|
|
166
205
|
return dir;
|
|
206
|
+
if (entry?.configHome?.kind === 'none')
|
|
207
|
+
return exports.NO_LOCAL_CONFIG_DIR_SENTINEL;
|
|
167
208
|
return '.claude';
|
|
168
209
|
}
|
|
169
210
|
/**
|
|
@@ -210,6 +251,12 @@ const RUNTIME_LABELS = {
|
|
|
210
251
|
codebuddy: 'CodeBuddy',
|
|
211
252
|
cline: 'Cline',
|
|
212
253
|
zcode: 'ZCode',
|
|
254
|
+
pi: 'pi',
|
|
255
|
+
// #2103: vscode is a registered (role:runtime) capability for validator +
|
|
256
|
+
// host-integration coverage, even though it is never CLI-installed (no
|
|
257
|
+
// --vscode flag — see NON_INSTALLABLE_RUNTIMES in tests/runtime-flags.test.cjs).
|
|
258
|
+
// A distinct label is still required by the drift guard below.
|
|
259
|
+
vscode: 'VS Code',
|
|
213
260
|
};
|
|
214
261
|
/**
|
|
215
262
|
* Map a canonical runtime id to its short display label for the
|
|
@@ -257,6 +304,12 @@ const GLOBAL_CONFIG_HOME_FRAGMENTS = {
|
|
|
257
304
|
cline: "'.cline'",
|
|
258
305
|
kimi: "'.config', 'agents'",
|
|
259
306
|
zcode: "'.zcode'",
|
|
307
|
+
// pi's global config home is ~/.pi/agent (configHome: dot-home-nested,
|
|
308
|
+
// parent '.pi', name 'agent' — capabilities/pi/capability.json), matching
|
|
309
|
+
// resolveConfigHomeFromDescriptor's `path.join(home, parent, name)` for the
|
|
310
|
+
// no-probe dot-home-nested case (src/runtime-homes.cts). Two-segment
|
|
311
|
+
// path.join args, same shape as opencode/kilo/kimi above.
|
|
312
|
+
pi: "'.pi', 'agent'",
|
|
260
313
|
};
|
|
261
314
|
/**
|
|
262
315
|
* Return the global config-home path-fragment source snippet for a runtime
|
|
@@ -277,9 +330,14 @@ function getGlobalConfigHomeFragment(runtime) {
|
|
|
277
330
|
* function declaration block (the add-a-host tax ADR-1239 Phase B / #1679 AC2
|
|
278
331
|
* removes).
|
|
279
332
|
*/
|
|
333
|
+
// #2094: 'trae' stays here — bin/install.js's agents-converter dispatch
|
|
334
|
+
// (convertClaudeAgentToTraeAgent selection) still reads isTrae directly.
|
|
335
|
+
// Removing it is gated on migrating that runtime-keyed `else if` chain to a
|
|
336
|
+
// cross-runtime agents-dispatch table (out of scope for #2094, which only
|
|
337
|
+
// folds the shared-hooks-install skip).
|
|
280
338
|
const RUNTIME_FLAG_IDS = Object.freeze([
|
|
281
339
|
'opencode', 'kilo', 'codex', 'copilot', 'antigravity', 'cursor',
|
|
282
|
-
'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi', 'zcode',
|
|
340
|
+
'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi', 'zcode', 'pi',
|
|
283
341
|
]);
|
|
284
342
|
/**
|
|
285
343
|
* Return a frozen map of `is<Runtime>` boolean predicates for the given runtime
|
|
@@ -34,7 +34,6 @@ exports.safeJsonParse = safeJsonParse;
|
|
|
34
34
|
exports.validatePhaseNumber = validatePhaseNumber;
|
|
35
35
|
exports.validateFieldName = validateFieldName;
|
|
36
36
|
exports.validatePromptStructure = validatePromptStructure;
|
|
37
|
-
exports.scanEntropyAnomalies = scanEntropyAnomalies;
|
|
38
37
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
39
38
|
const node_os_1 = __importDefault(require("node:os"));
|
|
40
39
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -443,38 +442,9 @@ function validatePromptStructure(text, fileType) {
|
|
|
443
442
|
}
|
|
444
443
|
return { valid: violations.length === 0, violations };
|
|
445
444
|
}
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
freq[ch] = (freq[ch] || 0) + 1;
|
|
453
|
-
}
|
|
454
|
-
const len = text.length;
|
|
455
|
-
let entropy = 0;
|
|
456
|
-
for (const count of Object.values(freq)) {
|
|
457
|
-
const p = count / len;
|
|
458
|
-
entropy -= p * Math.log2(p);
|
|
459
|
-
}
|
|
460
|
-
return entropy;
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Scan text for paragraphs with anomalously high Shannon entropy.
|
|
464
|
-
*/
|
|
465
|
-
function scanEntropyAnomalies(text) {
|
|
466
|
-
if (!text || typeof text !== 'string') {
|
|
467
|
-
return { clean: true, findings: [] };
|
|
468
|
-
}
|
|
469
|
-
const findings = [];
|
|
470
|
-
const paragraphs = text.split(/\n\n+/);
|
|
471
|
-
for (const para of paragraphs) {
|
|
472
|
-
if (para.length <= 50)
|
|
473
|
-
continue;
|
|
474
|
-
const entropy = shannonEntropy(para);
|
|
475
|
-
if (entropy > 5.5) {
|
|
476
|
-
findings.push(`High-entropy paragraph detected (${entropy.toFixed(2)} bits/char) — possible encoded payload`);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
return { clean: findings.length === 0, findings };
|
|
480
|
-
}
|
|
445
|
+
// NOTE (#2198): scanEntropyAnomalies + shannonEntropy were removed as dead exports.
|
|
446
|
+
// They had zero production callers — the live hooks (gsd-prompt-guard.js,
|
|
447
|
+
// gsd-read-injection-scanner.js) inline their own pattern subsets for hook
|
|
448
|
+
// independence and never called these functions. scanForInjection is retained
|
|
449
|
+
// below: it serves as the CI codebase-scanner engine
|
|
450
|
+
// (tests/prompt-injection-scan.security.test.cjs), not as a live hook.
|
|
@@ -38,6 +38,8 @@ exports.projectPersistentPathExportActions = projectPersistentPathExportActions;
|
|
|
38
38
|
exports.execGit = execGit;
|
|
39
39
|
exports.execNpm = execNpm;
|
|
40
40
|
exports.execTool = execTool;
|
|
41
|
+
exports.resolveGsdToolsPath = resolveGsdToolsPath;
|
|
42
|
+
exports.dispatchGsdCommand = dispatchGsdCommand;
|
|
41
43
|
exports.probeTty = probeTty;
|
|
42
44
|
exports.normalizeContent = normalizeContent;
|
|
43
45
|
exports.retryRenameSync = retryRenameSync;
|
|
@@ -122,10 +124,18 @@ function formatManagedHookScriptToken(scriptPath, opts = {}) {
|
|
|
122
124
|
return null;
|
|
123
125
|
return JSON.stringify(scriptPath.replace(/\\/g, '/'));
|
|
124
126
|
}
|
|
125
|
-
function projectLocalHookPrefix({ runtime = 'claude', dirName }) {
|
|
127
|
+
function projectLocalHookPrefix({ runtime: _runtime = 'claude', dirName, hookPathStyle }) {
|
|
126
128
|
if (!dirName)
|
|
127
129
|
return dirName;
|
|
128
|
-
|
|
130
|
+
// Descriptor-driven (ADR-1239 / #2096): folded from a hardcoded
|
|
131
|
+
// `runtime === 'antigravity'` literal into the runtime's declared
|
|
132
|
+
// `hostBehaviors.hookPathStyle`. Runtimes that always run project hooks
|
|
133
|
+
// with the project dir as cwd (Antigravity today) declare 'raw' and get
|
|
134
|
+
// the bare dirName; every other runtime keeps the $CLAUDE_PROJECT_DIR-
|
|
135
|
+
// anchored prefix. `runtime` itself is now unused here but stays in the
|
|
136
|
+
// signature for call-site/back-compat parity (kept `_`-prefixed to
|
|
137
|
+
// satisfy no-unused-vars).
|
|
138
|
+
return (hookPathStyle === 'raw')
|
|
129
139
|
? dirName
|
|
130
140
|
: `"$CLAUDE_PROJECT_DIR"/${dirName}`;
|
|
131
141
|
}
|
|
@@ -461,6 +471,109 @@ function execTool(program, args, opts = {}) {
|
|
|
461
471
|
});
|
|
462
472
|
return _spawnResult(result, program);
|
|
463
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Resolve the absolute path to gsd-tools.cjs relative to THIS module.
|
|
476
|
+
*
|
|
477
|
+
* This file compiles to gsd-core/bin/lib/shell-command-projection.cjs — a
|
|
478
|
+
* sibling of gsd-core/bin/gsd-tools.cjs — so the relative walk-up is stable
|
|
479
|
+
* regardless of install location (global/local/dev-repo layouts all ship
|
|
480
|
+
* gsd-core/bin/ as a unit).
|
|
481
|
+
*/
|
|
482
|
+
function resolveGsdToolsPath() {
|
|
483
|
+
return node_path_1.default.resolve(__dirname, '..', 'gsd-tools.cjs');
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Subprocess-shim dispatch to gsd-tools.cjs (ADR-1239 #2102 Stage 2).
|
|
487
|
+
*
|
|
488
|
+
* No fully-populated in-process command-routing hub exists anywhere in the
|
|
489
|
+
* tree — every `createHub()` caller (cjs-command-router-adapter.cts,
|
|
490
|
+
* phase-command-router.cts, command-routing-hub.cts's own tests) builds a
|
|
491
|
+
* single-family hub for its own narrow purpose. The ONLY dispatch path that
|
|
492
|
+
* covers the FULL family/subcommand surface is the gsd-tools.cjs CLI itself.
|
|
493
|
+
* This mirrors the SUBPROCESS-REUSE precedent already established for the
|
|
494
|
+
* OpenCode/Kilo hook bridge (see .opencode/plugins/gsd-core.js header:
|
|
495
|
+
* "Architecture: SUBPROCESS REUSE ... spawns existing hook scripts as child
|
|
496
|
+
* processes") — the same pattern, applied to command dispatch instead of
|
|
497
|
+
* hook dispatch.
|
|
498
|
+
*
|
|
499
|
+
* Output-flag choice (verified by direct invocation — see #2102 dispatch
|
|
500
|
+
* notes for the sample invocations): always pass `--raw` (undecorated,
|
|
501
|
+
* programmatically-consumable stdout on success) and `--json-errors` (a
|
|
502
|
+
* structured `{ok:false,reason,message}` JSON object on stderr, with a
|
|
503
|
+
* non-zero exit, instead of a free-text "Error: ..." line). Both are global
|
|
504
|
+
* flags accepted by every gsd-tools.cjs family/subcommand, so passing them
|
|
505
|
+
* unconditionally is safe for the full command surface.
|
|
506
|
+
*
|
|
507
|
+
* `family` maps 1:1 onto gsd-tools.cjs's first positional argv token;
|
|
508
|
+
* `subcommand` (when present) onto the second — e.g.
|
|
509
|
+
* `{family:'phase', subcommand:'add'}` → `gsd-tools.cjs phase add`. An empty
|
|
510
|
+
* `subcommand` is omitted entirely (some families, e.g. `config-path`, take
|
|
511
|
+
* no subcommand).
|
|
512
|
+
*
|
|
513
|
+
* NEVER throws. Degrades to `{ ok:false, ... }` on:
|
|
514
|
+
* - a missing/invalid "family" (validated locally, no subprocess spawned)
|
|
515
|
+
* - ENOENT / a missing gsd-tools.cjs (via the injectable `gsdToolsPath`)
|
|
516
|
+
* - a wall-clock timeout (`timedOut:true`, mirroring the
|
|
517
|
+
* `signal === 'SIGTERM' && error.code === 'ETIMEDOUT'` idiom already used
|
|
518
|
+
* by worktree-safety.cts)
|
|
519
|
+
* - any other unanticipated throw from the underlying spawn (defensive
|
|
520
|
+
* try/catch — execTool itself is spawnSync-based and does not throw).
|
|
521
|
+
*/
|
|
522
|
+
function dispatchGsdCommand({ family, subcommand, args = [], cwd, timeout = 30_000, gsdToolsPath, } = {}) {
|
|
523
|
+
if (typeof family !== 'string' || family.length === 0) {
|
|
524
|
+
return {
|
|
525
|
+
ok: false,
|
|
526
|
+
stdout: '',
|
|
527
|
+
stderr: 'dispatchGsdCommand requires a non-empty string "family".',
|
|
528
|
+
code: null,
|
|
529
|
+
timedOut: false,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const resolvedCwd = cwd || process.cwd();
|
|
533
|
+
const toolsPath = gsdToolsPath || resolveGsdToolsPath();
|
|
534
|
+
const argv = [
|
|
535
|
+
toolsPath,
|
|
536
|
+
family,
|
|
537
|
+
...(subcommand ? [subcommand] : []),
|
|
538
|
+
...(Array.isArray(args) ? args : []),
|
|
539
|
+
'--cwd', resolvedCwd,
|
|
540
|
+
'--raw',
|
|
541
|
+
'--json-errors',
|
|
542
|
+
];
|
|
543
|
+
let result;
|
|
544
|
+
try {
|
|
545
|
+
result = execTool(process.execPath, argv, { cwd: resolvedCwd, timeout });
|
|
546
|
+
}
|
|
547
|
+
catch (e) {
|
|
548
|
+
// Defensive belt-and-suspenders: execTool is spawnSync-based and does not
|
|
549
|
+
// throw today, but a degraded result here keeps this seam's no-throw
|
|
550
|
+
// contract true even under an unanticipated future failure mode.
|
|
551
|
+
return {
|
|
552
|
+
ok: false,
|
|
553
|
+
stdout: '',
|
|
554
|
+
stderr: e instanceof Error ? e.message : String(e),
|
|
555
|
+
code: null,
|
|
556
|
+
timedOut: false,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
// Mirrors the established `result.error && (result.error as
|
|
560
|
+
// NodeJS.ErrnoException).code === ...` idiom (graphify.cts, worktree-safety.cts):
|
|
561
|
+
// narrow away null via `!== null` FIRST, then cast — asserting `Error | null`
|
|
562
|
+
// to `NodeJS.ErrnoException | null` directly (paired with optional chaining)
|
|
563
|
+
// trips a typescript-eslint no-unnecessary-type-assertion false positive for
|
|
564
|
+
// this exact narrowing shape (all of ErrnoException's extra fields over Error
|
|
565
|
+
// are optional).
|
|
566
|
+
const timedOut = result.signal === 'SIGTERM'
|
|
567
|
+
&& result.error !== null
|
|
568
|
+
&& result.error.code === 'ETIMEDOUT';
|
|
569
|
+
return {
|
|
570
|
+
ok: result.exitCode === 0 && !timedOut,
|
|
571
|
+
stdout: result.stdout,
|
|
572
|
+
stderr: result.stderr,
|
|
573
|
+
code: result.exitCode,
|
|
574
|
+
timedOut,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
464
577
|
function probeTty(opts = {}) {
|
|
465
578
|
const platform = opts.platform ?? process.platform;
|
|
466
579
|
if (platform === 'win32')
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Stale-bake guard for static-frontmatter runtimes (#1688, follow-up to #1650).
|
|
5
5
|
*
|
|
6
|
-
* Runtimes `codex` and `
|
|
7
|
-
* static config at install time (bin/install.js
|
|
8
|
-
* ~10008-10026 for opencode
|
|
6
|
+
* Runtimes `codex`, `opencode`, and (since #2093) `kilo` bake the resolved
|
|
7
|
+
* model ID into each agent's static config at install time (bin/install.js
|
|
8
|
+
* ~5667-5767 for codex, ~10008-10026 for opencode, and the adjacent kilo
|
|
9
|
+
* branch added by #2093). Their task/spawn_agent interfaces do not accept
|
|
9
10
|
* an inline `model` parameter, so editing `model_overrides` in
|
|
10
11
|
* `.planning/config.json` or `~/.gsd/defaults.json` has NO effect until the
|
|
11
12
|
* user re-runs `gsd install <runtime>` (or `gsd update`). The failure is
|
|
@@ -25,9 +26,17 @@ const os = require('os');
|
|
|
25
26
|
* Runtimes whose agent config is static frontmatter/TOML baked at install time.
|
|
26
27
|
* MUST stay in sync with the bake paths in bin/install.js. The parity test in
|
|
27
28
|
* tests/stale-bake-guard.test.cjs asserts this matches the runtimes that
|
|
28
|
-
* actually emit a baked model: line id #2256 (opencode)
|
|
29
|
+
* actually emit a baked model: line id #2256 (opencode), #49/#2256 (codex),
|
|
30
|
+
* and #2093 (kilo).
|
|
29
31
|
*/
|
|
30
|
-
const STATIC_FRONTMATTER_RUNTIMES = Object.freeze(['codex', 'opencode']);
|
|
32
|
+
const STATIC_FRONTMATTER_RUNTIMES = Object.freeze(['codex', 'kilo', 'opencode']);
|
|
33
|
+
|
|
34
|
+
/** Per-runtime `gsd install` flag, for the remediation hint in the warning. */
|
|
35
|
+
const INSTALL_FLAG_BY_RUNTIME = Object.freeze({
|
|
36
|
+
codex: '--codex',
|
|
37
|
+
opencode: '--opencode',
|
|
38
|
+
kilo: '--kilo',
|
|
39
|
+
});
|
|
31
40
|
|
|
32
41
|
const _warnedKeys = new Set();
|
|
33
42
|
|
|
@@ -55,7 +64,7 @@ function formatStaleBakeWarning({ runtime, configPath, configMtimeMs, agentMtime
|
|
|
55
64
|
const signal = detectStaleBake({ runtime, configMtimeMs, agentMtimeMs });
|
|
56
65
|
if (!signal) return '';
|
|
57
66
|
const configDate = new Date(configMtimeMs).toISOString();
|
|
58
|
-
const installFlag = runtime
|
|
67
|
+
const installFlag = INSTALL_FLAG_BY_RUNTIME[runtime] || `--${runtime}`;
|
|
59
68
|
return [
|
|
60
69
|
`gsd: model config in ${configPath} changed since agents were last baked (${configDate}).`,
|
|
61
70
|
` Static-frontmatter runtime '${runtime}' ignores the new model_overrides`,
|
|
@@ -79,13 +88,24 @@ function resolveRuntimeFromConfig(config) {
|
|
|
79
88
|
|
|
80
89
|
/**
|
|
81
90
|
* Resolve the install root for a runtime's agent files, honoring the same env
|
|
82
|
-
* vars the installer does (CODEX_HOME, OPENCODE_CONFIG_DIR).
|
|
83
|
-
* absolute directory or `null` for unsupported runtimes.
|
|
91
|
+
* vars the installer does (CODEX_HOME, OPENCODE_CONFIG_DIR, KILO_CONFIG_DIR).
|
|
92
|
+
* Returns the absolute directory or `null` for unsupported runtimes.
|
|
84
93
|
*/
|
|
85
94
|
function resolveAgentDir(runtime, { env = process.env, homedir = os.homedir } = {}) {
|
|
86
95
|
if (runtime === 'opencode') {
|
|
87
96
|
const base = (env.OPENCODE_CONFIG_DIR && String(env.OPENCODE_CONFIG_DIR).trim()) || path.join(homedir(), '.config', 'opencode');
|
|
88
|
-
|
|
97
|
+
// #2093 fix: the installer writes agents to `<base>/agents` (plural — see
|
|
98
|
+
// bin/install.js's universal `agentsDest = path.join(targetDir, 'agents')`,
|
|
99
|
+
// verified against a live `--opencode --global` install). The prior
|
|
100
|
+
// singular `agent` never matched the real install output, so this guard's
|
|
101
|
+
// `findOldestAgentMtime` always hit ENOENT and warnIfStaleBake was a
|
|
102
|
+
// silent no-op for opencode in production — discovered while wiring the
|
|
103
|
+
// parallel kilo entry below (same bake mechanism, #2093).
|
|
104
|
+
return path.join(base, 'agents');
|
|
105
|
+
}
|
|
106
|
+
if (runtime === 'kilo') {
|
|
107
|
+
const base = (env.KILO_CONFIG_DIR && String(env.KILO_CONFIG_DIR).trim()) || path.join(homedir(), '.config', 'kilo');
|
|
108
|
+
return path.join(base, 'agents');
|
|
89
109
|
}
|
|
90
110
|
if (runtime === 'codex') {
|
|
91
111
|
const base = (env.CODEX_HOME && String(env.CODEX_HOME).trim()) || path.join(homedir(), '.codex');
|
|
@@ -148,7 +168,7 @@ function findOldestAgentMtime(runtime, { env = process.env, homedir = os.homedir
|
|
|
148
168
|
for (const entry of entries) {
|
|
149
169
|
if (!entry.isFile()) continue;
|
|
150
170
|
if (!entry.name.startsWith('gsd-')) continue;
|
|
151
|
-
const isAgentFile = (runtime === 'opencode' && entry.name.endsWith('.md'))
|
|
171
|
+
const isAgentFile = ((runtime === 'opencode' || runtime === 'kilo') && entry.name.endsWith('.md'))
|
|
152
172
|
|| (runtime === 'codex' && (entry.name.endsWith('.toml') || entry.name.endsWith('.md')));
|
|
153
173
|
if (!isAgentFile) continue;
|
|
154
174
|
try {
|
|
@@ -472,10 +472,14 @@ function _syncGsdDir(stagedDir, destDir, kind, manifest, runtime) {
|
|
|
472
472
|
// Normalize: allow legacy string context for backward-compat with internal callers
|
|
473
473
|
const kindName = (typeof kind === 'string') ? kind : kind.kind;
|
|
474
474
|
const kindPrefix = (typeof kind === 'object' && kind !== null) ? kind.prefix : 'gsd-';
|
|
475
|
-
// #1575:
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
|
|
475
|
+
// #1575 / #2103: agent files are renamed .md -> <agentFileExtension> at copy
|
|
476
|
+
// time when the runtime's descriptor declares hostBehaviors.agentFileExtension
|
|
477
|
+
// (e.g. copilot's '.agent.md'), mirroring install-engine.cts's staged-copy
|
|
478
|
+
// loop (`_copyStaged`) — ONE descriptor read shared by both surfaces instead
|
|
479
|
+
// of a duplicated hardcoded `runtime === 'copilot'` literal. Other runtimes
|
|
480
|
+
// (no agentFileExtension declared) keep the staged filename verbatim.
|
|
481
|
+
const _agentExt = runtime ? runtimeArtifactConversion.agentFileExtensionFor(runtime) : undefined;
|
|
482
|
+
const isRenamedAgents = !!_agentExt && kindName === 'agents';
|
|
479
483
|
if (kindName === 'skills') {
|
|
480
484
|
// Skills kind: work with directories, not files.
|
|
481
485
|
// Each staged entry is a directory named ${prefix}${stem}.
|
|
@@ -510,8 +514,8 @@ function _syncGsdDir(stagedDir, destDir, kind, manifest, runtime) {
|
|
|
510
514
|
const stagedFiles = node_fs_1.default.readdirSync(stagedDir).filter(f => f.endsWith('.md'));
|
|
511
515
|
const stagedDestNames = new Set();
|
|
512
516
|
for (const file of stagedFiles) {
|
|
513
|
-
const destName =
|
|
514
|
-
? file.replace(/\.md$/,
|
|
517
|
+
const destName = isRenamedAgents
|
|
518
|
+
? file.replace(/\.md$/, _agentExt)
|
|
515
519
|
: (kindName === 'agents' || namespacedByDir)
|
|
516
520
|
? file
|
|
517
521
|
: `${kindPrefix}${file.slice(0, -3)}.md`;
|