@opengsd/gsd-core 1.7.0-rc.5 → 1.7.0
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-executor.md +2 -1
- package/agents/gsd-security-auditor.md +13 -15
- package/agents/gsd-ui-checker.md +2 -0
- package/agents/gsd-ui-researcher.md +1 -0
- package/bin/install.js +975 -196
- package/commands/gsd/mempalace-capture.md +27 -1
- package/commands/gsd/surface.md +6 -6
- package/gsd-core/bin/gsd-tools.cjs +63 -2
- package/gsd-core/bin/lib/api-coverage.cjs +3 -4
- package/gsd-core/bin/lib/audit.cjs +7 -6
- package/gsd-core/bin/lib/capability-registry.cjs +503 -87
- package/gsd-core/bin/lib/capability-validator.cjs +56 -18
- package/gsd-core/bin/lib/check-command-router.cjs +1 -1
- package/gsd-core/bin/lib/clock.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +48 -9
- package/gsd-core/bin/lib/config-loader.cjs +6 -2
- package/gsd-core/bin/lib/config.cjs +12 -0
- package/gsd-core/bin/lib/core-utils.cjs +8 -2
- package/gsd-core/bin/lib/drift.cjs +4 -4
- package/gsd-core/bin/lib/frontmatter.cjs +22 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
- package/gsd-core/bin/lib/host-integration.cjs +33 -8
- package/gsd-core/bin/lib/init.cjs +60 -53
- package/gsd-core/bin/lib/install-engine.cjs +93 -22
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
- package/gsd-core/bin/lib/markdown-table.cjs +698 -0
- package/gsd-core/bin/lib/mcp-server.cjs +18 -7
- package/gsd-core/bin/lib/milestone.cjs +217 -31
- package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
- package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
- package/gsd-core/bin/lib/phase-locator.cjs +23 -2
- package/gsd-core/bin/lib/phase.cjs +436 -61
- package/gsd-core/bin/lib/plan-scan.cjs +3 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
- package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
- package/gsd-core/bin/lib/roadmap.cjs +100 -49
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +242 -44
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -14
- 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 +526 -29
- package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
- package/gsd-core/bin/lib/schema-detect.cjs +2 -1
- package/gsd-core/bin/lib/security.cjs +7 -37
- package/gsd-core/bin/lib/shell-command-projection.cjs +176 -27
- package/gsd-core/bin/lib/smart-entry.cjs +4 -3
- package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
- package/gsd-core/bin/lib/state-transition.cjs +100 -45
- package/gsd-core/bin/lib/state.cjs +391 -126
- package/gsd-core/bin/lib/surface.cjs +12 -8
- package/gsd-core/bin/lib/template.cjs +2 -1
- package/gsd-core/bin/lib/uat.cjs +54 -8
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
- package/gsd-core/bin/lib/verify.cjs +4 -3
- package/gsd-core/bin/lib/workstream.cjs +3 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
- package/gsd-core/bin/lib/write-set.cjs +38 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -3
- package/gsd-core/references/checkpoints.md +12 -0
- 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/add-tests.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +7 -4
- package/gsd-core/workflows/debug.md +2 -0
- package/gsd-core/workflows/execute-phase.md +5 -3
- package/gsd-core/workflows/fast.md +8 -22
- package/gsd-core/workflows/plan-phase.md +6 -0
- package/gsd-core/workflows/progress.md +2 -2
- package/gsd-core/workflows/quick.md +2 -0
- package/gsd-core/workflows/review.md +42 -3
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +7 -4
- package/gsd-core/workflows/ship.md +8 -2
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +146 -1
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/hooks/dist/gsd-statusline.js +164 -14
- 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-statusline.js +164 -14
- 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 +10 -4
- package/pi/gsd.cjs +354 -0
- package/scripts/build-hooks.js +3 -0
- package/scripts/ci-test-scope.cjs +39 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +35 -35
- package/scripts/gen-install-tree-fixtures.cjs +75 -0
- package/scripts/gen-registry.cjs +128 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-table-schema-drift.cjs +157 -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/skills/gsd-mempalace-capture/SKILL.md +27 -1
- package/skills/gsd-surface/SKILL.md +6 -6
- 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
|
|
@@ -13,6 +13,7 @@ exports.ORM_INFO = exports.SCHEMA_PATTERNS = void 0;
|
|
|
13
13
|
exports.detectSchemaFiles = detectSchemaFiles;
|
|
14
14
|
exports.detectSchemaOrm = detectSchemaOrm;
|
|
15
15
|
exports.checkSchemaDrift = checkSchemaDrift;
|
|
16
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
16
17
|
exports.SCHEMA_PATTERNS = [
|
|
17
18
|
{ pattern: /^src\/collections\/.*\.ts$/, orm: 'payload' },
|
|
18
19
|
{ pattern: /^src\/globals\/.*\.ts$/, orm: 'payload' },
|
|
@@ -61,7 +62,7 @@ function detectSchemaFiles(files) {
|
|
|
61
62
|
const matches = [];
|
|
62
63
|
const orms = new Set();
|
|
63
64
|
for (const rawFile of files) {
|
|
64
|
-
const file =
|
|
65
|
+
const file = (0, shell_command_projection_cjs_1.posixNormalize)(rawFile);
|
|
65
66
|
for (const { pattern, orm } of exports.SCHEMA_PATTERNS) {
|
|
66
67
|
if (pattern.test(file)) {
|
|
67
68
|
matches.push(rawFile);
|
|
@@ -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"));
|
|
@@ -342,7 +341,7 @@ function sanitizeForDisplay(text) {
|
|
|
342
341
|
let sanitized = sanitizeForPrompt(text);
|
|
343
342
|
const protocolLeakPatterns = [
|
|
344
343
|
/^\s*(?:assistant|user|system)\s+to=[^:\s]+:[^\n]+$/i,
|
|
345
|
-
/^\s*<\|(?:assistant|user|system)[^|]*\|>\s*$/i,
|
|
344
|
+
/^\s*<\|(?:assistant|user|system)[^|]*\|>\s*$/i, // allow-adhoc-markdown: not a GFM table-cell scan — matches `<|role|>` protocol-leak marker tokens (prompt-injection sanitization), a false-positive on the table-regex pipe+cell-class fingerprint
|
|
346
345
|
];
|
|
347
346
|
sanitized = sanitized
|
|
348
347
|
.split('\n')
|
|
@@ -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.
|
|
@@ -15,6 +15,9 @@ 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.toPosixPath = toPosixPath;
|
|
19
|
+
exports.toNativePath = toNativePath;
|
|
20
|
+
exports.posixNormalize = posixNormalize;
|
|
18
21
|
exports.hookCommandNeedsPowerShellCallOperator = hookCommandNeedsPowerShellCallOperator;
|
|
19
22
|
exports.formatHookCommandForRuntime = formatHookCommandForRuntime;
|
|
20
23
|
exports.shellHookOmitsBashRunner = shellHookOmitsBashRunner;
|
|
@@ -38,6 +41,8 @@ exports.projectPersistentPathExportActions = projectPersistentPathExportActions;
|
|
|
38
41
|
exports.execGit = execGit;
|
|
39
42
|
exports.execNpm = execNpm;
|
|
40
43
|
exports.execTool = execTool;
|
|
44
|
+
exports.resolveGsdToolsPath = resolveGsdToolsPath;
|
|
45
|
+
exports.dispatchGsdCommand = dispatchGsdCommand;
|
|
41
46
|
exports.probeTty = probeTty;
|
|
42
47
|
exports.normalizeContent = normalizeContent;
|
|
43
48
|
exports.retryRenameSync = retryRenameSync;
|
|
@@ -50,26 +55,58 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
50
55
|
// can intercept calls from this seam — destructured imports capture references
|
|
51
56
|
// at load time and become un-mockable.
|
|
52
57
|
const node_child_process_1 = __importDefault(require("node:child_process"));
|
|
58
|
+
/**
|
|
59
|
+
* Convert a filesystem path to POSIX form (forward slashes) by translating the
|
|
60
|
+
* platform-native separator. Single seam for native→POSIX conversion.
|
|
61
|
+
*
|
|
62
|
+
* Prefer this over `p.replace(/\\/g, '/')`: the regex form hardcodes both
|
|
63
|
+
* separators and corrupts POSIX paths containing a literal backslash (a legal
|
|
64
|
+
* filename character). Splitting on `path.sep` only ever touches real
|
|
65
|
+
* separators — a no-op on POSIX, `\`→`/` on Windows.
|
|
66
|
+
*/
|
|
67
|
+
function toPosixPath(p) {
|
|
68
|
+
return p.split(node_path_1.default.sep).join(node_path_1.default.posix.sep);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Convert a filesystem path to the platform-native separator form. No-op on
|
|
72
|
+
* POSIX; `/`→`\` on Windows. Prefer this over a
|
|
73
|
+
* `process.platform === 'win32' ? p.replace(/\//g, '\\') : p` ternary.
|
|
74
|
+
*/
|
|
75
|
+
function toNativePath(p) {
|
|
76
|
+
return p.split(node_path_1.default.posix.sep).join(node_path_1.default.sep);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Normalize ALL backslashes to forward slashes, unconditionally and independent
|
|
80
|
+
* of the running OS. Use this when emitting a path into a POSIX/bash target
|
|
81
|
+
* (which may differ from the running platform — e.g. generating a Windows config
|
|
82
|
+
* on a Linux runner) or when parsing input whose separators are unpredictable.
|
|
83
|
+
*
|
|
84
|
+
* Contrast `toPosixPath`, which is running-OS-relative (splits on `path.sep`) and
|
|
85
|
+
* is for *this machine's* filesystem paths. Do NOT use `toPosixPath` for
|
|
86
|
+
* target-platform projection — on a Linux runner it would not convert a
|
|
87
|
+
* Windows-target path's backslashes.
|
|
88
|
+
*/
|
|
89
|
+
function posixNormalize(p) {
|
|
90
|
+
return p.replace(/\\/g, '/');
|
|
91
|
+
}
|
|
53
92
|
/**
|
|
54
93
|
* Return true when a managed hook command must be prefixed with PowerShell's
|
|
55
94
|
* call operator so a quoted executable token is invokable by the target
|
|
56
95
|
* runtime/shell combination.
|
|
57
96
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* one-line re-enable, per the conservative policy below. Note: Antigravity —
|
|
66
|
-
* the Gemini-backend successor — never matched the old `runtime === 'gemini'`
|
|
67
|
-
* check, so its behavior (no prefix) is unchanged.
|
|
97
|
+
* The `&`/no-`&` decision is keyed on the **effective hook-execution shell**
|
|
98
|
+
* (`opts.hookShell`), not on runtime alone — a single runtime (Claude Code)
|
|
99
|
+
* can host either Git Bash or PowerShell on Windows, and no single static
|
|
100
|
+
* command string is valid in both (#2236):
|
|
101
|
+
* - Git Bash: `"node.exe" "hook.js"` works; `& "node.exe" …` → syntax error.
|
|
102
|
+
* - PowerShell: `& "node.exe" "hook.js"` works; bare `"node.exe" …` →
|
|
103
|
+
* `Unexpected token`.
|
|
68
104
|
*
|
|
69
|
-
*
|
|
105
|
+
* Default is `false` (Git Bash form) for backward compatibility. Set
|
|
106
|
+
* `opts.hookShell = 'powershell'` to emit the PowerShell call-operator form.
|
|
70
107
|
*/
|
|
71
|
-
function hookCommandNeedsPowerShellCallOperator(
|
|
72
|
-
return
|
|
108
|
+
function hookCommandNeedsPowerShellCallOperator(opts = {}) {
|
|
109
|
+
return opts.hookShell === 'powershell';
|
|
73
110
|
}
|
|
74
111
|
/**
|
|
75
112
|
* Project a fully-assembled hook command string for the target runtime.
|
|
@@ -120,39 +157,48 @@ function formatManagedHookScriptToken(scriptPath, opts = {}) {
|
|
|
120
157
|
const platform = opts.platform || process.platform;
|
|
121
158
|
if (platform !== 'win32')
|
|
122
159
|
return null;
|
|
123
|
-
return JSON.stringify(scriptPath
|
|
160
|
+
return JSON.stringify(posixNormalize(scriptPath));
|
|
124
161
|
}
|
|
125
|
-
function projectLocalHookPrefix({ runtime = 'claude', dirName }) {
|
|
162
|
+
function projectLocalHookPrefix({ runtime: _runtime = 'claude', dirName, hookPathStyle }) {
|
|
126
163
|
if (!dirName)
|
|
127
164
|
return dirName;
|
|
128
|
-
|
|
165
|
+
// Descriptor-driven (ADR-1239 / #2096): folded from a hardcoded
|
|
166
|
+
// `runtime === 'antigravity'` literal into the runtime's declared
|
|
167
|
+
// `hostBehaviors.hookPathStyle`. Runtimes that always run project hooks
|
|
168
|
+
// with the project dir as cwd (Antigravity today) declare 'raw' and get
|
|
169
|
+
// the bare dirName; every other runtime keeps the $CLAUDE_PROJECT_DIR-
|
|
170
|
+
// anchored prefix. `runtime` itself is now unused here but stays in the
|
|
171
|
+
// signature for call-site/back-compat parity (kept `_`-prefixed to
|
|
172
|
+
// satisfy no-unused-vars).
|
|
173
|
+
return (hookPathStyle === 'raw')
|
|
129
174
|
? dirName
|
|
130
175
|
: `"$CLAUDE_PROJECT_DIR"/${dirName}`;
|
|
131
176
|
}
|
|
132
177
|
function projectPortableHookBaseDir({ configDir, homeDir }) {
|
|
133
|
-
const normalizedConfigDir = String(configDir || '')
|
|
134
|
-
const normalizedHome = String(homeDir || '')
|
|
178
|
+
const normalizedConfigDir = posixNormalize(String(configDir || ''));
|
|
179
|
+
const normalizedHome = posixNormalize(String(homeDir || ''));
|
|
135
180
|
if (!normalizedConfigDir || !normalizedHome)
|
|
136
181
|
return normalizedConfigDir;
|
|
137
182
|
return normalizedConfigDir.startsWith(normalizedHome)
|
|
138
183
|
? '$HOME' + normalizedConfigDir.slice(normalizedHome.length)
|
|
139
184
|
: normalizedConfigDir;
|
|
140
185
|
}
|
|
141
|
-
function projectShellCommandText({ runnerToken, argTokens = [], runtime = 'generic', platform = process.platform, }) {
|
|
186
|
+
function projectShellCommandText({ runnerToken, argTokens = [], runtime = 'generic', platform = process.platform, hookShell, }) {
|
|
142
187
|
if (!runnerToken)
|
|
143
188
|
return null;
|
|
144
189
|
const parts = [runnerToken, ...argTokens.filter(Boolean)];
|
|
145
|
-
return formatHookCommandForRuntime(parts.join(' '), { platform, runtime });
|
|
190
|
+
return formatHookCommandForRuntime(parts.join(' '), { platform, runtime, hookShell });
|
|
146
191
|
}
|
|
147
|
-
function projectManagedHookCommand({ absoluteRunner, scriptPath, runtime = 'generic', platform = process.platform }) {
|
|
192
|
+
function projectManagedHookCommand({ absoluteRunner, scriptPath, runtime = 'generic', platform = process.platform, hookShell }) {
|
|
148
193
|
if (!absoluteRunner || !scriptPath)
|
|
149
194
|
return null;
|
|
150
|
-
const normalizedScriptPath = platform === 'win32' ? scriptPath
|
|
195
|
+
const normalizedScriptPath = platform === 'win32' ? posixNormalize(scriptPath) : scriptPath;
|
|
151
196
|
return projectShellCommandText({
|
|
152
197
|
runnerToken: absoluteRunner,
|
|
153
198
|
argTokens: [JSON.stringify(normalizedScriptPath)],
|
|
154
199
|
runtime,
|
|
155
200
|
platform,
|
|
201
|
+
hookShell,
|
|
156
202
|
});
|
|
157
203
|
}
|
|
158
204
|
const MANAGED_HOOK_BASENAMES_BY_SURFACE = {
|
|
@@ -245,14 +291,14 @@ function isManagedHookCommand(commandText, opts = {}) {
|
|
|
245
291
|
for (const arg of opts.args) {
|
|
246
292
|
if (typeof arg !== 'string')
|
|
247
293
|
continue;
|
|
248
|
-
const argBasename = arg
|
|
294
|
+
const argBasename = posixNormalize(arg).split('/').pop() || '';
|
|
249
295
|
if (isManagedHookBasename(argBasename, { surface }))
|
|
250
296
|
return true;
|
|
251
297
|
}
|
|
252
298
|
}
|
|
253
|
-
const normalizedCommand = commandText
|
|
299
|
+
const normalizedCommand = posixNormalize(commandText);
|
|
254
300
|
if (typeof opts.configDir === 'string' && opts.configDir.length > 0) {
|
|
255
|
-
const normalizedHooksDir = `${node_path_1.default.join(opts.configDir, 'hooks')
|
|
301
|
+
const normalizedHooksDir = `${posixNormalize(node_path_1.default.join(opts.configDir, 'hooks'))}/`;
|
|
256
302
|
if (!normalizedCommand.includes(normalizedHooksDir))
|
|
257
303
|
return false;
|
|
258
304
|
}
|
|
@@ -282,7 +328,7 @@ const ANCHORED_HOOK_SCRIPT_TOKEN = /^"\$[A-Za-z_][A-Za-z0-9_]*"\//;
|
|
|
282
328
|
function projectLegacySettingsHookCommand({ absoluteRunner, scriptPath, scriptToken, runtime = 'generic', platform = process.platform, }) {
|
|
283
329
|
if (!absoluteRunner || !scriptPath)
|
|
284
330
|
return null;
|
|
285
|
-
const normalizedScriptPath = platform === 'win32' ? scriptPath
|
|
331
|
+
const normalizedScriptPath = platform === 'win32' ? posixNormalize(scriptPath) : scriptPath;
|
|
286
332
|
// #1693: a script path already carrying a `"$CLAUDE_PROJECT_DIR"`-anchored
|
|
287
333
|
// quoted prefix (local installs) is already a valid shell token — only the
|
|
288
334
|
// variable is quoted, the rest is bare. JSON.stringify-ing it on Windows
|
|
@@ -340,7 +386,7 @@ function projectPathActionProjection({ mode = 'repair', targetDir, platform = pr
|
|
|
340
386
|
let shellActions;
|
|
341
387
|
if (isWin32) {
|
|
342
388
|
const psTargetDir = escapePowerShellSingleQuoted(targetDir);
|
|
343
|
-
const bashTargetDir = escapeSingleQuotedShellLiteral(String(targetDir)
|
|
389
|
+
const bashTargetDir = escapeSingleQuotedShellLiteral(posixNormalize(String(targetDir)));
|
|
344
390
|
shellActions = [
|
|
345
391
|
{
|
|
346
392
|
label: 'PowerShell',
|
|
@@ -461,6 +507,109 @@ function execTool(program, args, opts = {}) {
|
|
|
461
507
|
});
|
|
462
508
|
return _spawnResult(result, program);
|
|
463
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* Resolve the absolute path to gsd-tools.cjs relative to THIS module.
|
|
512
|
+
*
|
|
513
|
+
* This file compiles to gsd-core/bin/lib/shell-command-projection.cjs — a
|
|
514
|
+
* sibling of gsd-core/bin/gsd-tools.cjs — so the relative walk-up is stable
|
|
515
|
+
* regardless of install location (global/local/dev-repo layouts all ship
|
|
516
|
+
* gsd-core/bin/ as a unit).
|
|
517
|
+
*/
|
|
518
|
+
function resolveGsdToolsPath() {
|
|
519
|
+
return node_path_1.default.resolve(__dirname, '..', 'gsd-tools.cjs');
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Subprocess-shim dispatch to gsd-tools.cjs (ADR-1239 #2102 Stage 2).
|
|
523
|
+
*
|
|
524
|
+
* No fully-populated in-process command-routing hub exists anywhere in the
|
|
525
|
+
* tree — every `createHub()` caller (cjs-command-router-adapter.cts,
|
|
526
|
+
* phase-command-router.cts, command-routing-hub.cts's own tests) builds a
|
|
527
|
+
* single-family hub for its own narrow purpose. The ONLY dispatch path that
|
|
528
|
+
* covers the FULL family/subcommand surface is the gsd-tools.cjs CLI itself.
|
|
529
|
+
* This mirrors the SUBPROCESS-REUSE precedent already established for the
|
|
530
|
+
* OpenCode/Kilo hook bridge (see .opencode/plugins/gsd-core.js header:
|
|
531
|
+
* "Architecture: SUBPROCESS REUSE ... spawns existing hook scripts as child
|
|
532
|
+
* processes") — the same pattern, applied to command dispatch instead of
|
|
533
|
+
* hook dispatch.
|
|
534
|
+
*
|
|
535
|
+
* Output-flag choice (verified by direct invocation — see #2102 dispatch
|
|
536
|
+
* notes for the sample invocations): always pass `--raw` (undecorated,
|
|
537
|
+
* programmatically-consumable stdout on success) and `--json-errors` (a
|
|
538
|
+
* structured `{ok:false,reason,message}` JSON object on stderr, with a
|
|
539
|
+
* non-zero exit, instead of a free-text "Error: ..." line). Both are global
|
|
540
|
+
* flags accepted by every gsd-tools.cjs family/subcommand, so passing them
|
|
541
|
+
* unconditionally is safe for the full command surface.
|
|
542
|
+
*
|
|
543
|
+
* `family` maps 1:1 onto gsd-tools.cjs's first positional argv token;
|
|
544
|
+
* `subcommand` (when present) onto the second — e.g.
|
|
545
|
+
* `{family:'phase', subcommand:'add'}` → `gsd-tools.cjs phase add`. An empty
|
|
546
|
+
* `subcommand` is omitted entirely (some families, e.g. `config-path`, take
|
|
547
|
+
* no subcommand).
|
|
548
|
+
*
|
|
549
|
+
* NEVER throws. Degrades to `{ ok:false, ... }` on:
|
|
550
|
+
* - a missing/invalid "family" (validated locally, no subprocess spawned)
|
|
551
|
+
* - ENOENT / a missing gsd-tools.cjs (via the injectable `gsdToolsPath`)
|
|
552
|
+
* - a wall-clock timeout (`timedOut:true`, mirroring the
|
|
553
|
+
* `signal === 'SIGTERM' && error.code === 'ETIMEDOUT'` idiom already used
|
|
554
|
+
* by worktree-safety.cts)
|
|
555
|
+
* - any other unanticipated throw from the underlying spawn (defensive
|
|
556
|
+
* try/catch — execTool itself is spawnSync-based and does not throw).
|
|
557
|
+
*/
|
|
558
|
+
function dispatchGsdCommand({ family, subcommand, args = [], cwd, timeout = 30_000, gsdToolsPath, } = {}) {
|
|
559
|
+
if (typeof family !== 'string' || family.length === 0) {
|
|
560
|
+
return {
|
|
561
|
+
ok: false,
|
|
562
|
+
stdout: '',
|
|
563
|
+
stderr: 'dispatchGsdCommand requires a non-empty string "family".',
|
|
564
|
+
code: null,
|
|
565
|
+
timedOut: false,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
const resolvedCwd = cwd || process.cwd();
|
|
569
|
+
const toolsPath = gsdToolsPath || resolveGsdToolsPath();
|
|
570
|
+
const argv = [
|
|
571
|
+
toolsPath,
|
|
572
|
+
family,
|
|
573
|
+
...(subcommand ? [subcommand] : []),
|
|
574
|
+
...(Array.isArray(args) ? args : []),
|
|
575
|
+
'--cwd', resolvedCwd,
|
|
576
|
+
'--raw',
|
|
577
|
+
'--json-errors',
|
|
578
|
+
];
|
|
579
|
+
let result;
|
|
580
|
+
try {
|
|
581
|
+
result = execTool(process.execPath, argv, { cwd: resolvedCwd, timeout });
|
|
582
|
+
}
|
|
583
|
+
catch (e) {
|
|
584
|
+
// Defensive belt-and-suspenders: execTool is spawnSync-based and does not
|
|
585
|
+
// throw today, but a degraded result here keeps this seam's no-throw
|
|
586
|
+
// contract true even under an unanticipated future failure mode.
|
|
587
|
+
return {
|
|
588
|
+
ok: false,
|
|
589
|
+
stdout: '',
|
|
590
|
+
stderr: e instanceof Error ? e.message : String(e),
|
|
591
|
+
code: null,
|
|
592
|
+
timedOut: false,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
// Mirrors the established `result.error && (result.error as
|
|
596
|
+
// NodeJS.ErrnoException).code === ...` idiom (graphify.cts, worktree-safety.cts):
|
|
597
|
+
// narrow away null via `!== null` FIRST, then cast — asserting `Error | null`
|
|
598
|
+
// to `NodeJS.ErrnoException | null` directly (paired with optional chaining)
|
|
599
|
+
// trips a typescript-eslint no-unnecessary-type-assertion false positive for
|
|
600
|
+
// this exact narrowing shape (all of ErrnoException's extra fields over Error
|
|
601
|
+
// are optional).
|
|
602
|
+
const timedOut = result.signal === 'SIGTERM'
|
|
603
|
+
&& result.error !== null
|
|
604
|
+
&& result.error.code === 'ETIMEDOUT';
|
|
605
|
+
return {
|
|
606
|
+
ok: result.exitCode === 0 && !timedOut,
|
|
607
|
+
stdout: result.stdout,
|
|
608
|
+
stderr: result.stderr,
|
|
609
|
+
code: result.exitCode,
|
|
610
|
+
timedOut,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
464
613
|
function probeTty(opts = {}) {
|
|
465
614
|
const platform = opts.platform ?? process.platform;
|
|
466
615
|
if (platform === 'win32')
|
|
@@ -42,6 +42,7 @@ exports.runSmartEntry = runSmartEntry;
|
|
|
42
42
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
43
43
|
const node_path_1 = __importDefault(require("node:path"));
|
|
44
44
|
const node_child_process_1 = require("node:child_process");
|
|
45
|
+
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
45
46
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
46
47
|
const ioMod = require("./io.cjs");
|
|
47
48
|
const { output } = ioMod;
|
|
@@ -274,9 +275,9 @@ function detectSignals(cwd, now = Date.now) {
|
|
|
274
275
|
fmScalar(fm, body, 'progress', 'Progress');
|
|
275
276
|
// Blockers list: `- <text>` items under a `## Blockers` heading.
|
|
276
277
|
const blockers = [];
|
|
277
|
-
const
|
|
278
|
-
if (
|
|
279
|
-
const items =
|
|
278
|
+
const blockersSection = (0, markdown_sectionizer_cjs_1.collectSection)(body, (h) => h.level === 2 && h.text.trim().toLowerCase() === 'blockers', { levelBounded: true });
|
|
279
|
+
if (blockersSection) {
|
|
280
|
+
const items = blockersSection.body.match(/^-\s+(.+)$/gm) || [];
|
|
280
281
|
for (const item of items)
|
|
281
282
|
blockers.push(item.replace(/^-\s+/, '').trim());
|
|
282
283
|
}
|
|
@@ -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 {
|