@opengsd/gsd-core 1.7.0-rc.4 → 1.7.0-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +20 -0
- package/agents/gsd-doc-classifier.md +105 -0
- package/agents/gsd-doc-synthesizer.md +61 -0
- package/agents/gsd-ui-checker.md +28 -0
- package/bin/install.js +624 -456
- package/gsd-core/bin/gsd-tools.cjs +40 -1
- package/gsd-core/bin/lib/api-coverage.cjs +466 -0
- package/gsd-core/bin/lib/audit.cjs +6 -3
- package/gsd-core/bin/lib/capability-loader.cjs +11 -9
- package/gsd-core/bin/lib/capability-registry.cjs +325 -64
- package/gsd-core/bin/lib/capability-validator.cjs +1 -1
- package/gsd-core/bin/lib/capability-writer.cjs +10 -1
- package/gsd-core/bin/lib/check-command-router.cjs +242 -3
- package/gsd-core/bin/lib/commands.cjs +7 -5
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +96 -0
- package/gsd-core/bin/lib/core-utils.cjs +4 -1
- package/gsd-core/bin/lib/host-integration-adapters/cline-sdk-binding.cjs +234 -0
- package/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs +145 -0
- package/gsd-core/bin/lib/host-integration.cjs +16 -0
- package/gsd-core/bin/lib/init.cjs +74 -30
- package/gsd-core/bin/lib/install-effort-resolver.cjs +213 -0
- package/gsd-core/bin/lib/install-engine.cjs +151 -11
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +68 -17
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +50 -11
- package/gsd-core/bin/lib/milestone.cjs +3 -3
- package/gsd-core/bin/lib/normalize-test-command.cjs +187 -0
- package/gsd-core/bin/lib/phase-id.cjs +132 -3
- package/gsd-core/bin/lib/phase.cjs +78 -16
- package/gsd-core/bin/lib/planning-workspace.cjs +17 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +5 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +21 -30
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +9 -9
- package/gsd-core/bin/lib/roadmap.cjs +42 -56
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +13 -6
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +17 -10
- package/gsd-core/bin/lib/runtime-homes.cjs +8 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +33 -25
- package/gsd-core/bin/lib/spec-section.cjs +111 -0
- package/gsd-core/bin/lib/state-transition.cjs +1 -1
- package/gsd-core/bin/lib/state.cjs +24 -24
- package/gsd-core/bin/lib/surface.cjs +30 -0
- package/gsd-core/bin/lib/uat.cjs +4 -1
- package/gsd-core/bin/lib/validate.cjs +15 -6
- package/gsd-core/bin/lib/verify.cjs +33 -37
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/bin/shared/model-catalog.json +6 -6
- package/gsd-core/references/api-coverage.md +104 -0
- package/gsd-core/references/model-profiles.md +2 -2
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/specless-probe-fallback.md +172 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/workflows/audit-fix.md +9 -1
- package/gsd-core/workflows/code-review-fix.md +7 -3
- package/gsd-core/workflows/code-review.md +4 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +8 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +42 -0
- package/gsd-core/workflows/execute-phase.md +1 -25
- package/gsd-core/workflows/plan-phase.md +31 -2
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/review.md +59 -13
- package/gsd-core/workflows/settings-advanced.md +5 -5
- package/gsd-core/workflows/settings.md +2 -2
- package/gsd-core/workflows/verify-phase.md +3 -2
- package/gsd-core/workflows/verify-work.md +38 -0
- package/hooks/dist/gsd-cursor-pre-tool.js +76 -0
- package/hooks/dist/gsd-cursor-stop.js +48 -0
- package/hooks/dist/gsd-cursor-subagent-start.js +50 -0
- package/hooks/dist/gsd-cursor-subagent-stop.js +40 -0
- package/hooks/dist/managed-hooks-registry.cjs +4 -0
- package/hooks/gsd-cursor-pre-tool.js +76 -0
- package/hooks/gsd-cursor-stop.js +48 -0
- package/hooks/gsd-cursor-subagent-start.js +50 -0
- package/hooks/gsd-cursor-subagent-stop.js +40 -0
- package/hooks/managed-hooks-registry.cjs +4 -0
- package/package.json +2 -1
- package/scripts/build-hooks.js +5 -1
- package/scripts/lint-phase-id-drift.cjs +150 -0
- package/scripts/run-tests.cjs +21 -1
package/bin/install.js
CHANGED
|
@@ -43,6 +43,7 @@ const {
|
|
|
43
43
|
readBaseRefFromSettings,
|
|
44
44
|
} = require('../gsd-core/bin/lib/worktree-base-ref.cjs');
|
|
45
45
|
const { resolveInstallPlan } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs');
|
|
46
|
+
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
|
|
46
47
|
const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
|
|
47
48
|
// Canonical set of hook files shipped to users. Imported here so writeManifest()
|
|
48
49
|
// records exactly the same set that build-hooks.js copies to hooks/dist/, making
|
|
@@ -101,6 +102,45 @@ const reset = '\x1b[0m';
|
|
|
101
102
|
// Codex config.toml constants
|
|
102
103
|
const GSD_CODEX_MARKER = '# GSD Agent Configuration \u2014 managed by gsd-core installer';
|
|
103
104
|
const GSD_CODEX_HOOKS_OWNERSHIP_PREFIX = '# GSD codex_hooks ownership: ';
|
|
105
|
+
// Known scalar fields of Codex's `AgentsToml` struct (codex-rs/config/src/
|
|
106
|
+
// config_toml.rs \u2014 `[agents]` table). Codex marks the struct
|
|
107
|
+
// `#[schemars(deny_unknown_fields)]`, so a bare `[agents]` table is valid ONLY
|
|
108
|
+
// when every direct key is one of these (named agent roles live in the flattened
|
|
109
|
+
// `[agents.<name>]` sub-tables, a separate `AgentRoleToml`). GSD writes only
|
|
110
|
+
// `max_depth` (ADR-1239 upgrade 2 / #2088); the full set is enumerated so the
|
|
111
|
+
// schema check accepts a user's other legitimate AgentsToml scalars too.
|
|
112
|
+
const CODEX_AGENTS_TOML_SCALAR_KEYS = new Set([
|
|
113
|
+
'max_threads',
|
|
114
|
+
'max_depth',
|
|
115
|
+
'job_max_runtime_seconds',
|
|
116
|
+
'interrupt_message',
|
|
117
|
+
]);
|
|
118
|
+
// GSD's managed dispatch-depth value. Codex's implicit default is also 1 (root
|
|
119
|
+
// sessions start at depth 0); writing it EXPLICITLY pins the negotiated
|
|
120
|
+
// `dispatch.maxDepth: 1` axis instead of relying on codex-cli's implicit default
|
|
121
|
+
// (ADR-1239 upgrade 2 / #2088). Per the negotiated capability, GSD-hosted Codex
|
|
122
|
+
// dispatch is single-level (maxDepth === 1 \u2192 `degradationFor` flattens waves).
|
|
123
|
+
const GSD_CODEX_AGENTS_MAX_DEPTH = 1;
|
|
124
|
+
// Codex hooks.json lifecycle events GSD registers beyond SessionStart (which has
|
|
125
|
+
// its own dedicated path). This is Codex's OWN hook-event vocabulary (per
|
|
126
|
+
// developers.openai.com/codex/config-reference), distinct from the cross-runtime
|
|
127
|
+
// settings.json `extendedHookEvents` descriptor field (a claude/gemini-family
|
|
128
|
+
// allowlist consumed only by hooksSurface==='settings-json' runtimes — Codex is
|
|
129
|
+
// codex-hooks-json). All route through gsd-context-monitor.js. #772 wired the
|
|
130
|
+
// first three; #2088 adds the remaining six documented events so GSD's monitor
|
|
131
|
+
// fires at the same lifecycle points as in Claude Code. Install and uninstall
|
|
132
|
+
// share this list so the registered set and the removed set never diverge.
|
|
133
|
+
const CODEX_EXTENDED_HOOK_EVENTS = [
|
|
134
|
+
'SubagentStart',
|
|
135
|
+
'Stop',
|
|
136
|
+
'PostToolUse',
|
|
137
|
+
'PreToolUse',
|
|
138
|
+
'PermissionRequest',
|
|
139
|
+
'PreCompact',
|
|
140
|
+
'PostCompact',
|
|
141
|
+
'SubagentStop',
|
|
142
|
+
'UserPromptSubmit',
|
|
143
|
+
];
|
|
104
144
|
// Codex's hook-enabling feature flag (issue #3566). Codex itself marks
|
|
105
145
|
// `codex_hooks` as a `legacy_key` in codex-rs/features/src/legacy.rs; the
|
|
106
146
|
// canonical current key under [features] is `hooks`. The installer always
|
|
@@ -126,6 +166,9 @@ function isCodexHooksFeatureKey(key) {
|
|
|
126
166
|
//
|
|
127
167
|
// Merge policy: additive, non-destructive \u2014 existing user entries are preserved;
|
|
128
168
|
// GSD entries are appended only when not already present (idempotent).
|
|
169
|
+
// The reference/default runtime (ADR-1239 reference host). Single-sourced here
|
|
170
|
+
// instead of scattered literal 'claude' defaults/rosters (#2086).
|
|
171
|
+
const DEFAULT_RUNTIME = 'claude';
|
|
129
172
|
const GSD_CLAUDE_ALLOW_PERMISSIONS = Object.freeze([
|
|
130
173
|
'Bash(npx gsd-core *)',
|
|
131
174
|
'Read(.planning/*)',
|
|
@@ -212,12 +255,30 @@ const GSD_COPILOT_SESSION_HOOK_PWSH =
|
|
|
212
255
|
// Cursor reads hook configs from <project-root>/.cursor/hooks.json (local) or
|
|
213
256
|
// ~/.cursor/hooks.json (global) with the shape { version: 1, hooks: { <event>: [...] } }.
|
|
214
257
|
// Events use camelCase: sessionStart, postToolUse, preToolUse, etc.
|
|
215
|
-
// A `command` hook entry runs an external script. GSD registers
|
|
216
|
-
//
|
|
217
|
-
//
|
|
258
|
+
// A `command` hook entry runs an external script. GSD registers six managed hooks
|
|
259
|
+
// (AC4a upgrade, #2089 — ADR-1239):
|
|
260
|
+
// sessionStart → gsd-cursor-session-start.js (context injection)
|
|
261
|
+
// postToolUse → gsd-cursor-post-tool.js (STATE.md update monitor)
|
|
262
|
+
// preToolUse → gsd-cursor-pre-tool.js (write-path guard)
|
|
263
|
+
// stop → gsd-cursor-stop.js (verify-work reminder)
|
|
264
|
+
// subagentStart → gsd-cursor-subagent-start.js (subagent context injection)
|
|
265
|
+
// subagentStop → gsd-cursor-subagent-stop.js (subagent completion reminder)
|
|
218
266
|
// Cursor docs: https://cursor.com/docs/hooks
|
|
219
267
|
const GSD_CURSOR_SESSION_HOOK_SCRIPT = 'gsd-cursor-session-start.js';
|
|
220
268
|
const GSD_CURSOR_POST_TOOL_HOOK_SCRIPT = 'gsd-cursor-post-tool.js';
|
|
269
|
+
const GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT = 'gsd-cursor-pre-tool.js';
|
|
270
|
+
const GSD_CURSOR_STOP_HOOK_SCRIPT = 'gsd-cursor-stop.js';
|
|
271
|
+
const GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT = 'gsd-cursor-subagent-start.js';
|
|
272
|
+
const GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT = 'gsd-cursor-subagent-stop.js';
|
|
273
|
+
// All GSD-managed Cursor hook scripts (used by uninstall cleanup).
|
|
274
|
+
const GSD_CURSOR_HOOK_SCRIPTS = [
|
|
275
|
+
GSD_CURSOR_SESSION_HOOK_SCRIPT,
|
|
276
|
+
GSD_CURSOR_POST_TOOL_HOOK_SCRIPT,
|
|
277
|
+
GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT,
|
|
278
|
+
GSD_CURSOR_STOP_HOOK_SCRIPT,
|
|
279
|
+
GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT,
|
|
280
|
+
GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT,
|
|
281
|
+
];
|
|
221
282
|
// Marker comment embedded in managed hook entries so GSD can find+remove them.
|
|
222
283
|
const GSD_CURSOR_HOOK_MARKER = 'gsd-managed';
|
|
223
284
|
|
|
@@ -273,60 +334,20 @@ const {
|
|
|
273
334
|
} = require(path.join(_gsdLibDir, 'model-catalog.cjs'));
|
|
274
335
|
const {
|
|
275
336
|
resolveTierEntry: gsdResolveTierEntry,
|
|
276
|
-
EFFORT_SET: GSD_EFFORT_SET,
|
|
277
337
|
} = require(path.join(_gsdLibDir, 'model-resolver.cjs'));
|
|
278
338
|
|
|
279
|
-
// #
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const manifestPath = path.join(
|
|
292
|
-
__dirname,
|
|
293
|
-
'..',
|
|
294
|
-
'gsd-core',
|
|
295
|
-
'bin',
|
|
296
|
-
'shared',
|
|
297
|
-
'config-defaults.manifest.json'
|
|
298
|
-
);
|
|
299
|
-
let manifestData;
|
|
300
|
-
try {
|
|
301
|
-
manifestData = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
302
|
-
} catch (_err) {
|
|
303
|
-
// Fail loudly — a missing manifest is a broken install, not a soft degradation.
|
|
304
|
-
throw new Error(
|
|
305
|
-
`gsd install: cannot load config-defaults.manifest.json at ${manifestPath}: ${_err.message}`
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const tierDefaults =
|
|
310
|
-
(manifestData.effort &&
|
|
311
|
-
manifestData.effort.routing_tier_defaults &&
|
|
312
|
-
typeof manifestData.effort.routing_tier_defaults === 'object' &&
|
|
313
|
-
!Array.isArray(manifestData.effort.routing_tier_defaults))
|
|
314
|
-
? manifestData.effort.routing_tier_defaults
|
|
315
|
-
: { light: 'low', standard: 'high', heavy: 'xhigh' }; // guard: unreachable if manifest is valid
|
|
316
|
-
|
|
317
|
-
const effortDefault =
|
|
318
|
-
(manifestData.effort && typeof manifestData.effort.default === 'string')
|
|
319
|
-
? manifestData.effort.default
|
|
320
|
-
: 'high'; // guard: unreachable if manifest is valid
|
|
321
|
-
|
|
322
|
-
_gsdEffortCatalogCache = {
|
|
323
|
-
AGENT_DEFAULT_TIERS,
|
|
324
|
-
renderEffortForRuntime,
|
|
325
|
-
EFFORT_MANIFEST_TIER_DEFAULTS: tierDefaults,
|
|
326
|
-
EFFORT_MANIFEST_DEFAULT: effortDefault,
|
|
327
|
-
};
|
|
328
|
-
return _gsdEffortCatalogCache;
|
|
329
|
-
}
|
|
339
|
+
// #2071 — install-time effort resolution (readGsdEffectiveEffortConfig /
|
|
340
|
+
// resolveInstallTimeEffort, plus their _getGsdEffortCatalog + _readGsdConfigFile
|
|
341
|
+
// helpers) was extracted into the shipped gsd-core/bin/lib/install-effort-resolver.cjs
|
|
342
|
+
// so `gsd-tools effort sync` can require it from the installed runtime instead of this
|
|
343
|
+
// package-root bin/install.js, which the installer never copies (#2071 crash). The
|
|
344
|
+
// installer imports it back here — single source of truth for both surfaces.
|
|
345
|
+
const {
|
|
346
|
+
readGsdEffectiveEffortConfig,
|
|
347
|
+
resolveInstallTimeEffort,
|
|
348
|
+
_getGsdEffortCatalog,
|
|
349
|
+
_readGsdConfigFile,
|
|
350
|
+
} = require(path.join(_gsdLibDir, 'install-effort-resolver.cjs'));
|
|
330
351
|
|
|
331
352
|
const {
|
|
332
353
|
MINIMAL_SKILL_ALLOWLIST,
|
|
@@ -350,6 +371,80 @@ try {
|
|
|
350
371
|
} catch (_) {
|
|
351
372
|
_capabilityRegistry = undefined;
|
|
352
373
|
}
|
|
374
|
+
|
|
375
|
+
// Fail-safe floor for the reference host's #338-privacy-critical behaviors, used
|
|
376
|
+
// ONLY when the first-party capability registry cannot be loaded (a broken bundle).
|
|
377
|
+
// Without it, a registry-load failure would make `_hostBehaviors('claude')` return
|
|
378
|
+
// {} and silently route a claude LOCAL install to the repo-shared, committed
|
|
379
|
+
// `settings.json` instead of the gitignored `settings.local.json` (#338) — leaking
|
|
380
|
+
// engineer-specific absolute paths. Keyed by runtime id (a DATA lookup, not a
|
|
381
|
+
// hardcoded string-equality branch) so behavior degrades CLOSED (safe), never open.
|
|
382
|
+
// The live descriptor (capabilities/claude/capability.json) remains the source of
|
|
383
|
+
// truth; this mirrors only the privacy-load-bearing subset. (ADR-1239 / #2086)
|
|
384
|
+
const FALLBACK_HOST_BEHAVIORS = Object.freeze({
|
|
385
|
+
claude: Object.freeze({
|
|
386
|
+
settingsFileByScope: Object.freeze({ local: 'settings.local.json', global: 'settings.json' }),
|
|
387
|
+
permissionsSchema: 'claude',
|
|
388
|
+
sourceMarkerFile: '.gsd-source',
|
|
389
|
+
}),
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Resolve a runtime's host behaviors from a capability registry, with the
|
|
394
|
+
* #338-privacy fail-safe floor when the registry (or the runtime's descriptor)
|
|
395
|
+
* is unavailable. Registry is passed in so this is unit-testable under a
|
|
396
|
+
* simulated registry-load failure. (ADR-1239 / #2086)
|
|
397
|
+
*/
|
|
398
|
+
function _resolveHostBehaviors(runtime, registry) {
|
|
399
|
+
const cap = registry && registry.runtimes && registry.runtimes[runtime];
|
|
400
|
+
const declared = cap && cap.runtime && cap.runtime.hostBehaviors;
|
|
401
|
+
if (declared) return declared;
|
|
402
|
+
return FALLBACK_HOST_BEHAVIORS[runtime] || {};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Host-specific install behaviors, declared on the runtime descriptor
|
|
407
|
+
* (capabilities/<runtime>/capability.json -> runtime.hostBehaviors) instead of
|
|
408
|
+
* scattered `runtime === '<id>'` string checks (ADR-1239 / #2086). Returns {}
|
|
409
|
+
* for runtimes that declare none, so every behavior branch degrades to the
|
|
410
|
+
* generic path by default — EXCEPT the reference host's #338-critical keys, which
|
|
411
|
+
* fall back to FALLBACK_HOST_BEHAVIORS if the registry failed to load.
|
|
412
|
+
*/
|
|
413
|
+
function _hostBehaviors(runtime) {
|
|
414
|
+
return _resolveHostBehaviors(runtime, _capabilityRegistry);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Resolve the ACTUAL on-disk skills-install directory for a runtime, honoring a
|
|
419
|
+
* skills-kind `home` override (ADR-1239 upgrade 3 / #2088: e.g. Codex skills ->
|
|
420
|
+
* $HOME/.agents/skills instead of the runtime's configDir). Descriptor-driven
|
|
421
|
+
* (no runtime === '<id>' check) so the snapshot/rollback machinery and post-install
|
|
422
|
+
* verification look where the skills actually landed. Falls back to <targetDir>/skills.
|
|
423
|
+
*/
|
|
424
|
+
function _resolveSkillsRootDir(runtime, targetDir, scope) {
|
|
425
|
+
try {
|
|
426
|
+
const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope);
|
|
427
|
+
const skillsKind = layout.kinds.find((k) => k.kind === 'skills');
|
|
428
|
+
if (skillsKind) return path.join(skillsKind.home || targetDir, skillsKind.destSubpath);
|
|
429
|
+
} catch (_e) { /* fall through to the configDir default */ }
|
|
430
|
+
return path.join(targetDir, 'skills');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Construct the imperative Host-Integration adapter (ADR-1239 / #2086), FAIL-OPEN.
|
|
435
|
+
* `createImperativeAdapter` composes the capability registry via
|
|
436
|
+
* `loadRegistry({includeInstalled:true})`, which require()s several capability
|
|
437
|
+
* modules. If any is unavailable (e.g. a packaging regression), return null so
|
|
438
|
+
* the caller degrades to the engine directly rather than hard-crashing install/
|
|
439
|
+
* uninstall — matching the optional `capability-registry.cjs` load posture above.
|
|
440
|
+
*/
|
|
441
|
+
function _runtimeAdapter(runtime) {
|
|
442
|
+
try {
|
|
443
|
+
return createImperativeAdapter({ runtime });
|
|
444
|
+
} catch {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
353
448
|
const {
|
|
354
449
|
applyInstallerMigrationPlan,
|
|
355
450
|
discoverInstallerMigrations,
|
|
@@ -910,6 +1005,9 @@ function resolveKiloConfigPath(configDir) {
|
|
|
910
1005
|
return path.join(configDir, 'kilo.json');
|
|
911
1006
|
}
|
|
912
1007
|
|
|
1008
|
+
// #2087 — attribution config-path resolvers, keyed by descriptor (hostBehaviors.attributionConfigResolver)
|
|
1009
|
+
const ATTRIBUTION_CONFIG_RESOLVERS = { opencode: resolveOpencodeConfigPath, kilo: resolveKiloConfigPath };
|
|
1010
|
+
|
|
913
1011
|
/**
|
|
914
1012
|
* Strip JSONC comments (// and /* */) from a string to produce valid JSON.
|
|
915
1013
|
* Handles comments inside strings correctly (does not strip them).
|
|
@@ -1071,126 +1169,6 @@ function readGsdEffectiveModelOverrides(targetDir = null) {
|
|
|
1071
1169
|
return { ...(global || {}), ...(projectOverrides || {}) };
|
|
1072
1170
|
}
|
|
1073
1171
|
|
|
1074
|
-
/**
|
|
1075
|
-
* #443 — Read the merged `effort` config block for install-time effort resolution.
|
|
1076
|
-
*
|
|
1077
|
-
* Probes the same config sources as readGsdRuntimeProfileResolver (per-project
|
|
1078
|
-
* `.planning/config.json` wins over `~/.gsd/defaults.json`) but extracts the
|
|
1079
|
-
* `effort` object instead of the model-profile fields.
|
|
1080
|
-
*
|
|
1081
|
-
* Returns the merged `effort` object or null when neither source defines one.
|
|
1082
|
-
* The caller can pass this to resolveInstallTimeEffort() which is pure and
|
|
1083
|
-
* requires no filesystem access beyond what this helper already performs.
|
|
1084
|
-
*
|
|
1085
|
-
* @param {string|null} targetDir Runtime install root (walks up to find .planning/).
|
|
1086
|
-
* @returns {object|null}
|
|
1087
|
-
*/
|
|
1088
|
-
function readGsdEffectiveEffortConfig(targetDir = null) {
|
|
1089
|
-
const homeDefaults = _readGsdConfigFile(
|
|
1090
|
-
path.join(os.homedir(), '.gsd', 'defaults.json'),
|
|
1091
|
-
'~/.gsd/defaults.json'
|
|
1092
|
-
);
|
|
1093
|
-
|
|
1094
|
-
let projectConfig = null;
|
|
1095
|
-
if (targetDir) {
|
|
1096
|
-
let probeDir = path.resolve(targetDir);
|
|
1097
|
-
for (let depth = 0; depth < 8; depth += 1) {
|
|
1098
|
-
const candidate = path.join(probeDir, '.planning', 'config.json');
|
|
1099
|
-
if (fs.existsSync(candidate)) {
|
|
1100
|
-
projectConfig = _readGsdConfigFile(candidate, '.planning/config.json');
|
|
1101
|
-
break;
|
|
1102
|
-
}
|
|
1103
|
-
const parent = path.dirname(probeDir);
|
|
1104
|
-
if (parent === probeDir) break;
|
|
1105
|
-
probeDir = parent;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
const homeEffort = (homeDefaults && homeDefaults.effort && typeof homeDefaults.effort === 'object' && !Array.isArray(homeDefaults.effort))
|
|
1110
|
-
? homeDefaults.effort
|
|
1111
|
-
: null;
|
|
1112
|
-
const projectEffort = (projectConfig && projectConfig.effort && typeof projectConfig.effort === 'object' && !Array.isArray(projectConfig.effort))
|
|
1113
|
-
? projectConfig.effort
|
|
1114
|
-
: null;
|
|
1115
|
-
|
|
1116
|
-
if (!homeEffort && !projectEffort) return null;
|
|
1117
|
-
|
|
1118
|
-
// Per-project wins on conflict within each sub-field. Merge field-by-field so
|
|
1119
|
-
// a project config that only sets agent_overrides still inherits global
|
|
1120
|
-
// routing_tier_defaults and default.
|
|
1121
|
-
return {
|
|
1122
|
-
...(homeEffort || {}),
|
|
1123
|
-
...(projectEffort || {}),
|
|
1124
|
-
// Deep-merge agent_overrides (project wins per-key)
|
|
1125
|
-
agent_overrides: {
|
|
1126
|
-
...((homeEffort && homeEffort.agent_overrides) || {}),
|
|
1127
|
-
...((projectEffort && projectEffort.agent_overrides) || {}),
|
|
1128
|
-
},
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
/**
|
|
1134
|
-
* #443 — Resolve install-time effort for a given agent, using the same
|
|
1135
|
-
* precedence chain as resolveEffortInternal() in core.cjs, but operating
|
|
1136
|
-
* on a pre-loaded effortCfg object (no loadConfig side-effects at install).
|
|
1137
|
-
*
|
|
1138
|
-
* Precedence (mirrors resolveEffortInternal):
|
|
1139
|
-
* 1. effortCfg.agent_overrides[agentName]
|
|
1140
|
-
* 2. effortCfg.routing_tier_defaults[agentTier] (if effortCfg present)
|
|
1141
|
-
* — OR manifest tier defaults when effortCfg is null
|
|
1142
|
-
* 3. effortCfg.default
|
|
1143
|
-
* 4. 'high' (hardcoded fallback)
|
|
1144
|
-
*
|
|
1145
|
-
* @param {object|null} effortCfg Result of readGsdEffectiveEffortConfig().
|
|
1146
|
-
* @param {string} agentName e.g. 'gsd-planner'
|
|
1147
|
-
* @returns {string} Universal effort string (low/medium/high/xhigh/max/minimal)
|
|
1148
|
-
*/
|
|
1149
|
-
function resolveInstallTimeEffort(effortCfg, agentName) {
|
|
1150
|
-
// Validates each candidate against the canonical EFFORT_SET (sourced once
|
|
1151
|
-
// from core.cjs) before accepting it, mirroring resolveEffortInternal exactly.
|
|
1152
|
-
// Invalid values fall through to the next precedence layer; final fallback 'high'.
|
|
1153
|
-
|
|
1154
|
-
// Step 1: agent_overrides
|
|
1155
|
-
if (effortCfg) {
|
|
1156
|
-
const ao = effortCfg.agent_overrides;
|
|
1157
|
-
if (ao && typeof ao === 'object' && !Array.isArray(ao)) {
|
|
1158
|
-
const v = ao[agentName];
|
|
1159
|
-
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
// Step 2: routing_tier_defaults keyed by the agent's catalog tier
|
|
1164
|
-
const { AGENT_DEFAULT_TIERS, EFFORT_MANIFEST_TIER_DEFAULTS, EFFORT_MANIFEST_DEFAULT } = _getGsdEffortCatalog();
|
|
1165
|
-
const agentTier = AGENT_DEFAULT_TIERS[agentName];
|
|
1166
|
-
if (agentTier) {
|
|
1167
|
-
if (effortCfg && effortCfg.routing_tier_defaults &&
|
|
1168
|
-
typeof effortCfg.routing_tier_defaults === 'object' &&
|
|
1169
|
-
!Array.isArray(effortCfg.routing_tier_defaults)) {
|
|
1170
|
-
const v = effortCfg.routing_tier_defaults[agentTier];
|
|
1171
|
-
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
|
|
1172
|
-
} else if (!effortCfg) {
|
|
1173
|
-
// No effort config — use manifest tier defaults
|
|
1174
|
-
const v = EFFORT_MANIFEST_TIER_DEFAULTS[agentTier];
|
|
1175
|
-
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
|
|
1176
|
-
}
|
|
1177
|
-
// effortCfg exists but has no routing_tier_defaults — fall through
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
// Step 3: effort.default
|
|
1181
|
-
if (effortCfg) {
|
|
1182
|
-
const d = effortCfg.default;
|
|
1183
|
-
if (typeof d === 'string' && GSD_EFFORT_SET.has(d)) return d;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
// Step 4: manifest default (sourced from config-defaults.manifest.json effort.default)
|
|
1187
|
-
// If even the manifest default is invalid, fall back to 'high'.
|
|
1188
|
-
if (typeof EFFORT_MANIFEST_DEFAULT === 'string' && GSD_EFFORT_SET.has(EFFORT_MANIFEST_DEFAULT)) {
|
|
1189
|
-
return EFFORT_MANIFEST_DEFAULT;
|
|
1190
|
-
}
|
|
1191
|
-
return 'high';
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
1172
|
/**
|
|
1195
1173
|
* #443 — Inject `effort: <value>` into YAML frontmatter of a Claude .md agent
|
|
1196
1174
|
* file in a newline-agnostic way (LF and CRLF source files are both handled).
|
|
@@ -1296,29 +1274,6 @@ const READONLY_AGENT_DISALLOWED_TOOLS = {
|
|
|
1296
1274
|
'gsd-ui-auditor': 'Edit, MultiEdit',
|
|
1297
1275
|
};
|
|
1298
1276
|
|
|
1299
|
-
/**
|
|
1300
|
-
* #2517 — Read a single GSD config file (defaults.json or per-project
|
|
1301
|
-
* config.json) into a plain object, returning null on missing/empty files
|
|
1302
|
-
* and warning to stderr on JSON parse failures so silent corruption can't
|
|
1303
|
-
* mask broken configs (review finding #5).
|
|
1304
|
-
*/
|
|
1305
|
-
function _readGsdConfigFile(absPath, label) {
|
|
1306
|
-
if (!fs.existsSync(absPath)) return null;
|
|
1307
|
-
let raw;
|
|
1308
|
-
try {
|
|
1309
|
-
raw = fs.readFileSync(absPath, 'utf-8');
|
|
1310
|
-
} catch (err) {
|
|
1311
|
-
process.stderr.write(`gsd: warning — could not read ${label} (${absPath}): ${err.message}\n`);
|
|
1312
|
-
return null;
|
|
1313
|
-
}
|
|
1314
|
-
try {
|
|
1315
|
-
return JSON.parse(raw);
|
|
1316
|
-
} catch (err) {
|
|
1317
|
-
process.stderr.write(`gsd: warning — invalid JSON in ${label} (${absPath}): ${err.message}\n`);
|
|
1318
|
-
return null;
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
1277
|
/**
|
|
1323
1278
|
* #2517 — Build a runtime-aware tier resolver for the install path.
|
|
1324
1279
|
*
|
|
@@ -1421,15 +1376,14 @@ function getCommitAttribution(runtime) {
|
|
|
1421
1376
|
|
|
1422
1377
|
let result;
|
|
1423
1378
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
: resolveKiloConfigPath;
|
|
1379
|
+
const _attrResolverKey = _hostBehaviors(runtime).attributionConfigResolver;
|
|
1380
|
+
if (_attrResolverKey && ATTRIBUTION_CONFIG_RESOLVERS[_attrResolverKey]) {
|
|
1381
|
+
const resolveConfigPath = ATTRIBUTION_CONFIG_RESOLVERS[_attrResolverKey];
|
|
1428
1382
|
const config = readSettings(resolveConfigPath(getGlobalConfigDir(runtime, null)));
|
|
1429
1383
|
result = (config && config.disable_ai_attribution === true) ? null : undefined;
|
|
1430
|
-
} else if (runtime === '
|
|
1384
|
+
} else if (_hostBehaviors(runtime).attributionSource === 'settings-json-commit') {
|
|
1431
1385
|
// Claude Code
|
|
1432
|
-
const settings = readSettings(path.join(getGlobalConfigDir(
|
|
1386
|
+
const settings = readSettings(path.join(getGlobalConfigDir(runtime, explicitConfigDir), 'settings.json'));
|
|
1433
1387
|
if (!settings || !settings.attribution || settings.attribution.commit === undefined) {
|
|
1434
1388
|
result = undefined;
|
|
1435
1389
|
} else if (settings.attribution.commit === '') {
|
|
@@ -1897,7 +1851,7 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
|
|
|
1897
1851
|
// Hermes' SKILL.md spec lists `version` as a required frontmatter field.
|
|
1898
1852
|
// Track GSD's package version so Hermes' skill_view() reports a stable
|
|
1899
1853
|
// identifier per install.
|
|
1900
|
-
if (runtime
|
|
1854
|
+
if (_hostBehaviors(runtime).skillFrontmatterVersion) fm += `version: ${yamlQuote(pkg.version)}\n`;
|
|
1901
1855
|
// #778 (b) — Qwen-only numeric priority for /skills ordering. Scoped to qwen
|
|
1902
1856
|
// so Claude/Hermes skill frontmatter is unchanged (they ignore the field, but
|
|
1903
1857
|
// we keep their output byte-stable). skillName is the `gsd-<stem>` dir name.
|
|
@@ -3266,6 +3220,77 @@ function cleanupWindsurfLegacyDevinSkills(workspaceDir) {
|
|
|
3266
3220
|
return removed;
|
|
3267
3221
|
}
|
|
3268
3222
|
|
|
3223
|
+
/**
|
|
3224
|
+
* Migrate a skills kind that moved to an alternate `home` (ADR-1239 split-home):
|
|
3225
|
+
* remove now-stale `<prefix>*` skill dirs left at the OLD configDir-rooted
|
|
3226
|
+
* location by installs from before the move. Without this, upgrading (e.g. Codex
|
|
3227
|
+
* relocating skills to ~/.agents/skills) orphans the pre-move dirs at
|
|
3228
|
+
* ~/.codex/skills. Only managed `<prefix>*` dirs are touched; user-owned content
|
|
3229
|
+
* (non-prefixed dirs, gsd-dev-preferences, symlinks) is preserved. Fail-open.
|
|
3230
|
+
* @param {string} oldSkillsDir absolute path to the pre-move skills location
|
|
3231
|
+
* @param {string} prefix managed skill-dir prefix (e.g. 'gsd-')
|
|
3232
|
+
* @returns {number} count of stale dirs removed
|
|
3233
|
+
*/
|
|
3234
|
+
function cleanupMovedSkillsOldLocation(oldSkillsDir, prefix) {
|
|
3235
|
+
if (!fs.existsSync(oldSkillsDir)) return 0;
|
|
3236
|
+
|
|
3237
|
+
// Mirror the user-owned list from cleanupCodexSkillMetadataSidecars (#2973).
|
|
3238
|
+
const _userOwnedSkillDirs = new Set(['gsd-dev-preferences']);
|
|
3239
|
+
let removed = 0;
|
|
3240
|
+
|
|
3241
|
+
for (const entry of fs.readdirSync(oldSkillsDir, { withFileTypes: true })) {
|
|
3242
|
+
if (!entry.isDirectory() || !entry.name.startsWith(prefix)) continue;
|
|
3243
|
+
if (_userOwnedSkillDirs.has(entry.name)) continue;
|
|
3244
|
+
|
|
3245
|
+
const dirToRemove = path.join(oldSkillsDir, entry.name);
|
|
3246
|
+
try {
|
|
3247
|
+
// Symlink guard (mirrors cleanupWindsurfLegacyDevinSkills): never delete
|
|
3248
|
+
// through a symlinked gsd-* dir — it could escape the tree.
|
|
3249
|
+
const stat = fs.lstatSync(dirToRemove);
|
|
3250
|
+
if (stat.isSymbolicLink()) continue;
|
|
3251
|
+
|
|
3252
|
+
fs.rmSync(dirToRemove, { recursive: true, force: true });
|
|
3253
|
+
removed++;
|
|
3254
|
+
} catch (_err) {
|
|
3255
|
+
// Fail open — a single bad dir must not block install/uninstall.
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
// Prune the old skills dir if now empty — leaves the configHome clean.
|
|
3260
|
+
// Never remove a non-empty container (user may keep other content there).
|
|
3261
|
+
try {
|
|
3262
|
+
if (fs.existsSync(oldSkillsDir) && fs.readdirSync(oldSkillsDir).length === 0) {
|
|
3263
|
+
fs.rmdirSync(oldSkillsDir);
|
|
3264
|
+
}
|
|
3265
|
+
} catch (_err) {
|
|
3266
|
+
// best-effort container cleanup
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
return removed;
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
/**
|
|
3273
|
+
* When a runtime's skills kind declares an alternate `home` (split-home move),
|
|
3274
|
+
* return the now-stale configDir-rooted skills location that installs before the
|
|
3275
|
+
* move used; null when no move is in effect (no home override, or home resolves
|
|
3276
|
+
* to the same path). Descriptor-driven — no per-runtime hardcoding.
|
|
3277
|
+
* @returns {string|null}
|
|
3278
|
+
*/
|
|
3279
|
+
function _resolveMovedSkillsOldDir(runtime, targetDir, scope) {
|
|
3280
|
+
try {
|
|
3281
|
+
const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope);
|
|
3282
|
+
const skillsKind = layout.kinds.find((k) => k.kind === 'skills');
|
|
3283
|
+
if (skillsKind && skillsKind.home) {
|
|
3284
|
+
const oldDir = path.join(targetDir, skillsKind.destSubpath);
|
|
3285
|
+
const newDir = path.join(skillsKind.home, skillsKind.destSubpath);
|
|
3286
|
+
if (path.resolve(oldDir) !== path.resolve(newDir)) return oldDir;
|
|
3287
|
+
}
|
|
3288
|
+
} catch (_e) {
|
|
3289
|
+
// No migration when the layout can't resolve — never block on this.
|
|
3290
|
+
}
|
|
3291
|
+
return null;
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3269
3294
|
/**
|
|
3270
3295
|
* Generate the GSD config block for Codex config.toml.
|
|
3271
3296
|
* @param {Array<{name: string, description: string}>} agents
|
|
@@ -3281,6 +3306,17 @@ function generateCodexConfigBlock(agents, targetDir) {
|
|
|
3281
3306
|
'',
|
|
3282
3307
|
];
|
|
3283
3308
|
|
|
3309
|
+
// ADR-1239 upgrade 2 / #2088 — explicit dispatch tuning. Pin `max_depth` on the
|
|
3310
|
+
// `[agents]` (AgentsToml) table rather than relying on codex-cli's implicit
|
|
3311
|
+
// default, realizing the negotiated `dispatch.maxDepth: 1` axis. This bare
|
|
3312
|
+
// `[agents]` scalar table coexists with the flattened `[agents.<name>]` role
|
|
3313
|
+
// sub-tables below (validated by validateCodexConfigSchema, which permits a
|
|
3314
|
+
// known-scalar-only `[agents]`). Emitted before the role tables so the parent
|
|
3315
|
+
// table is opened first.
|
|
3316
|
+
lines.push('[agents]');
|
|
3317
|
+
lines.push(`max_depth = ${GSD_CODEX_AGENTS_MAX_DEPTH}`);
|
|
3318
|
+
lines.push('');
|
|
3319
|
+
|
|
3284
3320
|
for (const { name, description } of agents) {
|
|
3285
3321
|
// #2727 — Codex 0.124.0 requires [agents.<name>] struct format, not [[agents]] sequence.
|
|
3286
3322
|
// [[agents]] (introduced in #2645) is rejected by codex-cli 0.124.0 with
|
|
@@ -3294,6 +3330,52 @@ function generateCodexConfigBlock(agents, targetDir) {
|
|
|
3294
3330
|
return lines.join('\n');
|
|
3295
3331
|
}
|
|
3296
3332
|
|
|
3333
|
+
/**
|
|
3334
|
+
* Extract a user's pre-existing AgentsToml scalar assignments from a bare
|
|
3335
|
+
* `[agents]` table — every known scalar EXCEPT `max_depth` (which GSD manages
|
|
3336
|
+
* and always re-emits as 1). Returned as raw `key = value` line strings so
|
|
3337
|
+
* mergeCodexConfig can PRESERVE them in the managed block instead of silently
|
|
3338
|
+
* dropping the user's tuning when the bare `[agents]` table is purged (#2088
|
|
3339
|
+
* review finding: the loosened validator declares such a table legitimate, so
|
|
3340
|
+
* install must not destroy it). Only the first bare `[agents]` section is read;
|
|
3341
|
+
* `[agents.<name>]` role tables are ignored. Fail-open → [].
|
|
3342
|
+
* @returns {string[]}
|
|
3343
|
+
*/
|
|
3344
|
+
function extractCodexUserAgentsScalars(content) {
|
|
3345
|
+
const preserved = [];
|
|
3346
|
+
let section;
|
|
3347
|
+
try {
|
|
3348
|
+
section = getTomlTableSections(content).find((s) => !s.array && s.path === 'agents');
|
|
3349
|
+
} catch (_e) {
|
|
3350
|
+
return preserved;
|
|
3351
|
+
}
|
|
3352
|
+
if (!section) return preserved;
|
|
3353
|
+
const body = content.slice(section.headerEnd, section.end);
|
|
3354
|
+
for (const record of getTomlLineRecords(body)) {
|
|
3355
|
+
if (record.startsInMultilineString || record.tableHeader) continue;
|
|
3356
|
+
const trimmed = record.text.trim();
|
|
3357
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
3358
|
+
if (!record.keySegments || record.keySegments.length !== 1) continue;
|
|
3359
|
+
const key = record.keySegments[0];
|
|
3360
|
+
if (key === 'max_depth') continue; // GSD-managed — GSD's value wins.
|
|
3361
|
+
if (!CODEX_AGENTS_TOML_SCALAR_KEYS.has(key)) continue;
|
|
3362
|
+
preserved.push(trimmed);
|
|
3363
|
+
}
|
|
3364
|
+
return preserved;
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
/**
|
|
3368
|
+
* Splice preserved user AgentsToml scalar lines into the managed GSD config
|
|
3369
|
+
* block, immediately after the `[agents]` header and before GSD's `max_depth`
|
|
3370
|
+
* line. Operates on the pre-EOL-normalization block (LF joins), matching only
|
|
3371
|
+
* the bare `[agents]` header (never `[agents.<name>]`). Returns the block
|
|
3372
|
+
* unchanged when there is nothing to preserve or the anchor is absent.
|
|
3373
|
+
*/
|
|
3374
|
+
function spliceCodexAgentsScalars(block, scalarLines) {
|
|
3375
|
+
if (!scalarLines || scalarLines.length === 0) return block;
|
|
3376
|
+
return block.replace(/(\n\[agents\]\n)(max_depth = )/, `$1${scalarLines.join('\n')}\n$2`);
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3297
3379
|
/**
|
|
3298
3380
|
* Strip any managed GSD agent sections from a TOML string.
|
|
3299
3381
|
*
|
|
@@ -3316,6 +3398,16 @@ function stripCodexGsdAgentSections(content) {
|
|
|
3316
3398
|
return true;
|
|
3317
3399
|
}
|
|
3318
3400
|
|
|
3401
|
+
// GSD's managed `[agents]` scalar block (ADR-1239 upgrade 2 / #2088 — the
|
|
3402
|
+
// `max_depth` dispatch-tuning table). Install purges any pre-existing bare
|
|
3403
|
+
// `[agents]` and writes its own, so a known-scalar-only bare `[agents]` is
|
|
3404
|
+
// GSD-owned; strip it on uninstall. (The marker path already removes it via
|
|
3405
|
+
// the marker-to-EOF cut; this covers the no-marker fallback.)
|
|
3406
|
+
if (!section.array && section.path === 'agents') {
|
|
3407
|
+
const body = content.slice(section.headerEnd, section.end);
|
|
3408
|
+
return codexBareAgentsHasOnlyKnownScalars(body);
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3319
3411
|
// Legacy `[[agents]]` array-of-tables (#2645) — only strip blocks whose
|
|
3320
3412
|
// `name = "gsd-..."`, preserving user-authored [[agents]] entries.
|
|
3321
3413
|
if (section.array && section.path === 'agents') {
|
|
@@ -3343,7 +3435,11 @@ function stripGsdFromCodexConfig(content) {
|
|
|
3343
3435
|
const codexHooksOwnership = getManagedCodexHooksOwnership(content);
|
|
3344
3436
|
|
|
3345
3437
|
if (markerIndex !== -1) {
|
|
3346
|
-
// Has GSD marker — remove everything from marker to EOF
|
|
3438
|
+
// Has GSD marker — remove everything from marker to EOF. First recover the
|
|
3439
|
+
// user's own AgentsToml scalars (max_threads etc.) that install folded into
|
|
3440
|
+
// the managed [agents] block (#2088), so a full install→uninstall cycle
|
|
3441
|
+
// round-trips the user's tuning. GSD-managed max_depth is dropped.
|
|
3442
|
+
const preservedScalars = extractCodexUserAgentsScalars(content.slice(markerIndex));
|
|
3347
3443
|
let before = content.substring(0, markerIndex);
|
|
3348
3444
|
before = stripCodexHooksFeatureAssignments(before, codexHooksOwnership);
|
|
3349
3445
|
// Also strip GSD-injected feature keys above the marker (Case 3 inject)
|
|
@@ -3352,6 +3448,9 @@ function stripGsdFromCodexConfig(content) {
|
|
|
3352
3448
|
before = before.replace(/^\[features\]\s*\n(?=\[|$)/m, '');
|
|
3353
3449
|
before = before.replace(/^\[agents\]\s*\n(?=\[|$)/m, '');
|
|
3354
3450
|
before = before.replace(/^(?:\r?\n)+/, '').trimEnd();
|
|
3451
|
+
if (preservedScalars.length > 0) {
|
|
3452
|
+
before = (before ? before + eol + eol : '') + '[agents]' + eol + preservedScalars.join(eol);
|
|
3453
|
+
}
|
|
3355
3454
|
if (!before) return null;
|
|
3356
3455
|
return before + eol;
|
|
3357
3456
|
}
|
|
@@ -3362,7 +3461,11 @@ function stripGsdFromCodexConfig(content) {
|
|
|
3362
3461
|
cleaned = cleaned.replace(/^multi_agent\s*=\s*true\s*(?:\r?\n)?/m, '');
|
|
3363
3462
|
cleaned = cleaned.replace(/^default_mode_request_user_input\s*=\s*true\s*(?:\r?\n)?/m, '');
|
|
3364
3463
|
|
|
3365
|
-
//
|
|
3464
|
+
// #2088: recover the user's own AgentsToml scalars before the [agents] table is
|
|
3465
|
+
// stripped, so they survive uninstall even in the no-marker fallback path.
|
|
3466
|
+
const preservedScalars = extractCodexUserAgentsScalars(cleaned);
|
|
3467
|
+
|
|
3468
|
+
// Remove [agents.gsd-*] sections + the managed known-scalar [agents] table.
|
|
3366
3469
|
cleaned = stripCodexGsdAgentSections(cleaned);
|
|
3367
3470
|
|
|
3368
3471
|
// Remove [features] section if now empty (only header, no keys before next section)
|
|
@@ -3373,6 +3476,10 @@ function stripGsdFromCodexConfig(content) {
|
|
|
3373
3476
|
|
|
3374
3477
|
cleaned = cleaned.replace(/^(?:\r?\n)+/, '').trimEnd();
|
|
3375
3478
|
|
|
3479
|
+
if (preservedScalars.length > 0) {
|
|
3480
|
+
cleaned = (cleaned ? cleaned + eol + eol : '') + '[agents]' + eol + preservedScalars.join(eol);
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3376
3483
|
if (!cleaned) return null;
|
|
3377
3484
|
return cleaned + eol;
|
|
3378
3485
|
}
|
|
@@ -4844,6 +4951,33 @@ function parseTomlToObject(content) {
|
|
|
4844
4951
|
* - `hooks.<Event>` MUST be an array of tables when present (Codex ≥0.124
|
|
4845
4952
|
* rejects bare `[hooks.<Event>]` single-bracket maps).
|
|
4846
4953
|
*/
|
|
4954
|
+
/**
|
|
4955
|
+
* True when a bare `[agents]` table body contains ONLY known AgentsToml scalar
|
|
4956
|
+
* keys (CODEX_AGENTS_TOML_SCALAR_KEYS) — i.e. it is a valid AgentsToml struct
|
|
4957
|
+
* that Codex's `deny_unknown_fields` will accept, not the break-causing form
|
|
4958
|
+
* (#2760) that carries an unknown key. Comments and blank lines are ignored; an
|
|
4959
|
+
* empty body is trivially valid. Mirrors isLegacyGsdAgentsSection's line scan.
|
|
4960
|
+
*/
|
|
4961
|
+
function codexBareAgentsHasOnlyKnownScalars(body) {
|
|
4962
|
+
const lineRecords = getTomlLineRecords(body);
|
|
4963
|
+
for (const record of lineRecords) {
|
|
4964
|
+
// Conservative reject of anything not positively a single known-scalar
|
|
4965
|
+
// assignment. A multiline-string value cannot be a valid AgentsToml scalar
|
|
4966
|
+
// (max_threads/max_depth/job_max_runtime_seconds are integers,
|
|
4967
|
+
// interrupt_message is a bool — none are strings), so codex would reject it
|
|
4968
|
+
// too; rejecting here is correct, not a false negative.
|
|
4969
|
+
if (record.startsInMultilineString) return false;
|
|
4970
|
+
if (record.tableHeader) return false;
|
|
4971
|
+
const trimmed = record.text.trim();
|
|
4972
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
4973
|
+
if (!record.keySegments || record.keySegments.length !== 1 ||
|
|
4974
|
+
!CODEX_AGENTS_TOML_SCALAR_KEYS.has(record.keySegments[0])) {
|
|
4975
|
+
return false;
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
return true;
|
|
4979
|
+
}
|
|
4980
|
+
|
|
4847
4981
|
function validateCodexConfigSchema(content) {
|
|
4848
4982
|
let parsed;
|
|
4849
4983
|
try {
|
|
@@ -4872,10 +5006,21 @@ function validateCodexConfigSchema(content) {
|
|
|
4872
5006
|
}
|
|
4873
5007
|
|
|
4874
5008
|
if (!section.array && section.path === 'agents') {
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
5009
|
+
// #2760 rejected ALL bare `[agents]` tables because a bare table holding a
|
|
5010
|
+
// non-AgentsToml key (`default = "x"`, a role name, etc.) triggers Codex's
|
|
5011
|
+
// "invalid type: ..., expected struct AgentsToml" and breaks every CLI
|
|
5012
|
+
// invocation. But a bare `[agents]` whose keys are all valid AgentsToml
|
|
5013
|
+
// scalars (max_depth/max_threads/...) IS a valid struct — that is exactly
|
|
5014
|
+
// GSD's managed `max_depth` dispatch-tuning block (ADR-1239 upgrade 2 /
|
|
5015
|
+
// #2088), and a user's own scalar tuning. Permit known-scalar-only; still
|
|
5016
|
+
// reject any bare `[agents]` carrying an unknown key.
|
|
5017
|
+
const body = content.slice(section.headerEnd, section.end);
|
|
5018
|
+
if (!codexBareAgentsHasOnlyKnownScalars(body)) {
|
|
5019
|
+
return {
|
|
5020
|
+
ok: false,
|
|
5021
|
+
reason: 'bare [agents] table with a non-AgentsToml key is invalid in current Codex schema (expected [agents.<name>] struct form, or only AgentsToml scalars like max_depth/max_threads)',
|
|
5022
|
+
};
|
|
5023
|
+
}
|
|
4879
5024
|
}
|
|
4880
5025
|
|
|
4881
5026
|
// hooks.state.* is Codex's persistent hook-trust namespace (added in
|
|
@@ -5151,7 +5296,13 @@ function mergeCodexConfig(configPath, gsdBlock) {
|
|
|
5151
5296
|
|
|
5152
5297
|
const existing = fs.readFileSync(configPath, 'utf8');
|
|
5153
5298
|
const eol = detectLineEnding(existing);
|
|
5154
|
-
|
|
5299
|
+
// #2088 review: the bare `[agents]` table is purged below (Case 2/3 via
|
|
5300
|
+
// stripLeakedGsdCodexSections) to keep a single managed `[agents]`. Preserve
|
|
5301
|
+
// the user's own AgentsToml scalar tuning (max_threads, job_max_runtime_seconds,
|
|
5302
|
+
// interrupt_message — everything except GSD-managed max_depth) by re-emitting
|
|
5303
|
+
// it inside the managed block, so install never silently drops it.
|
|
5304
|
+
const mergedGsdBlock = spliceCodexAgentsScalars(gsdBlock, extractCodexUserAgentsScalars(existing));
|
|
5305
|
+
const normalizedGsdBlock = mergedGsdBlock.replace(/\r?\n/g, eol);
|
|
5155
5306
|
const markerIndex = existing.indexOf(GSD_CODEX_MARKER);
|
|
5156
5307
|
|
|
5157
5308
|
// Case 2: Has GSD marker — truncate and re-append
|
|
@@ -6105,62 +6256,14 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
|
|
|
6105
6256
|
// convertClaudeCommandToKiloSkill: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
6106
6257
|
// Imported from installEngine above.
|
|
6107
6258
|
|
|
6108
|
-
/**
|
|
6109
|
-
* Copy commands to a flat structure for OpenCode
|
|
6110
|
-
* OpenCode expects: command/gsd-help.md (invoked as /gsd-help)
|
|
6111
|
-
* Source structure: commands/gsd/help.md
|
|
6112
|
-
*
|
|
6113
|
-
* @param {string} srcDir - Source directory (e.g., commands/gsd/)
|
|
6114
|
-
* @param {string} destDir - Destination directory (e.g., command/)
|
|
6115
|
-
* @param {string} prefix - Prefix for filenames (e.g., 'gsd')
|
|
6116
|
-
* @param {string} pathPrefix - Path prefix for file references
|
|
6117
|
-
* @param {string} runtime - Target runtime ('claude', 'opencode', or 'kilo')
|
|
6118
|
-
*/
|
|
6119
6259
|
// applyOpencodeFamilyPathPrefix: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
6120
6260
|
// Imported from installEngine above.
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
// Remove old gsd-*.md files before copying new ones
|
|
6128
|
-
if (fs.existsSync(destDir)) {
|
|
6129
|
-
for (const file of fs.readdirSync(destDir)) {
|
|
6130
|
-
if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
|
|
6131
|
-
fs.unlinkSync(path.join(destDir, file));
|
|
6132
|
-
}
|
|
6133
|
-
}
|
|
6134
|
-
} else {
|
|
6135
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
6136
|
-
}
|
|
6137
|
-
|
|
6138
|
-
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
6139
|
-
|
|
6140
|
-
for (const entry of entries) {
|
|
6141
|
-
const srcPath = path.join(srcDir, entry.name);
|
|
6142
|
-
|
|
6143
|
-
if (entry.isDirectory()) {
|
|
6144
|
-
// Recurse into subdirectories, adding to prefix
|
|
6145
|
-
// e.g., commands/gsd/debug/start.md -> command/gsd-debug-start.md
|
|
6146
|
-
copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
|
|
6147
|
-
} else if (entry.name.endsWith('.md')) {
|
|
6148
|
-
// Flatten: help.md -> gsd-help.md
|
|
6149
|
-
const baseName = entry.name.replace('.md', '');
|
|
6150
|
-
const destName = `${prefix}-${baseName}.md`;
|
|
6151
|
-
const destPath = path.join(destDir, destName);
|
|
6152
|
-
|
|
6153
|
-
let content = fs.readFileSync(srcPath, 'utf8');
|
|
6154
|
-
content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix);
|
|
6155
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6156
|
-
content = runtime === 'kilo'
|
|
6157
|
-
? convertClaudeToKiloFrontmatter(content)
|
|
6158
|
-
: convertClaudeToOpencodeFrontmatter(content);
|
|
6159
|
-
|
|
6160
|
-
fs.writeFileSync(destPath, content);
|
|
6161
|
-
}
|
|
6162
|
-
}
|
|
6163
|
-
}
|
|
6261
|
+
//
|
|
6262
|
+
// copyFlattenedCommands (OpenCode/Kilo flattened command/ writer): moved to
|
|
6263
|
+
// src/install-engine.cts as installOpencodeFamilyCommands (ADR-1239 / #2087).
|
|
6264
|
+
// OpenCode/Kilo installs now route through installRuntimeArtifacts's
|
|
6265
|
+
// combinedFamilyInstall path (installOpencodeFamilyArtifacts) instead of the
|
|
6266
|
+
// bespoke inline block that used to call this function.
|
|
6164
6267
|
|
|
6165
6268
|
function listCodexSkillNames(skillsDir, prefix = 'gsd-') {
|
|
6166
6269
|
if (!fs.existsSync(skillsDir)) return [];
|
|
@@ -6462,7 +6565,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
6462
6565
|
// copyWithPathReplacement is the emit path for gsd-core/workflows/*.md;
|
|
6463
6566
|
// _applyRuntimeRewrites is NOT invoked here, so this is what makes the fix
|
|
6464
6567
|
// live in real installs (it is a no-op for files without those lines).
|
|
6465
|
-
if (runtime
|
|
6568
|
+
if (!_hostBehaviors(runtime).authorsCanonicalWorkflow) {
|
|
6466
6569
|
content = _stampNonClaudeRuntimeDefaults(content, runtime);
|
|
6467
6570
|
}
|
|
6468
6571
|
|
|
@@ -6664,16 +6767,20 @@ const GSD_UNINSTALL_HOOKS = [
|
|
|
6664
6767
|
* @param {boolean} isGlobal - Whether to uninstall from global or local
|
|
6665
6768
|
* @param {string} runtime - Target runtime ('claude', 'opencode', 'codex', 'copilot')
|
|
6666
6769
|
*/
|
|
6667
|
-
function uninstall(isGlobal, runtime =
|
|
6770
|
+
function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
6668
6771
|
const { isOpencode, isKilo, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
6669
6772
|
const dirName = getDirName(runtime);
|
|
6670
6773
|
|
|
6671
6774
|
// Get the target directory based on runtime and install type. Cline local
|
|
6672
6775
|
// installs write to the project root (.clinerules/ lives at the root, not in
|
|
6673
6776
|
// a .cline/ subdir), mirroring the install() path resolution (#787).
|
|
6777
|
+
// Descriptor-driven (ADR-1239 / #2090): cline local installs write to the
|
|
6778
|
+
// project root (.clinerules/ lives at the root, not in a .cline/ subdir),
|
|
6779
|
+
// mirroring the install() path resolution (#787). Folded from a hardcoded
|
|
6780
|
+
// `runtime === 'cline'` branch into hostBehaviors.localTargetIsProjectRoot.
|
|
6674
6781
|
const targetDir = isGlobal
|
|
6675
6782
|
? getGlobalConfigDir(runtime, explicitConfigDir)
|
|
6676
|
-
: runtime
|
|
6783
|
+
: _hostBehaviors(runtime).localTargetIsProjectRoot
|
|
6677
6784
|
? process.cwd()
|
|
6678
6785
|
: path.join(process.cwd(), dirName);
|
|
6679
6786
|
|
|
@@ -6722,11 +6829,34 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6722
6829
|
|
|
6723
6830
|
// 1. Remove GSD commands/skills (layout-driven)
|
|
6724
6831
|
const scope = isGlobal ? 'global' : 'local';
|
|
6725
|
-
|
|
6832
|
+
// ADR-1239 / #2086: drive uninstall through the public Host-Integration Interface.
|
|
6833
|
+
// Fail-open to the engine directly if the composed-registry adapter can't load.
|
|
6834
|
+
const _uninstallAdapter = _runtimeAdapter(runtime);
|
|
6835
|
+
if (_uninstallAdapter) {
|
|
6836
|
+
_uninstallAdapter.uninstall({ configDir: targetDir, scope });
|
|
6837
|
+
} else {
|
|
6838
|
+
uninstallRuntimeArtifacts(runtime, targetDir, scope);
|
|
6839
|
+
}
|
|
6726
6840
|
removedCount++;
|
|
6727
6841
|
|
|
6842
|
+
// ADR-1239 split-home migration: the adapter/plan uninstall targets the new
|
|
6843
|
+
// `home` location (e.g. Codex → ~/.agents/skills). A user who installed
|
|
6844
|
+
// BEFORE the move and never reinstalled still has managed gsd-* skill dirs at
|
|
6845
|
+
// the old configDir-rooted location (~/.codex/skills) — remove those too so
|
|
6846
|
+
// uninstall leaves nothing behind. User-owned content is preserved.
|
|
6847
|
+
{
|
|
6848
|
+
const _movedOldSkillsDir = _resolveMovedSkillsOldDir(runtime, targetDir, scope);
|
|
6849
|
+
if (_movedOldSkillsDir) {
|
|
6850
|
+
const migrated = cleanupMovedSkillsOldLocation(_movedOldSkillsDir, 'gsd-');
|
|
6851
|
+
if (migrated > 0) {
|
|
6852
|
+
removedCount++;
|
|
6853
|
+
console.log(` ${green}✓${reset} Removed ${migrated} legacy skill dir(s) from ${_movedOldSkillsDir}`);
|
|
6854
|
+
}
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
|
|
6728
6858
|
// 1a. Non-layout Codex side-effects: agent .toml files, config.toml sections, hooks.json
|
|
6729
|
-
if (
|
|
6859
|
+
if (_hostBehaviors(runtime).tomlConfigInstall) {
|
|
6730
6860
|
const codexAgentsDir = path.join(targetDir, 'agents');
|
|
6731
6861
|
if (fs.existsSync(codexAgentsDir)) {
|
|
6732
6862
|
const tomlFiles = fs.readdirSync(codexAgentsDir);
|
|
@@ -6765,8 +6895,10 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6765
6895
|
console.log(` ${green}✓${reset} Removed managed Codex SessionStart hook from hooks.json`);
|
|
6766
6896
|
}
|
|
6767
6897
|
|
|
6768
|
-
// #772: remove
|
|
6769
|
-
|
|
6898
|
+
// #772/#2088: remove every managed Codex extended hook-event registration.
|
|
6899
|
+
// Shares CODEX_EXTENDED_HOOK_EVENTS with the install loop — removal set ==
|
|
6900
|
+
// registration set, so no managed event is ever orphaned.
|
|
6901
|
+
for (const eventName of CODEX_EXTENDED_HOOK_EVENTS) {
|
|
6770
6902
|
const eventCleanup = removeCodexHooksJsonEvent(targetDir, eventName);
|
|
6771
6903
|
if (eventCleanup.changed) {
|
|
6772
6904
|
removedCount++;
|
|
@@ -6813,7 +6945,9 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6813
6945
|
// 1b-cline. Non-layout Cline side-effects (issue #787): remove the
|
|
6814
6946
|
// directory-form rules + PreToolUse hook, and strip the GSD block from the
|
|
6815
6947
|
// global cross-tool ~/.agents/AGENTS.md target.
|
|
6816
|
-
|
|
6948
|
+
// Descriptor-driven (ADR-1239 / #2090): folded from `runtime === 'cline'`
|
|
6949
|
+
// into hostBehaviors.clineRulesSurface.
|
|
6950
|
+
if (_hostBehaviors(runtime).clineRulesSurface) {
|
|
6817
6951
|
const clinerulesDir = path.join(targetDir, '.clinerules');
|
|
6818
6952
|
for (const rel of ['gsd.md', path.join('hooks', 'PreToolUse')]) {
|
|
6819
6953
|
const p = path.join(clinerulesDir, rel);
|
|
@@ -6859,17 +6993,20 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6859
6993
|
}
|
|
6860
6994
|
}
|
|
6861
6995
|
|
|
6862
|
-
// 1b-cursor.
|
|
6863
|
-
// hook entries from hooks.json and clean up the managed hook
|
|
6864
|
-
|
|
6996
|
+
// 1b-cursor. Descriptor-driven hook-bus cleanup (ADR-1239 / #2089): remove
|
|
6997
|
+
// GSD-managed hook entries from hooks.json and clean up the managed hook
|
|
6998
|
+
// scripts. Gated by the hostBehaviors.hooksJsonSurface descriptor axis, not a
|
|
6999
|
+
// hardcoded `isCursor` branch.
|
|
7000
|
+
if (_hostBehaviors(runtime).hooksJsonSurface) {
|
|
6865
7001
|
const hooksJsonCleanup = removeCursorHooksJson(targetDir);
|
|
6866
7002
|
if (hooksJsonCleanup.changed) {
|
|
6867
7003
|
removedCount++;
|
|
6868
7004
|
console.log(` ${green}✓${reset} Removed GSD-managed Cursor hooks from hooks.json`);
|
|
6869
7005
|
}
|
|
6870
|
-
// Remove
|
|
7006
|
+
// Remove all GSD-managed hook scripts (sessionStart, postToolUse, preToolUse,
|
|
7007
|
+
// stop, subagentStart, subagentStop — AC4a, #2089).
|
|
6871
7008
|
const hooksDir = path.join(targetDir, 'hooks');
|
|
6872
|
-
for (const script of
|
|
7009
|
+
for (const script of GSD_CURSOR_HOOK_SCRIPTS) {
|
|
6873
7010
|
const p = path.join(hooksDir, script);
|
|
6874
7011
|
try {
|
|
6875
7012
|
if (fs.existsSync(p)) {
|
|
@@ -6888,7 +7025,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6888
7025
|
|
|
6889
7026
|
// 1c. Claude local: remove flat gsd-*.md commands from commands/ (current layout,
|
|
6890
7027
|
// #1367 fix). Also remove legacy commands/gsd/ subdirectory from prior installs.
|
|
6891
|
-
if (!isGlobal && runtime === '
|
|
7028
|
+
if (!isGlobal && _hostBehaviors(runtime).localInstallStyle === 'legacy-flat') {
|
|
6892
7029
|
const commandsDir = path.join(targetDir, 'commands');
|
|
6893
7030
|
// Remove flat gsd-*.md files (current layout after #1367 fix)
|
|
6894
7031
|
if (fs.existsSync(commandsDir)) {
|
|
@@ -6930,7 +7067,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6930
7067
|
// removes the directory; we must preserve/restore user artifacts before that path.
|
|
6931
7068
|
// This block runs AFTER uninstallRuntimeArtifacts, so we check if the directory
|
|
6932
7069
|
// was already removed and skip if so (idempotent).
|
|
6933
|
-
if (isQwen ||
|
|
7070
|
+
if (isQwen || _hostBehaviors(runtime).legacyCommandsGsdCleanup === true) {
|
|
6934
7071
|
// dev-preferences may have survived in skills/ as SKILL.md — nothing to do for
|
|
6935
7072
|
// that case. If a stale commands/gsd/ still exists (e.g. legacy was not removed),
|
|
6936
7073
|
// attempt migration. In practice _runLegacyUninstallCleanup removes it first,
|
|
@@ -7043,9 +7180,10 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
7043
7180
|
// 4z. Remove the OpenCode native plugin adapter (#1914). Only GSD's own
|
|
7044
7181
|
// plugin file is removed; the plugins/ dir is pruned only if it becomes
|
|
7045
7182
|
// empty, preserving any user-authored OpenCode plugins.
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
const
|
|
7183
|
+
const _np = _hostBehaviors(runtime).nativePlugin;
|
|
7184
|
+
if (_np) {
|
|
7185
|
+
const pluginsDir = path.join(targetDir, _np.dir);
|
|
7186
|
+
const pluginPath = path.join(pluginsDir, _np.file);
|
|
7049
7187
|
if (fs.existsSync(pluginPath)) {
|
|
7050
7188
|
try {
|
|
7051
7189
|
fs.unlinkSync(pluginPath);
|
|
@@ -7191,7 +7329,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
7191
7329
|
// to preserve any user-added allow/deny entries.
|
|
7192
7330
|
// Uses a local flag to avoid the shared `settingsModified` producing a false
|
|
7193
7331
|
// "Removed GSD permissions" message when only hooks/statusline changed.
|
|
7194
|
-
if (runtime === 'claude' && settings.permissions) {
|
|
7332
|
+
if (_hostBehaviors(runtime).permissionsSchema === 'claude' && settings.permissions) {
|
|
7195
7333
|
let permissionsModified = false;
|
|
7196
7334
|
if (Array.isArray(settings.permissions.allow)) {
|
|
7197
7335
|
const before = settings.permissions.allow.length;
|
|
@@ -7224,7 +7362,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
7224
7362
|
}
|
|
7225
7363
|
|
|
7226
7364
|
// 6. For OpenCode, clean up permissions from opencode.json or opencode.jsonc
|
|
7227
|
-
if (
|
|
7365
|
+
if (resolveInstallPlan(runtime).finishPermissionWriter === 'opencode') {
|
|
7228
7366
|
const configPath = resolveOpencodeConfigPath(targetDir);
|
|
7229
7367
|
if (fs.existsSync(configPath)) {
|
|
7230
7368
|
try {
|
|
@@ -7666,19 +7804,24 @@ function resolveInstallRelativePath(baseDir, relPath) {
|
|
|
7666
7804
|
/**
|
|
7667
7805
|
* Write file manifest after installation for future modification detection
|
|
7668
7806
|
*/
|
|
7669
|
-
function writeManifest(configDir, runtime =
|
|
7807
|
+
function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
7670
7808
|
const { isOpencode, isKilo, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
7671
7809
|
const gsdDir = path.join(configDir, 'gsd-core');
|
|
7672
7810
|
// #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/).
|
|
7673
7811
|
// Claude local uses flatCommandsDir instead for manifest recording.
|
|
7674
7812
|
const flatCommandsDir = path.join(configDir, 'commands');
|
|
7675
|
-
const opencodeCommandDir = path.join(configDir, 'command');
|
|
7676
|
-
// Hermes nests GSD skills under skills/gsd/ as a single category (#2841)
|
|
7813
|
+
const opencodeCommandDir = path.join(configDir, _hostBehaviors(runtime).flatCommandDir || 'command');
|
|
7814
|
+
// Hermes nests GSD skills under skills/gsd/ as a single category (#2841) —
|
|
7815
|
+
// already encoded in its layout descriptor's destSubpath ('skills/gsd').
|
|
7677
7816
|
// All other runtimes that use the Codex-style skills layout use a flat skills/ root.
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7817
|
+
// ADR-1239 upgrade 3 (#2088): honor a skills-kind `home` override (e.g. Codex
|
|
7818
|
+
// skills -> $HOME/.agents/skills instead of configDir/skills) via the same
|
|
7819
|
+
// descriptor-driven helper used by the snapshot/rollback/verification paths,
|
|
7820
|
+
// so the manifest records what's actually on disk. _resolveSkillsRootDir already
|
|
7821
|
+
// resolves destSubpath (which includes hermes's 'skills/gsd' nesting) — do not
|
|
7822
|
+
// re-append 'gsd' or the hermes dir gets double-nested to skills/gsd/gsd.
|
|
7823
|
+
const codexSkillsDir = _resolveSkillsRootDir(runtime, configDir, options.scope === 'local' ? 'local' : 'global');
|
|
7824
|
+
const codexSkillsManifestPrefix = _hostBehaviors(runtime).skillsManifestPrefix || 'skills/';
|
|
7682
7825
|
const agentsDir = path.join(configDir, 'agents');
|
|
7683
7826
|
const manifest = {
|
|
7684
7827
|
version: pkg.version,
|
|
@@ -7704,21 +7847,21 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
7704
7847
|
// Claude local (#1367): flat gsd-*.md files at commands/ level.
|
|
7705
7848
|
// Only claude local writes gsd-*.md here; global installs don't emit commands,
|
|
7706
7849
|
// so this branch is a no-op for global (no matching files to find).
|
|
7707
|
-
if (runtime === '
|
|
7850
|
+
if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && fs.existsSync(flatCommandsDir)) {
|
|
7708
7851
|
for (const file of fs.readdirSync(flatCommandsDir)) {
|
|
7709
7852
|
if (file.startsWith('gsd-') && file.endsWith('.md')) {
|
|
7710
7853
|
manifest.files['commands/' + file] = fileHash(path.join(flatCommandsDir, file));
|
|
7711
7854
|
}
|
|
7712
7855
|
}
|
|
7713
7856
|
}
|
|
7714
|
-
if ((
|
|
7857
|
+
if (_hostBehaviors(runtime).flatCommandDir && fs.existsSync(opencodeCommandDir)) {
|
|
7715
7858
|
for (const file of fs.readdirSync(opencodeCommandDir)) {
|
|
7716
7859
|
if (file.startsWith('gsd-') && file.endsWith('.md')) {
|
|
7717
7860
|
manifest.files['command/' + file] = fileHash(path.join(opencodeCommandDir, file));
|
|
7718
7861
|
}
|
|
7719
7862
|
}
|
|
7720
7863
|
}
|
|
7721
|
-
if ((
|
|
7864
|
+
if (!_hostBehaviors(runtime).skipCodexSkillsManifest && fs.existsSync(codexSkillsDir)) {
|
|
7722
7865
|
// All runtimes (including Hermes post-#947) use the canonical 'gsd-' prefix.
|
|
7723
7866
|
const skillListPrefix = 'gsd-';
|
|
7724
7867
|
for (const skillName of listCodexSkillNames(codexSkillsDir, skillListPrefix)) {
|
|
@@ -7728,8 +7871,8 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
7728
7871
|
manifest.files[`${codexSkillsManifestPrefix}${skillName}/${rel}`] = hash;
|
|
7729
7872
|
}
|
|
7730
7873
|
}
|
|
7731
|
-
//
|
|
7732
|
-
if (
|
|
7874
|
+
// Descriptor-driven (#2090): hash the category DESCRIPTION.md so reinstall detects drift.
|
|
7875
|
+
if (_hostBehaviors(runtime).trackCategoryDescription) {
|
|
7733
7876
|
const descPath = path.join(codexSkillsDir, 'DESCRIPTION.md');
|
|
7734
7877
|
if (fs.existsSync(descPath)) {
|
|
7735
7878
|
manifest.files['skills/gsd/DESCRIPTION.md'] = fileHash(descPath);
|
|
@@ -7755,7 +7898,9 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
7755
7898
|
// Track Cline directory-form artifacts in the manifest (issue #787): the
|
|
7756
7899
|
// rules file and the PreToolUse hook. (~/.agents/AGENTS.md is tracked via its
|
|
7757
7900
|
// marker block, not the per-configDir manifest, since it lives outside it.)
|
|
7758
|
-
|
|
7901
|
+
// Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
|
|
7902
|
+
// hostBehaviors.clineRulesSurface.
|
|
7903
|
+
if (_hostBehaviors(runtime).clineRulesSurface) {
|
|
7759
7904
|
for (const rel of ['.clinerules/gsd.md', '.clinerules/hooks/PreToolUse']) {
|
|
7760
7905
|
const dest = path.join(configDir, rel);
|
|
7761
7906
|
if (fs.existsSync(dest)) {
|
|
@@ -7766,7 +7911,9 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
7766
7911
|
|
|
7767
7912
|
// Track hook files so saveLocalPatches() can detect user modifications
|
|
7768
7913
|
// Hooks are only installed for runtimes that use settings.json (not Codex/Copilot/Cline)
|
|
7769
|
-
|
|
7914
|
+
// Descriptor-driven (ADR-1239 / #2089+#2090): cline's exclusion is via
|
|
7915
|
+
// hostBehaviors.skipSharedHooksInstall (was hardcoded !isCline).
|
|
7916
|
+
if (!isCodex && !isCopilot && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf && !isTrae && !isKimi) {
|
|
7770
7917
|
const hooksDir = path.join(configDir, 'hooks');
|
|
7771
7918
|
if (fs.existsSync(hooksDir)) {
|
|
7772
7919
|
// Drive from INSTALLED_HOOK_FILES (the canonical HOOKS_TO_COPY set from
|
|
@@ -7830,10 +7977,11 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
7830
7977
|
|
|
7831
7978
|
// Track the OpenCode native plugin adapter (#1914) so update/drift detection
|
|
7832
7979
|
// and uninstall can account for it.
|
|
7833
|
-
|
|
7834
|
-
|
|
7980
|
+
const _npM = _hostBehaviors(runtime).nativePlugin;
|
|
7981
|
+
if (_npM) {
|
|
7982
|
+
const pluginInstallPath = path.join(configDir, _npM.dir, _npM.file);
|
|
7835
7983
|
if (fs.existsSync(pluginInstallPath)) {
|
|
7836
|
-
manifest.files[
|
|
7984
|
+
manifest.files[`${_npM.dir}/${_npM.file}`] = fileHash(pluginInstallPath);
|
|
7837
7985
|
}
|
|
7838
7986
|
}
|
|
7839
7987
|
|
|
@@ -8117,7 +8265,7 @@ function saveLocalPatches(configDir, pristineCtx) {
|
|
|
8117
8265
|
/**
|
|
8118
8266
|
* After install, report backed-up patches for user to reapply.
|
|
8119
8267
|
*/
|
|
8120
|
-
function reportLocalPatches(configDir, runtime =
|
|
8268
|
+
function reportLocalPatches(configDir, runtime = DEFAULT_RUNTIME) {
|
|
8121
8269
|
const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
|
|
8122
8270
|
const metaPath = path.join(patchesDir, 'backup-meta.json');
|
|
8123
8271
|
if (!fs.existsSync(metaPath)) return [];
|
|
@@ -8126,15 +8274,11 @@ function reportLocalPatches(configDir, runtime = 'claude') {
|
|
|
8126
8274
|
try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; }
|
|
8127
8275
|
|
|
8128
8276
|
if (meta.files && meta.files.length > 0) {
|
|
8129
|
-
const reapplyCommand = (runtime
|
|
8130
|
-
?
|
|
8131
|
-
: runtime === '
|
|
8132
|
-
? '
|
|
8133
|
-
:
|
|
8134
|
-
? 'gsd-update --reapply (mention the skill name)'
|
|
8135
|
-
: runtime === 'kimi'
|
|
8136
|
-
? '/skill:gsd-update --reapply'
|
|
8137
|
-
: '/gsd-update --reapply';
|
|
8277
|
+
const reapplyCommand = _hostBehaviors(runtime).reapplyCommand
|
|
8278
|
+
? _hostBehaviors(runtime).reapplyCommand
|
|
8279
|
+
: runtime === 'kimi'
|
|
8280
|
+
? '/skill:gsd-update --reapply'
|
|
8281
|
+
: '/gsd-update --reapply';
|
|
8138
8282
|
console.log('');
|
|
8139
8283
|
console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
|
|
8140
8284
|
for (const f of meta.files) {
|
|
@@ -8160,8 +8304,8 @@ function reportInstallerMigrationResult(result) {
|
|
|
8160
8304
|
}
|
|
8161
8305
|
}
|
|
8162
8306
|
|
|
8163
|
-
function install(isGlobal, runtime =
|
|
8164
|
-
const { isOpencode, isKilo, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
8307
|
+
function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
8308
|
+
const { isOpencode, isKilo, isZcode, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
8165
8309
|
const plan = resolveInstallPlan(runtime);
|
|
8166
8310
|
const dirName = getDirName(runtime);
|
|
8167
8311
|
const src = path.join(__dirname, '..');
|
|
@@ -8214,15 +8358,17 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8214
8358
|
};
|
|
8215
8359
|
|
|
8216
8360
|
// Get the target directory based on runtime and install type.
|
|
8217
|
-
//
|
|
8218
|
-
// lives at the root, not inside
|
|
8361
|
+
// Descriptor-driven (ADR-1239 / #2090): cline local installs write to the
|
|
8362
|
+
// project root (like Claude Code) — .clinerules lives at the root, not inside
|
|
8363
|
+
// a .cline/ subdirectory. Folded from `isCline` into
|
|
8364
|
+
// hostBehaviors.localTargetIsProjectRoot.
|
|
8219
8365
|
// #791: antigravity local installs write to .agents/ (canonical). The legacy .agent/
|
|
8220
8366
|
// directory is recognized by RUNTIME_DIRS (update-context) and _LEGACY_SCAN_SUBDIR_NAMES
|
|
8221
8367
|
// but NOT auto-removed here; legacy .agent/ gsd artifacts are recognized but not
|
|
8222
8368
|
// auto-removed on reinstall (dual-read fallback per issue #791 spec).
|
|
8223
8369
|
const targetDir = isGlobal
|
|
8224
8370
|
? getGlobalConfigDir(runtime, explicitConfigDir)
|
|
8225
|
-
:
|
|
8371
|
+
: _hostBehaviors(runtime).localTargetIsProjectRoot
|
|
8226
8372
|
? process.cwd()
|
|
8227
8373
|
: path.join(process.cwd(), dirName);
|
|
8228
8374
|
|
|
@@ -8301,7 +8447,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8301
8447
|
const isWindowsHost = process.platform === 'win32';
|
|
8302
8448
|
const pathPrefix = computePathPrefix({
|
|
8303
8449
|
isGlobal,
|
|
8304
|
-
isOpencode,
|
|
8450
|
+
isOpencode: _hostBehaviors(runtime).skipHomePrefixSubstitution === true,
|
|
8305
8451
|
isWindowsHost,
|
|
8306
8452
|
resolvedTarget,
|
|
8307
8453
|
homeDir,
|
|
@@ -8367,8 +8513,8 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8367
8513
|
// Map<filename, Buffer> — content snapshot of each pre-existing gsd-* agent file.
|
|
8368
8514
|
const codexPreInstallAgentContents = new Map();
|
|
8369
8515
|
let codexPreInstallVersionBytes = null;
|
|
8370
|
-
if (
|
|
8371
|
-
const _preSkillsDir =
|
|
8516
|
+
if (_hostBehaviors(runtime).tomlConfigInstall && !isMinimalMode(_effectiveInstallMode)) {
|
|
8517
|
+
const _preSkillsDir = _resolveSkillsRootDir(runtime, targetDir, isGlobal ? 'global' : 'local');
|
|
8372
8518
|
if (fs.existsSync(_preSkillsDir)) {
|
|
8373
8519
|
for (const entry of fs.readdirSync(_preSkillsDir, { withFileTypes: true })) {
|
|
8374
8520
|
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
|
@@ -8421,10 +8567,10 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8421
8567
|
// atomic-write temp files. It is safe to call before any writes have happened.
|
|
8422
8568
|
// The full restoreCodexSnapshot() (defined inside the config block) additionally
|
|
8423
8569
|
// handles config.toml, which is not yet touched at this point in the pipeline.
|
|
8424
|
-
const _codexPreConfigRollback = !
|
|
8570
|
+
const _codexPreConfigRollback = !_hostBehaviors(runtime).tomlConfigInstall || isMinimalMode(_effectiveInstallMode) ? null : () => {
|
|
8425
8571
|
rollbackInstallerMigrations();
|
|
8426
8572
|
// skills/gsd-* — pass 1: restore snapshot entries (may be absent if deleted mid-install).
|
|
8427
|
-
const _earlySkillsDir =
|
|
8573
|
+
const _earlySkillsDir = _resolveSkillsRootDir(runtime, targetDir, isGlobal ? 'global' : 'local');
|
|
8428
8574
|
for (const skillName of codexPreInstallSkillNames) {
|
|
8429
8575
|
const skillDirPath = path.join(_earlySkillsDir, skillName);
|
|
8430
8576
|
const fileMap = codexPreInstallSkillContents.get(skillName);
|
|
@@ -8587,7 +8733,6 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8587
8733
|
// Hermes: writeHermesCategoryDescription (not a layout kind)
|
|
8588
8734
|
// Cline global: skills emitted via layout; .clinerules still written below (#782)
|
|
8589
8735
|
// Cline local: no skills (only .clinerules) — falls through to cline-rules surface
|
|
8590
|
-
// OpenCode/Kilo: copyFlattenedCommands (frontmatter conversion not in commandsKind)
|
|
8591
8736
|
// Claude local: copyWithPathReplacement + stale-skills cleanup
|
|
8592
8737
|
|
|
8593
8738
|
// Layout-driven path for all skills-based runtimes (full and minimal modes).
|
|
@@ -8599,13 +8744,15 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8599
8744
|
// (it declares any skills/commands/agents/kimi-agents kind for this scope).
|
|
8600
8745
|
// This replaces the prior hardcoded `isCodex || isCopilot || ...` roster so a
|
|
8601
8746
|
// newly-added runtime with an artifact layout installs without a per-runtime
|
|
8602
|
-
// branch — the add-a-host tax ADR-1239 Phase B retires.
|
|
8603
|
-
//
|
|
8604
|
-
//
|
|
8747
|
+
// branch — the add-a-host tax ADR-1239 Phase B retires. OpenCode/Kilo now
|
|
8748
|
+
// route through this SAME path too: their hostBehaviors.combinedFamilyInstall
|
|
8749
|
+
// flag makes installRuntimeArtifacts (in src/install-engine.cts) delegate to
|
|
8750
|
+
// installOpencodeFamilyArtifacts for the combined commands+skills+native-plugin
|
|
8751
|
+
// install (ADR-1239 / #2087), replacing the bespoke inline block this comment
|
|
8752
|
+
// used to describe. Claude-local remains the one special-cased path
|
|
8605
8753
|
// (copyWithPathReplacement + stale-skills cleanup).
|
|
8606
8754
|
const _isSkillsRuntime = (() => {
|
|
8607
|
-
if (
|
|
8608
|
-
if (runtime === 'claude' && !isGlobal) return false; // claude-local legacy path
|
|
8755
|
+
if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && !isGlobal) return false; // legacy flat local path (descriptor-driven; #2086)
|
|
8609
8756
|
const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime];
|
|
8610
8757
|
const layout = cap && cap.runtime && cap.runtime.artifactLayout;
|
|
8611
8758
|
if (!layout) return false;
|
|
@@ -8616,7 +8763,28 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8616
8763
|
if (_isSkillsRuntime) {
|
|
8617
8764
|
// Layout-driven install for skills-based runtimes (full and minimal modes)
|
|
8618
8765
|
const scope = isGlobal ? 'global' : 'local';
|
|
8619
|
-
|
|
8766
|
+
// ADR-1239 upgrade 3 / #2088: a kind may declare an alternate install `home`
|
|
8767
|
+
// (e.g. Codex skills -> $HOME/.agents/skills) instead of the runtime's normal
|
|
8768
|
+
// configDir. Resolve the ACTUAL on-disk skills root here, descriptor-driven
|
|
8769
|
+
// (no isCodex check), so downstream sidecar-cleanup and post-install
|
|
8770
|
+
// verification look in the right place regardless of which runtime declares
|
|
8771
|
+
// an alternate home for its skills kind.
|
|
8772
|
+
const _skillsRootDir = _resolveSkillsRootDir(runtime, targetDir, scope);
|
|
8773
|
+
// ADR-1239 / #2086: drive install through the public Host-Integration Interface
|
|
8774
|
+
// (imperative adapter). The adapter delegates to the SAME installRuntimeArtifacts
|
|
8775
|
+
// engine call -> byte-identical output (gated by golden-install-parity). Fail-open
|
|
8776
|
+
// to the engine directly if the composed-registry adapter can't load.
|
|
8777
|
+
const _adapter = _runtimeAdapter(runtime);
|
|
8778
|
+
if (_adapter) {
|
|
8779
|
+
_adapter.install({
|
|
8780
|
+
configDir: targetDir,
|
|
8781
|
+
scope,
|
|
8782
|
+
resolvedProfile: _resolvedProfile,
|
|
8783
|
+
resolveAttribution: getCommitAttribution,
|
|
8784
|
+
});
|
|
8785
|
+
} else {
|
|
8786
|
+
installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile, getCommitAttribution);
|
|
8787
|
+
}
|
|
8620
8788
|
|
|
8621
8789
|
// #1326 — Codex only: remove stale agents/openai.yaml sidecars from managed
|
|
8622
8790
|
// gsd-* skill dirs. Prior installs wrote these files so Codex would show a
|
|
@@ -8624,8 +8792,23 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8624
8792
|
// index BOTH SKILL.md and the sidecar, causing each GSD skill to appear twice
|
|
8625
8793
|
// in autocomplete. Cleaning them up fixes the duplication; SKILL.md alone is
|
|
8626
8794
|
// sufficient for Codex discovery. User-owned dirs are never touched.
|
|
8627
|
-
if (
|
|
8628
|
-
cleanupCodexSkillMetadataSidecars(
|
|
8795
|
+
if (_hostBehaviors(runtime).cleanupSkillSidecars) {
|
|
8796
|
+
cleanupCodexSkillMetadataSidecars(_skillsRootDir);
|
|
8797
|
+
}
|
|
8798
|
+
|
|
8799
|
+
// ADR-1239 split-home migration: when a runtime's skills kind moved to an
|
|
8800
|
+
// alternate `home` (e.g. Codex → ~/.agents/skills), pre-move installs left
|
|
8801
|
+
// managed gsd-* skill dirs at the old configDir-rooted location
|
|
8802
|
+
// (~/.codex/skills). Reinstalling here writes the new location but would
|
|
8803
|
+
// otherwise orphan the old one — clean up the stale gsd-* dirs.
|
|
8804
|
+
{
|
|
8805
|
+
const _movedOldSkillsDir = _resolveMovedSkillsOldDir(runtime, targetDir, scope);
|
|
8806
|
+
if (_movedOldSkillsDir) {
|
|
8807
|
+
const migrated = cleanupMovedSkillsOldLocation(_movedOldSkillsDir, 'gsd-');
|
|
8808
|
+
if (migrated > 0) {
|
|
8809
|
+
console.log(` ${green}✓${reset} Migrated ${migrated} skill dir(s) off the legacy ${_movedOldSkillsDir} location`);
|
|
8810
|
+
}
|
|
8811
|
+
}
|
|
8629
8812
|
}
|
|
8630
8813
|
|
|
8631
8814
|
// #1629 Finding B: Windsurf local only — remove legacy .devin/skills/gsd-*
|
|
@@ -8639,13 +8822,13 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8639
8822
|
}
|
|
8640
8823
|
}
|
|
8641
8824
|
|
|
8642
|
-
//
|
|
8643
|
-
if (
|
|
8825
|
+
// Descriptor-driven (#2090): write DESCRIPTION.md for the gsd/ category after layout install
|
|
8826
|
+
if (_hostBehaviors(runtime).writeCategoryDescription) {
|
|
8644
8827
|
writeHermesCategoryDescription(path.join(targetDir, 'skills', 'gsd'));
|
|
8645
8828
|
}
|
|
8646
8829
|
|
|
8647
8830
|
// Verify installed artifacts and report
|
|
8648
|
-
if (
|
|
8831
|
+
if (_hostBehaviors(runtime).reportSkillsCount) {
|
|
8649
8832
|
const hermesSkillsDir = path.join(targetDir, 'skills', 'gsd');
|
|
8650
8833
|
if (fs.existsSync(hermesSkillsDir)) {
|
|
8651
8834
|
// Hermes layout uses prefix: 'gsd-' (#947) — skill dirs have gsd-<stem> names
|
|
@@ -8697,7 +8880,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8697
8880
|
}
|
|
8698
8881
|
}
|
|
8699
8882
|
} else {
|
|
8700
|
-
const skillsDir =
|
|
8883
|
+
const skillsDir = _skillsRootDir;
|
|
8701
8884
|
if (fs.existsSync(skillsDir)) {
|
|
8702
8885
|
const count = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
8703
8886
|
.filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length;
|
|
@@ -8725,8 +8908,9 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8725
8908
|
}
|
|
8726
8909
|
}
|
|
8727
8910
|
|
|
8728
|
-
//
|
|
8729
|
-
|
|
8911
|
+
// Descriptor-driven commands/ output report (#785 — Cursor 1.6 slash commands).
|
|
8912
|
+
// Gated by hostBehaviors.reportCommandsDir, not a hardcoded `isCursor` branch (#2089).
|
|
8913
|
+
if (_hostBehaviors(runtime).reportCommandsDir) {
|
|
8730
8914
|
const commandsDir = path.join(targetDir, 'commands');
|
|
8731
8915
|
if (fs.existsSync(commandsDir)) {
|
|
8732
8916
|
const cmdCount = fs.readdirSync(commandsDir)
|
|
@@ -8757,68 +8941,12 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8757
8941
|
}
|
|
8758
8942
|
}
|
|
8759
8943
|
}
|
|
8760
|
-
} else if (
|
|
8761
|
-
// OpenCode/Kilo: flat structure in command/ directory
|
|
8762
|
-
const commandDir = path.join(targetDir, 'command');
|
|
8763
|
-
fs.mkdirSync(commandDir, { recursive: true });
|
|
8764
|
-
|
|
8765
|
-
// Copy commands/gsd/*.md as command/gsd-*.md (flatten structure)
|
|
8766
|
-
const gsdSrc = _stageSkills(_commandsDir);
|
|
8767
|
-
copyFlattenedCommands(gsdSrc, commandDir, 'gsd', pathPrefix, runtime);
|
|
8768
|
-
if (verifyInstalled(commandDir, 'command/gsd-*')) {
|
|
8769
|
-
const count = fs.readdirSync(commandDir).filter(f => f.startsWith('gsd-')).length;
|
|
8770
|
-
console.log(` ${green}✓${reset} Installed ${count} commands to command/`);
|
|
8771
|
-
} else {
|
|
8772
|
-
failures.push('command/gsd-*');
|
|
8773
|
-
}
|
|
8774
|
-
|
|
8775
|
-
// Also emit OpenCode-family skills (skills/<name>/SKILL.md). OpenCode and
|
|
8776
|
-
// Kilo support native, on-demand skills in addition to flat commands — see
|
|
8777
|
-
// resolveRuntimeArtifactLayout's opencode/kilo entries. Derive skills from
|
|
8778
|
-
// the SAME staged command set (gsdSrc) so both surfaces match exactly. (#784)
|
|
8779
|
-
const _skillCount = installOpencodeFamilySkills(runtime, targetDir, gsdSrc, pathPrefix, getCommitAttribution);
|
|
8780
|
-
if (_skillCount > 0) {
|
|
8781
|
-
console.log(` ${green}✓${reset} Installed ${_skillCount} skills to skills/`);
|
|
8782
|
-
} else {
|
|
8783
|
-
failures.push('skills/gsd-*');
|
|
8784
|
-
}
|
|
8785
|
-
|
|
8786
|
-
// OpenCode-only: install the native plugin adapter (#1914). OpenCode
|
|
8787
|
-
// declares hooksSurface: 'none', so GSD's lifecycle hooks are never
|
|
8788
|
-
// registered as settings.json hooks the way Claude Code does — the hook
|
|
8789
|
-
// *scripts* ship to <configDir>/hooks/ but nothing invokes them. This
|
|
8790
|
-
// plugin bridges OpenCode's event bus onto those existing hook scripts
|
|
8791
|
-
// (prompt guard, read guard, injection scanner, context monitor, ...),
|
|
8792
|
-
// spawning them as subprocesses. OpenCode auto-discovers plugin files under
|
|
8793
|
-
// <configDir>/plugins/ at startup — no opencode.json registration needed
|
|
8794
|
-
// (its `plugin` array is for npm packages, not local file paths).
|
|
8795
|
-
//
|
|
8796
|
-
// The file MUST land as `.js`: OpenCode's loader globs
|
|
8797
|
-
// `{plugin,plugins}/*.{ts,js}` (verified against its source) — a `.cjs`
|
|
8798
|
-
// extension would never be discovered. The config dir carries a
|
|
8799
|
-
// `{"type":"commonjs"}` package.json (written above), so the `.js` file is
|
|
8800
|
-
// interpreted as CommonJS, matching the adapter's module.exports/require.
|
|
8801
|
-
// Kilo has no plugin surface, so this is gated to OpenCode only.
|
|
8802
|
-
if (isOpencode) {
|
|
8803
|
-
const pluginSrc = path.join(src, '.opencode', 'plugins', 'gsd-core.js');
|
|
8804
|
-
const pluginDestDir = path.join(targetDir, 'plugins');
|
|
8805
|
-
const pluginDest = path.join(pluginDestDir, 'gsd-core.js');
|
|
8806
|
-
if (fs.existsSync(pluginSrc)) {
|
|
8807
|
-
fs.mkdirSync(pluginDestDir, { recursive: true });
|
|
8808
|
-
fs.copyFileSync(pluginSrc, pluginDest);
|
|
8809
|
-
if (fs.existsSync(pluginDest)) {
|
|
8810
|
-
console.log(` ${green}✓${reset} Installed OpenCode plugin (bridges GSD hooks)`);
|
|
8811
|
-
} else {
|
|
8812
|
-
failures.push('plugins/gsd-core.js');
|
|
8813
|
-
}
|
|
8814
|
-
} else {
|
|
8815
|
-
failures.push('plugins/gsd-core.js');
|
|
8816
|
-
}
|
|
8817
|
-
}
|
|
8818
|
-
} else if (isCline) {
|
|
8944
|
+
} else if (_hostBehaviors(runtime).localCommandsViaRules) {
|
|
8819
8945
|
// Cline local install: rules-based only — commands are embedded in .clinerules (generated below).
|
|
8820
8946
|
// No skills/commands directory needed for local installs.
|
|
8821
8947
|
// Global installs are handled above by _isSkillsRuntime (#782).
|
|
8948
|
+
// Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
|
|
8949
|
+
// hostBehaviors.localCommandsViaRules.
|
|
8822
8950
|
console.log(` ${green}✓${reset} Cline: commands will be available via .clinerules`);
|
|
8823
8951
|
} else {
|
|
8824
8952
|
// Claude Code local: flat gsd-<cmd>.md layout — Claude Code registers
|
|
@@ -8916,11 +9044,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8916
9044
|
// other runtime/scope deploys commands/gsd, so its walk-up already resolves
|
|
8917
9045
|
// and needs no marker. Guarded on source presence so a half-published
|
|
8918
9046
|
// package never writes a dangling marker.
|
|
8919
|
-
if (runtime
|
|
9047
|
+
if (_hostBehaviors(runtime).sourceMarkerFile && isGlobal) {
|
|
8920
9048
|
const gsdSourceCommands = path.join(src, 'commands', 'gsd');
|
|
8921
9049
|
if (fs.existsSync(gsdSourceCommands)) {
|
|
8922
9050
|
try {
|
|
8923
|
-
|
|
9051
|
+
// ADR-1239 Phase B write-confinement: the descriptor-sourced marker filename
|
|
9052
|
+
// must resolve under targetDir (parity with the other descriptor-driven writes).
|
|
9053
|
+
const _markerPath = assertDestWithinConfigHome(targetDir, _hostBehaviors(runtime).sourceMarkerFile);
|
|
9054
|
+
fs.writeFileSync(_markerPath, gsdSourceCommands + '\n', 'utf8');
|
|
8924
9055
|
} catch (err) {
|
|
8925
9056
|
// Non-fatal: install proceeds. But on the Claude-global layout walk-up
|
|
8926
9057
|
// also fails (no commands/gsd source tree), so a silent write failure
|
|
@@ -9007,7 +9138,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9007
9138
|
for (const file of fs.readdirSync(agentsDest)) {
|
|
9008
9139
|
if (
|
|
9009
9140
|
file.startsWith('gsd-') &&
|
|
9010
|
-
(file.endsWith('.md') || (
|
|
9141
|
+
(file.endsWith('.md') || (_hostBehaviors(runtime).agentTomlFiles && file.endsWith('.toml')))
|
|
9011
9142
|
) {
|
|
9012
9143
|
fs.unlinkSync(path.join(agentsDest, file));
|
|
9013
9144
|
}
|
|
@@ -9025,7 +9156,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9025
9156
|
// Without stripping them here, a full → minimal reinstall would leave the
|
|
9026
9157
|
// runtime advertising the old full agent surface even though the agent
|
|
9027
9158
|
// files are gone. Reuse the same helper that powers `--uninstall`.
|
|
9028
|
-
if (
|
|
9159
|
+
if (_hostBehaviors(runtime).tomlConfigInstall) {
|
|
9029
9160
|
const codexConfigPath = path.join(targetDir, 'config.toml');
|
|
9030
9161
|
if (fs.existsSync(codexConfigPath)) {
|
|
9031
9162
|
const existing = fs.readFileSync(codexConfigPath, 'utf8');
|
|
@@ -9060,7 +9191,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9060
9191
|
}
|
|
9061
9192
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
9062
9193
|
// Convert frontmatter for runtime compatibility (agents need different handling)
|
|
9063
|
-
if (
|
|
9194
|
+
if (_hostBehaviors(runtime).frontmatterDialect === 'opencode') {
|
|
9064
9195
|
// Resolve per-agent model for OpenCode agents.
|
|
9065
9196
|
// Precedence: model_overrides[agent] > model_profile_overrides.opencode.<tier> > omit.
|
|
9066
9197
|
// model_overrides (#2256): explicit per-agent override, highest precedence.
|
|
@@ -9079,16 +9210,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9079
9210
|
}
|
|
9080
9211
|
}
|
|
9081
9212
|
content = convertClaudeToOpencodeFrontmatter(content, { isAgent: true, modelOverride: _ocModelOverride });
|
|
9082
|
-
} else if (
|
|
9213
|
+
} else if (_hostBehaviors(runtime).frontmatterDialect === 'kilo') {
|
|
9083
9214
|
content = convertClaudeToKiloFrontmatter(content, { isAgent: true });
|
|
9084
|
-
} else if (
|
|
9215
|
+
} else if (_hostBehaviors(runtime).frontmatterDialect === 'codex') {
|
|
9085
9216
|
content = convertClaudeAgentToCodexAgent(content);
|
|
9086
9217
|
} else if (isCopilot) {
|
|
9087
9218
|
content = convertClaudeAgentToCopilotAgent(content, isGlobal);
|
|
9088
9219
|
} else if (isAntigravity) {
|
|
9089
9220
|
content = convertClaudeAgentToAntigravityAgent(content, isGlobal);
|
|
9090
|
-
} else if (isCursor) {
|
|
9091
|
-
content = convertClaudeAgentToCursorAgent(content);
|
|
9092
9221
|
} else if (isWindsurf) {
|
|
9093
9222
|
content = convertClaudeAgentToWindsurfAgent(content);
|
|
9094
9223
|
} else if (isAugment) {
|
|
@@ -9097,13 +9226,15 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9097
9226
|
content = convertClaudeAgentToTraeAgent(content);
|
|
9098
9227
|
} else if (isCodebuddy) {
|
|
9099
9228
|
content = convertClaudeAgentToCodebuddyAgent(content);
|
|
9100
|
-
} else if (
|
|
9229
|
+
} else if (_hostBehaviors(runtime).frontmatterDialect === 'cline') {
|
|
9230
|
+
// Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
|
|
9231
|
+
// hostBehaviors.frontmatterDialect === 'cline'.
|
|
9101
9232
|
content = convertClaudeAgentToClineAgent(content);
|
|
9102
9233
|
} else if (isQwen) {
|
|
9103
9234
|
content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
|
|
9104
9235
|
content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
|
|
9105
9236
|
content = content.replace(/\.claude\//g, '.qwen/');
|
|
9106
|
-
} else if (
|
|
9237
|
+
} else if (_hostBehaviors(runtime).brandingRewrites) {
|
|
9107
9238
|
content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
|
|
9108
9239
|
content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
|
|
9109
9240
|
content = content.replace(/\.claude\//g, '.hermes/');
|
|
@@ -9115,11 +9246,11 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9115
9246
|
// Claude Code reads per-subagent `effort:` frontmatter (anthropics/claude-code #31536).
|
|
9116
9247
|
// Injection is per-runtime at install time because the canonical source
|
|
9117
9248
|
// agents/*.md must stay runtime-safe (no effort: key in source).
|
|
9118
|
-
if (runtime
|
|
9249
|
+
if ((_hostBehaviors(runtime).agentFrontmatterExtensions || []).includes('effort')) {
|
|
9119
9250
|
const _effortCfg = readGsdEffectiveEffortConfig(targetDir);
|
|
9120
9251
|
const _agentName = entry.name.replace(/\.md$/, '');
|
|
9121
9252
|
const _universalEffort = resolveInstallTimeEffort(_effortCfg, _agentName);
|
|
9122
|
-
const _renderedEffort = _getGsdEffortCatalog().renderEffortForRuntime(
|
|
9253
|
+
const _renderedEffort = _getGsdEffortCatalog().renderEffortForRuntime(runtime, _universalEffort).value;
|
|
9123
9254
|
content = injectEffortFrontmatter(content, _renderedEffort);
|
|
9124
9255
|
const _disallowedTools = READONLY_AGENT_DISALLOWED_TOOLS[_agentName];
|
|
9125
9256
|
if (_disallowedTools) content = injectDisallowedToolsFrontmatter(content, _disallowedTools);
|
|
@@ -9163,7 +9294,17 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9163
9294
|
failures.push('VERSION');
|
|
9164
9295
|
}
|
|
9165
9296
|
|
|
9166
|
-
|
|
9297
|
+
// #1821: Kilo and ZCode declare hooksSurface:'none' AND have no plugin surface,
|
|
9298
|
+
// so the staged hook scripts are dead weight for them — exclude both here.
|
|
9299
|
+
// OpenCode also declares hooksSurface:'none' but is deliberately NOT excluded:
|
|
9300
|
+
// its native plugin adapter (#1914, installed above under plugins/gsd-core.js)
|
|
9301
|
+
// spawns the staged hooks/*.js scripts via OpenCode's event bus and needs both
|
|
9302
|
+
// them and the CommonJS package.json marker written below.
|
|
9303
|
+
// #2089: Cursor's exclusion is now descriptor-driven via
|
|
9304
|
+
// hostBehaviors.skipSharedHooksInstall (was hardcoded !isCursor).
|
|
9305
|
+
// #2090: Cline's exclusion is likewise descriptor-driven (cline declares
|
|
9306
|
+
// skipSharedHooksInstall:true) — the redundant `&& !isCline` was removed.
|
|
9307
|
+
if (!isCodex && !isCopilot && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf && !isTrae && !isKimi && !isKilo && !isZcode) {
|
|
9167
9308
|
// Write package.json to force CommonJS mode for GSD scripts
|
|
9168
9309
|
// Prevents "require is not defined" errors when project has "type": "module"
|
|
9169
9310
|
// Node.js walks up looking for package.json - this stops inheritance from project
|
|
@@ -9192,7 +9333,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9192
9333
|
content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
|
|
9193
9334
|
content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
|
|
9194
9335
|
}
|
|
9195
|
-
if (
|
|
9336
|
+
if (_hostBehaviors(runtime).brandingRewrites) {
|
|
9196
9337
|
content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
|
|
9197
9338
|
content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
|
|
9198
9339
|
}
|
|
@@ -9254,12 +9395,16 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9254
9395
|
|
|
9255
9396
|
// Gate hooks/lib/ install on the same runtimes that receive hooks (see line ~8702).
|
|
9256
9397
|
// Codex/Copilot/Cursor/Windsurf/Trae/Cline do not use the shared hooks/lib/ helpers
|
|
9257
|
-
// (Cursor uses standalone .js hook scripts registered via hooks.json
|
|
9258
|
-
//
|
|
9259
|
-
//
|
|
9260
|
-
//
|
|
9398
|
+
// (Cursor uses standalone .js hook scripts registered via hooks.json — gated
|
|
9399
|
+
// descriptor-driven via hostBehaviors.skipSharedHooksInstall, #2089; Cline likewise
|
|
9400
|
+
// #2090; Codex uses hooks.json directly; the others skip hooks entirely); Kilo and
|
|
9401
|
+
// ZCode also skip hooks entirely (hooksSurface:'none' with no plugin surface — #1821).
|
|
9402
|
+
// OpenCode is NOT excluded: its #1914 plugin adapter spawns the staged hooks and
|
|
9403
|
+
// requires hooks/lib/ helpers. None of the excluded runtimes must receive the
|
|
9404
|
+
// hooks/lib/ helpers — otherwise the Codex comment downstream ("we deliberately do
|
|
9405
|
+
// *not* copy hooks/lib/ for Codex") is contradicted in practice.
|
|
9261
9406
|
const hooksLibSrc = path.join(src, 'hooks', 'lib');
|
|
9262
|
-
if (!isCodex && !isCopilot &&
|
|
9407
|
+
if (!isCodex && !isCopilot && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf && !isTrae && !isKimi && !isKilo && !isZcode && fs.existsSync(hooksLibSrc)) {
|
|
9263
9408
|
const hooksLibDest = path.join(targetDir, 'hooks', 'lib');
|
|
9264
9409
|
fs.mkdirSync(hooksLibDest, { recursive: true });
|
|
9265
9410
|
copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES);
|
|
@@ -9386,14 +9531,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9386
9531
|
}
|
|
9387
9532
|
|
|
9388
9533
|
// Write file manifest for future modification detection
|
|
9389
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode });
|
|
9534
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' });
|
|
9390
9535
|
console.log(` ${green}✓${reset} Wrote file manifest (${MANIFEST_NAME})`);
|
|
9391
9536
|
|
|
9392
9537
|
// Report any backed-up local patches
|
|
9393
9538
|
reportLocalPatches(targetDir, runtime);
|
|
9394
9539
|
|
|
9395
9540
|
// Verify no leaked .claude paths in non-Claude runtimes (manifest-scoped)
|
|
9396
|
-
if (runtime
|
|
9541
|
+
if (!_hostBehaviors(runtime).ownsClaudePaths) {
|
|
9397
9542
|
const leakedPaths = [];
|
|
9398
9543
|
// Only scan files that were written by this install (manifest-tracked).
|
|
9399
9544
|
// Scanning the entire targetDir can match user-authored content that
|
|
@@ -9529,7 +9674,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9529
9674
|
// (copyCommandsAsCodexSkills removes pre-existing gsd-* dirs before re-writing)
|
|
9530
9675
|
// are restored even when they are absent from disk at rollback time (#3245 CR).
|
|
9531
9676
|
// • Dirs that did not pre-exist: remove entirely.
|
|
9532
|
-
const _rollbackSkillsDir =
|
|
9677
|
+
const _rollbackSkillsDir = _resolveSkillsRootDir(runtime, targetDir, isGlobal ? 'global' : 'local');
|
|
9533
9678
|
// Pass 1 — restore snapshot entries (may be absent from disk if deleted mid-install).
|
|
9534
9679
|
for (const skillName of codexPreInstallSkillNames) {
|
|
9535
9680
|
const skillDirPath = path.join(_rollbackSkillsDir, skillName);
|
|
@@ -9640,7 +9785,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9640
9785
|
// Re-write the manifest now that .toml agent files exist on disk.
|
|
9641
9786
|
// The initial writeManifest call (before Codex config generation) could
|
|
9642
9787
|
// not include agents/gsd-*.toml because those files did not yet exist.
|
|
9643
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode });
|
|
9788
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' });
|
|
9644
9789
|
} else {
|
|
9645
9790
|
console.log(` ${dim}↳${reset} Skipping Codex agent config generation (minimal install)`);
|
|
9646
9791
|
}
|
|
@@ -9780,24 +9925,23 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9780
9925
|
}
|
|
9781
9926
|
}
|
|
9782
9927
|
|
|
9783
|
-
// ── Codex extended hook events (#772)
|
|
9784
|
-
// Codex CLI stabilised a full hook-event set in rust-v0.137.0.
|
|
9785
|
-
//
|
|
9786
|
-
//
|
|
9787
|
-
//
|
|
9788
|
-
//
|
|
9789
|
-
//
|
|
9790
|
-
//
|
|
9791
|
-
//
|
|
9792
|
-
//
|
|
9793
|
-
//
|
|
9794
|
-
// to a follow-on issue.
|
|
9928
|
+
// ── Codex extended hook events (#772, #2088) ─────────────────────────
|
|
9929
|
+
// Codex CLI stabilised a full hook-event set in rust-v0.137.0. GSD
|
|
9930
|
+
// registers CODEX_EXTENDED_HOOK_EVENTS (#2088 adds the 6 documented
|
|
9931
|
+
// events beyond the original #772 three) — all routed through
|
|
9932
|
+
// gsd-context-monitor.js so context-headroom warnings surface at each
|
|
9933
|
+
// lifecycle point: SubagentStart/SubagentStop (subagent open/close),
|
|
9934
|
+
// Stop (final-response), PreToolUse/PostToolUse (tool boundaries),
|
|
9935
|
+
// PermissionRequest (approval prompts), Pre/PostCompact (context
|
|
9936
|
+
// compaction), and UserPromptSubmit (per-turn context injection). The
|
|
9937
|
+
// context-monitor script decides per-payload what to do; unregistered
|
|
9938
|
+
// events simply never fire.
|
|
9795
9939
|
//
|
|
9796
9940
|
// Guard: only register when the context-monitor file exists and the node
|
|
9797
9941
|
// runner is available — same guards as the SessionStart path above.
|
|
9798
9942
|
const contextMonitorFile = path.join(targetDir, 'hooks', 'gsd-context-monitor.js');
|
|
9799
9943
|
if (codexNodeRunner && fs.existsSync(contextMonitorFile)) {
|
|
9800
|
-
for (const codexEvent of
|
|
9944
|
+
for (const codexEvent of CODEX_EXTENDED_HOOK_EVENTS) {
|
|
9801
9945
|
const eventWrite = ensureCodexHooksJsonEvent(targetDir, codexEvent, {
|
|
9802
9946
|
absoluteRunner: codexNodeRunner,
|
|
9803
9947
|
platform: process.platform,
|
|
@@ -9809,7 +9953,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9809
9953
|
}
|
|
9810
9954
|
}
|
|
9811
9955
|
} else if (!codexNodeRunner) {
|
|
9812
|
-
console.warn(` ${yellow}⚠${reset} Skipped Codex
|
|
9956
|
+
console.warn(` ${yellow}⚠${reset} Skipped Codex extended hook-event registration — Node runner unavailable.`);
|
|
9813
9957
|
}
|
|
9814
9958
|
// ── end Codex extended hook events ────────────────────────────────────
|
|
9815
9959
|
}
|
|
@@ -9874,16 +10018,20 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9874
10018
|
}
|
|
9875
10019
|
|
|
9876
10020
|
if (plan.installSurface === 'cursor-hooks-json') {
|
|
9877
|
-
// #
|
|
9878
|
-
//
|
|
9879
|
-
|
|
10021
|
+
// ADR-1239 / #2089: Cursor hooks.json driven by the descriptor-managed hook-bus
|
|
10022
|
+
// adapter. Registers all 6 managed events (sessionStart, postToolUse, preToolUse,
|
|
10023
|
+
// stop, subagentStart, subagentStop) via runtime-hooks-surface.cts, which reads
|
|
10024
|
+
// the event list from the descriptor-driven adapter module.
|
|
10025
|
+
const cursorHookResult = writeCursorHooksJson(targetDir, src, {
|
|
10026
|
+
managedHookEvents: _hostBehaviors(runtime).managedHookEvents,
|
|
10027
|
+
});
|
|
9880
10028
|
if (cursorHookResult.changed) {
|
|
9881
|
-
console.log(` ${green}✓${reset} Configured Cursor lifecycle hooks (sessionStart, postToolUse)`);
|
|
10029
|
+
console.log(` ${green}✓${reset} Configured Cursor lifecycle hooks (sessionStart, postToolUse, preToolUse, stop, subagentStart, subagentStop)`);
|
|
9882
10030
|
} else {
|
|
9883
10031
|
console.log(` ${green}✓${reset} Cursor lifecycle hooks already up to date`);
|
|
9884
10032
|
}
|
|
9885
10033
|
// Re-run the manifest pass so the hook scripts + hooks.json are hash-tracked.
|
|
9886
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode });
|
|
10034
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' });
|
|
9887
10035
|
persistActiveProfileMarker();
|
|
9888
10036
|
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
9889
10037
|
}
|
|
@@ -9901,7 +10049,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9901
10049
|
writeClineArtifacts(targetDir, isGlobal);
|
|
9902
10050
|
// Re-run the manifest pass: these artifacts are written *after* the earlier
|
|
9903
10051
|
// writeManifest() call, so a second pass is needed to hash-track them.
|
|
9904
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode });
|
|
10052
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' });
|
|
9905
10053
|
persistActiveProfileMarker();
|
|
9906
10054
|
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
9907
10055
|
}
|
|
@@ -9916,9 +10064,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9916
10064
|
// #338: local Claude installs write to settings.local.json (Claude Code's per-user/gitignored slot)
|
|
9917
10065
|
// so engineer-specific absolute paths (Node binary, home dir) never land in the repo-shared
|
|
9918
10066
|
// settings.json. Global installs and all other runtimes continue to use settings.json.
|
|
9919
|
-
const
|
|
9920
|
-
const
|
|
9921
|
-
const
|
|
10067
|
+
const _scopedSettings = _hostBehaviors(runtime).settingsFileByScope || null;
|
|
10068
|
+
const isLocalClaude = (!isGlobal && !!(_scopedSettings && _scopedSettings.local));
|
|
10069
|
+
const settingsFileName = isLocalClaude
|
|
10070
|
+
? _scopedSettings.local
|
|
10071
|
+
: ((_scopedSettings && _scopedSettings.global) || 'settings.json');
|
|
10072
|
+
// ADR-1239 Phase B write-confinement: the descriptor-sourced settings filename
|
|
10073
|
+
// must resolve under targetDir (this path also drives a recursive mkdirSync).
|
|
10074
|
+
const settingsPath = assertDestWithinConfigHome(targetDir, settingsFileName);
|
|
9922
10075
|
|
|
9923
10076
|
// #338 migration: if a prior local Claude install wrote GSD-shaped entries to settings.json,
|
|
9924
10077
|
// relocate them to settings.local.json and clear them from the shared file in the same run.
|
|
@@ -10101,7 +10254,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
10101
10254
|
// installAllRuntimes can register it at finalize time when the user opts
|
|
10102
10255
|
// in (#2795). Computed here (not in finishInstall) so the same buildHookCommand
|
|
10103
10256
|
// / localCmd resolution logic is shared with the other JS hooks.
|
|
10104
|
-
const updateBannerCommand =
|
|
10257
|
+
const updateBannerCommand = _hostBehaviors(runtime).skipUpdateBannerCommand
|
|
10105
10258
|
? null
|
|
10106
10259
|
: (isGlobal
|
|
10107
10260
|
? buildHookCommand(targetDir, 'gsd-update-banner.js', hookOpts)
|
|
@@ -10184,11 +10337,11 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
10184
10337
|
/**
|
|
10185
10338
|
* Apply statusline config, then print completion message
|
|
10186
10339
|
*/
|
|
10187
|
-
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime =
|
|
10340
|
+
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = DEFAULT_RUNTIME, isGlobal = true, configDir = null, bannerOpts = {}) {
|
|
10188
10341
|
const { isOpencode, isKilo, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
10189
10342
|
const plan = resolveInstallPlan(runtime);
|
|
10190
10343
|
|
|
10191
|
-
if (shouldInstallStatusline && plan.writesSharedSettings && !
|
|
10344
|
+
if (shouldInstallStatusline && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {
|
|
10192
10345
|
if (!isGlobal && !forceStatusline) {
|
|
10193
10346
|
// Local installs skip statusLine by default: repo settings.json takes precedence over
|
|
10194
10347
|
// profile-level settings.json in Claude Code, so writing here would silently clobber
|
|
@@ -10214,7 +10367,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
10214
10367
|
// settings.json hooks block — opencode/kilo/codex/cursor/windsurf/trae/
|
|
10215
10368
|
// cline either lack the surface or use a different config schema.
|
|
10216
10369
|
const { shouldInstallBanner, bannerCommand } = bannerOpts;
|
|
10217
|
-
if (shouldInstallBanner && settings && plan.writesSharedSettings && !
|
|
10370
|
+
if (shouldInstallBanner && settings && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {
|
|
10218
10371
|
if (!bannerCommand) {
|
|
10219
10372
|
console.warn(` ${yellow}⚠${reset} Skipped update banner registration — Node executable path unavailable. See #2979 / #3002.`);
|
|
10220
10373
|
} else {
|
|
@@ -10243,7 +10396,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
10243
10396
|
// Merges GSD-owned entries non-destructively (preserves existing user permissions).
|
|
10244
10397
|
// Scoped to Claude only: antigravity/qwen/hermes/codebuddy also write
|
|
10245
10398
|
// settings.json but use different runtimes and do not use these permission strings.
|
|
10246
|
-
if (runtime === 'claude') {
|
|
10399
|
+
if (_hostBehaviors(runtime).permissionsSchema === 'claude') {
|
|
10247
10400
|
mergeClaudePermissions(settings);
|
|
10248
10401
|
}
|
|
10249
10402
|
|
|
@@ -10276,7 +10429,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
10276
10429
|
// chat model instead of pinning the resolved model. See #1156 (default-to-omit
|
|
10277
10430
|
// intent) and #1569 (preserve explicit true). Guard matches the #130-class pattern
|
|
10278
10431
|
// on configureOpencodePermissions above.
|
|
10279
|
-
if (runtime
|
|
10432
|
+
if (!_hostBehaviors(runtime).nativeModelAliases && !process.env.GSD_TEST_MODE) {
|
|
10280
10433
|
const gsdDir = path.join(os.homedir(), '.gsd');
|
|
10281
10434
|
const defaultsPath = path.join(gsdDir, 'defaults.json');
|
|
10282
10435
|
try {
|
|
@@ -10318,7 +10471,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
10318
10471
|
// Restart is required for CC to pick up newly-installed skills, and the
|
|
10319
10472
|
// slash-menu surface depends on CC version — so the instruction needs to
|
|
10320
10473
|
// cover both invocation paths to avoid #2957-style "no commands appear".
|
|
10321
|
-
if (runtime
|
|
10474
|
+
if (_hostBehaviors(runtime).skillsGlobalOnboarding && isGlobal) {
|
|
10322
10475
|
console.log(`
|
|
10323
10476
|
${green}Done!${reset} Restart ${program}, then in any directory either type ${cyan}${command}${reset} or ask Claude to run the ${cyan}gsd-new-project${reset} skill.
|
|
10324
10477
|
|
|
@@ -10477,7 +10630,7 @@ function parseRuntimeInput(answer) {
|
|
|
10477
10630
|
}
|
|
10478
10631
|
}
|
|
10479
10632
|
|
|
10480
|
-
return selected.length > 0 ? selected : [
|
|
10633
|
+
return selected.length > 0 ? selected : [DEFAULT_RUNTIME];
|
|
10481
10634
|
}
|
|
10482
10635
|
|
|
10483
10636
|
function promptRuntime(callback) {
|
|
@@ -11096,7 +11249,7 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
11096
11249
|
throw error;
|
|
11097
11250
|
}
|
|
11098
11251
|
|
|
11099
|
-
const statuslineRuntimes = [
|
|
11252
|
+
const statuslineRuntimes = [DEFAULT_RUNTIME];
|
|
11100
11253
|
const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime));
|
|
11101
11254
|
|
|
11102
11255
|
const finalize = (shouldInstallStatusline, shouldInstallBanner) => {
|
|
@@ -11193,6 +11346,13 @@ module.exports = {
|
|
|
11193
11346
|
generateCodexAgentToml,
|
|
11194
11347
|
cleanupCodexSkillMetadataSidecars,
|
|
11195
11348
|
cleanupWindsurfLegacyDevinSkills,
|
|
11349
|
+
cleanupMovedSkillsOldLocation,
|
|
11350
|
+
_resolveMovedSkillsOldDir,
|
|
11351
|
+
_resolveSkillsRootDir,
|
|
11352
|
+
codexBareAgentsHasOnlyKnownScalars,
|
|
11353
|
+
extractCodexUserAgentsScalars,
|
|
11354
|
+
spliceCodexAgentsScalars,
|
|
11355
|
+
CODEX_EXTENDED_HOOK_EVENTS,
|
|
11196
11356
|
generateCodexConfigBlock,
|
|
11197
11357
|
stripGsdFromCodexConfig,
|
|
11198
11358
|
migrateCodexHooksMapFormat,
|
|
@@ -11212,6 +11372,9 @@ module.exports = {
|
|
|
11212
11372
|
install,
|
|
11213
11373
|
installAllRuntimes,
|
|
11214
11374
|
uninstall,
|
|
11375
|
+
// #2086 — host-behavior resolution + the #338 privacy fail-safe floor (exported for tests)
|
|
11376
|
+
_resolveHostBehaviors,
|
|
11377
|
+
FALLBACK_HOST_BEHAVIORS,
|
|
11215
11378
|
convertSlashCommandsToCodexSkillMentions,
|
|
11216
11379
|
convertClaudeCommandToCodexSkill,
|
|
11217
11380
|
convertClaudeCommandToKimiSkill,
|
|
@@ -11276,6 +11439,11 @@ module.exports = {
|
|
|
11276
11439
|
mergeGsdAgentsMd,
|
|
11277
11440
|
GSD_CURSOR_SESSION_HOOK_SCRIPT,
|
|
11278
11441
|
GSD_CURSOR_POST_TOOL_HOOK_SCRIPT,
|
|
11442
|
+
GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT,
|
|
11443
|
+
GSD_CURSOR_STOP_HOOK_SCRIPT,
|
|
11444
|
+
GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT,
|
|
11445
|
+
GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT,
|
|
11446
|
+
GSD_CURSOR_HOOK_SCRIPTS,
|
|
11279
11447
|
GSD_CURSOR_HOOK_MARKER,
|
|
11280
11448
|
buildCursorHookEntry,
|
|
11281
11449
|
isManagedCursorHookEntry,
|
|
@@ -11380,7 +11548,7 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
|
|
|
11380
11548
|
console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
|
|
11381
11549
|
process.exit(1);
|
|
11382
11550
|
}
|
|
11383
|
-
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : [
|
|
11551
|
+
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : [DEFAULT_RUNTIME];
|
|
11384
11552
|
for (const runtime of runtimes) {
|
|
11385
11553
|
uninstall(hasGlobal, runtime);
|
|
11386
11554
|
}
|
|
@@ -11392,12 +11560,12 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
|
|
|
11392
11560
|
}
|
|
11393
11561
|
} else if (hasGlobal || hasLocal) {
|
|
11394
11562
|
// Default to Claude if no runtime specified but location is
|
|
11395
|
-
installAllRuntimes([
|
|
11563
|
+
installAllRuntimes([DEFAULT_RUNTIME], hasGlobal, false);
|
|
11396
11564
|
} else {
|
|
11397
11565
|
// Interactive
|
|
11398
11566
|
if (!process.stdin.isTTY) {
|
|
11399
11567
|
console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
|
|
11400
|
-
installAllRuntimes([
|
|
11568
|
+
installAllRuntimes([DEFAULT_RUNTIME], true, false);
|
|
11401
11569
|
} else {
|
|
11402
11570
|
promptRuntime((runtimes) => {
|
|
11403
11571
|
promptLocation(runtimes);
|