@opengsd/gsd-core 1.7.0-rc.3 → 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/README.ja-JP.md +4 -3
- package/README.ko-KR.md +4 -3
- package/README.md +4 -3
- package/README.pt-BR.md +4 -3
- package/README.zh-CN.md +4 -3
- 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 +652 -466
- package/commands/gsd/map-codebase.md +4 -4
- package/commands/gsd/ns-project.md +2 -1
- package/commands/gsd/onboard.md +46 -0
- package/gsd-core/bin/gsd-tools.cjs +87 -3
- 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 +741 -62
- package/gsd-core/bin/lib/capability-state.cjs +117 -18
- package/gsd-core/bin/lib/capability-validator.cjs +1 -1
- package/gsd-core/bin/lib/capability-writer.cjs +21 -2
- package/gsd-core/bin/lib/check-command-router.cjs +242 -3
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +136 -0
- package/gsd-core/bin/lib/claude-orchestration.cjs +404 -0
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- 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-command-router.cjs +4 -0
- package/gsd-core/bin/lib/init.cjs +99 -99
- package/gsd-core/bin/lib/install-effort-resolver.cjs +213 -0
- package/gsd-core/bin/lib/install-engine.cjs +159 -16
- package/gsd-core/bin/lib/install-profiles.cjs +2 -0
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +75 -18
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +50 -11
- package/gsd-core/bin/lib/milestone.cjs +3 -3
- package/gsd-core/bin/lib/model-resolver.cjs +69 -4
- package/gsd-core/bin/lib/normalize-test-command.cjs +187 -0
- package/gsd-core/bin/lib/onboard-projection.cjs +309 -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/runtime-name-policy.cjs +3 -1
- 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 +81 -30
- 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 +11 -6
- package/gsd-core/references/api-coverage.md +104 -0
- package/gsd-core/references/gsd-run-resolver.md +8 -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/templates/project.md +1 -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/do.md +4 -3
- 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/help/modes/brief.md +2 -1
- package/gsd-core/workflows/help/modes/default.md +2 -1
- package/gsd-core/workflows/help/modes/full.md +11 -1
- package/gsd-core/workflows/help/modes/topic.md +1 -1
- package/gsd-core/workflows/onboard.md +277 -0
- package/gsd-core/workflows/plan-phase.md +31 -2
- package/gsd-core/workflows/quick.md +22 -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/gen-golden-install-parity-zcode.cjs +77 -0
- package/scripts/lint-phase-id-drift.cjs +150 -0
- package/scripts/run-tests.cjs +26 -4
- package/scripts/sync-runtime-launcher.cjs +20 -2
- package/skills/gsd-map-codebase/SKILL.md +3 -3
- package/skills/gsd-ns-project/SKILL.md +1 -0
- package/skills/gsd-onboard/SKILL.md +46 -0
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,
|
|
@@ -434,7 +529,7 @@ if (hasMinimal && _profileArgRaw) {
|
|
|
434
529
|
|
|
435
530
|
function selectRuntimesFromArgs(runtimeArgs) {
|
|
436
531
|
if (runtimeArgs.includes('--all')) {
|
|
437
|
-
return ['claude', 'kimi', 'kilo', 'opencode', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline'];
|
|
532
|
+
return ['claude', 'kimi', 'kilo', 'opencode', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'zcode'];
|
|
438
533
|
}
|
|
439
534
|
if (runtimeArgs.includes('--both')) {
|
|
440
535
|
return ['claude', 'opencode'];
|
|
@@ -456,6 +551,7 @@ function selectRuntimesFromArgs(runtimeArgs) {
|
|
|
456
551
|
if (runtimeArgs.includes('--kimi')) selected.push('kimi');
|
|
457
552
|
if (runtimeArgs.includes('--codebuddy')) selected.push('codebuddy');
|
|
458
553
|
if (runtimeArgs.includes('--cline')) selected.push('cline');
|
|
554
|
+
if (runtimeArgs.includes('--zcode')) selected.push('zcode');
|
|
459
555
|
return selected;
|
|
460
556
|
}
|
|
461
557
|
|
|
@@ -579,7 +675,7 @@ const banner = '\n' +
|
|
|
579
675
|
' GSD Core ' + dim + 'v' + pkg.version + reset + '\n' +
|
|
580
676
|
' Git. Ship. Done.\n' +
|
|
581
677
|
' A meta-prompting, context engineering and spec-driven\n' +
|
|
582
|
-
' development workflows for Claude Code, OpenCode, Kimi CLI, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline and
|
|
678
|
+
' development workflows for Claude Code, OpenCode, Kimi CLI, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline, CodeBuddy and ZCode.\n';
|
|
583
679
|
|
|
584
680
|
// Pure seam: parse --config-dir / -c from an arbitrary args array.
|
|
585
681
|
// Returns the path string, '' for an empty equals-form value, or null when the
|
|
@@ -636,7 +732,7 @@ if (hasUninstall) {
|
|
|
636
732
|
|
|
637
733
|
// Show help if requested
|
|
638
734
|
if (hasHelp) {
|
|
639
|
-
console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=<name>${reset} Install a named skill profile. Profiles:\n core — ${PROFILES.core.length} main-loop skills incl. phase (~130 desc tokens)\n standard — ${PROFILES.standard.length} skills incl. phase, review, config (~700)\n full — all skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi CLI under ~/.kimi-code${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi-code\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline globally${reset}\n npx ${pkg.name} --cline --global\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / COPILOT_HOME / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to the first existing generic skills root: ${cyan}~/.config/agents/skills${reset}, then ${cyan}~/.agents/skills${reset}; if neither exists, GSD creates ${cyan}~/.config/agents${reset}.\n Use ${cyan}--config-dir ~/.kimi-code${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi-code${reset} for brand-specific Kimi installs.\n`);
|
|
735
|
+
console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--zcode${reset} Install for ZCode only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=<name>${reset} Install a named skill profile. Profiles:\n core — ${PROFILES.core.length} main-loop skills incl. phase (~130 desc tokens)\n standard — ${PROFILES.standard.length} skills incl. phase, review, config (~700)\n full — all skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi CLI under ~/.kimi-code${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi-code\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline globally${reset}\n npx ${pkg.name} --cline --global\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / COPILOT_HOME / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to the first existing generic skills root: ${cyan}~/.config/agents/skills${reset}, then ${cyan}~/.agents/skills${reset}; if neither exists, GSD creates ${cyan}~/.config/agents${reset}.\n Use ${cyan}--config-dir ~/.kimi-code${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi-code${reset} for brand-specific Kimi installs.\n`);
|
|
640
736
|
process.exit(0);
|
|
641
737
|
}
|
|
642
738
|
|
|
@@ -909,6 +1005,9 @@ function resolveKiloConfigPath(configDir) {
|
|
|
909
1005
|
return path.join(configDir, 'kilo.json');
|
|
910
1006
|
}
|
|
911
1007
|
|
|
1008
|
+
// #2087 — attribution config-path resolvers, keyed by descriptor (hostBehaviors.attributionConfigResolver)
|
|
1009
|
+
const ATTRIBUTION_CONFIG_RESOLVERS = { opencode: resolveOpencodeConfigPath, kilo: resolveKiloConfigPath };
|
|
1010
|
+
|
|
912
1011
|
/**
|
|
913
1012
|
* Strip JSONC comments (// and /* */) from a string to produce valid JSON.
|
|
914
1013
|
* Handles comments inside strings correctly (does not strip them).
|
|
@@ -1070,126 +1169,6 @@ function readGsdEffectiveModelOverrides(targetDir = null) {
|
|
|
1070
1169
|
return { ...(global || {}), ...(projectOverrides || {}) };
|
|
1071
1170
|
}
|
|
1072
1171
|
|
|
1073
|
-
/**
|
|
1074
|
-
* #443 — Read the merged `effort` config block for install-time effort resolution.
|
|
1075
|
-
*
|
|
1076
|
-
* Probes the same config sources as readGsdRuntimeProfileResolver (per-project
|
|
1077
|
-
* `.planning/config.json` wins over `~/.gsd/defaults.json`) but extracts the
|
|
1078
|
-
* `effort` object instead of the model-profile fields.
|
|
1079
|
-
*
|
|
1080
|
-
* Returns the merged `effort` object or null when neither source defines one.
|
|
1081
|
-
* The caller can pass this to resolveInstallTimeEffort() which is pure and
|
|
1082
|
-
* requires no filesystem access beyond what this helper already performs.
|
|
1083
|
-
*
|
|
1084
|
-
* @param {string|null} targetDir Runtime install root (walks up to find .planning/).
|
|
1085
|
-
* @returns {object|null}
|
|
1086
|
-
*/
|
|
1087
|
-
function readGsdEffectiveEffortConfig(targetDir = null) {
|
|
1088
|
-
const homeDefaults = _readGsdConfigFile(
|
|
1089
|
-
path.join(os.homedir(), '.gsd', 'defaults.json'),
|
|
1090
|
-
'~/.gsd/defaults.json'
|
|
1091
|
-
);
|
|
1092
|
-
|
|
1093
|
-
let projectConfig = null;
|
|
1094
|
-
if (targetDir) {
|
|
1095
|
-
let probeDir = path.resolve(targetDir);
|
|
1096
|
-
for (let depth = 0; depth < 8; depth += 1) {
|
|
1097
|
-
const candidate = path.join(probeDir, '.planning', 'config.json');
|
|
1098
|
-
if (fs.existsSync(candidate)) {
|
|
1099
|
-
projectConfig = _readGsdConfigFile(candidate, '.planning/config.json');
|
|
1100
|
-
break;
|
|
1101
|
-
}
|
|
1102
|
-
const parent = path.dirname(probeDir);
|
|
1103
|
-
if (parent === probeDir) break;
|
|
1104
|
-
probeDir = parent;
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
const homeEffort = (homeDefaults && homeDefaults.effort && typeof homeDefaults.effort === 'object' && !Array.isArray(homeDefaults.effort))
|
|
1109
|
-
? homeDefaults.effort
|
|
1110
|
-
: null;
|
|
1111
|
-
const projectEffort = (projectConfig && projectConfig.effort && typeof projectConfig.effort === 'object' && !Array.isArray(projectConfig.effort))
|
|
1112
|
-
? projectConfig.effort
|
|
1113
|
-
: null;
|
|
1114
|
-
|
|
1115
|
-
if (!homeEffort && !projectEffort) return null;
|
|
1116
|
-
|
|
1117
|
-
// Per-project wins on conflict within each sub-field. Merge field-by-field so
|
|
1118
|
-
// a project config that only sets agent_overrides still inherits global
|
|
1119
|
-
// routing_tier_defaults and default.
|
|
1120
|
-
return {
|
|
1121
|
-
...(homeEffort || {}),
|
|
1122
|
-
...(projectEffort || {}),
|
|
1123
|
-
// Deep-merge agent_overrides (project wins per-key)
|
|
1124
|
-
agent_overrides: {
|
|
1125
|
-
...((homeEffort && homeEffort.agent_overrides) || {}),
|
|
1126
|
-
...((projectEffort && projectEffort.agent_overrides) || {}),
|
|
1127
|
-
},
|
|
1128
|
-
};
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
/**
|
|
1133
|
-
* #443 — Resolve install-time effort for a given agent, using the same
|
|
1134
|
-
* precedence chain as resolveEffortInternal() in core.cjs, but operating
|
|
1135
|
-
* on a pre-loaded effortCfg object (no loadConfig side-effects at install).
|
|
1136
|
-
*
|
|
1137
|
-
* Precedence (mirrors resolveEffortInternal):
|
|
1138
|
-
* 1. effortCfg.agent_overrides[agentName]
|
|
1139
|
-
* 2. effortCfg.routing_tier_defaults[agentTier] (if effortCfg present)
|
|
1140
|
-
* — OR manifest tier defaults when effortCfg is null
|
|
1141
|
-
* 3. effortCfg.default
|
|
1142
|
-
* 4. 'high' (hardcoded fallback)
|
|
1143
|
-
*
|
|
1144
|
-
* @param {object|null} effortCfg Result of readGsdEffectiveEffortConfig().
|
|
1145
|
-
* @param {string} agentName e.g. 'gsd-planner'
|
|
1146
|
-
* @returns {string} Universal effort string (low/medium/high/xhigh/max/minimal)
|
|
1147
|
-
*/
|
|
1148
|
-
function resolveInstallTimeEffort(effortCfg, agentName) {
|
|
1149
|
-
// Validates each candidate against the canonical EFFORT_SET (sourced once
|
|
1150
|
-
// from core.cjs) before accepting it, mirroring resolveEffortInternal exactly.
|
|
1151
|
-
// Invalid values fall through to the next precedence layer; final fallback 'high'.
|
|
1152
|
-
|
|
1153
|
-
// Step 1: agent_overrides
|
|
1154
|
-
if (effortCfg) {
|
|
1155
|
-
const ao = effortCfg.agent_overrides;
|
|
1156
|
-
if (ao && typeof ao === 'object' && !Array.isArray(ao)) {
|
|
1157
|
-
const v = ao[agentName];
|
|
1158
|
-
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// Step 2: routing_tier_defaults keyed by the agent's catalog tier
|
|
1163
|
-
const { AGENT_DEFAULT_TIERS, EFFORT_MANIFEST_TIER_DEFAULTS, EFFORT_MANIFEST_DEFAULT } = _getGsdEffortCatalog();
|
|
1164
|
-
const agentTier = AGENT_DEFAULT_TIERS[agentName];
|
|
1165
|
-
if (agentTier) {
|
|
1166
|
-
if (effortCfg && effortCfg.routing_tier_defaults &&
|
|
1167
|
-
typeof effortCfg.routing_tier_defaults === 'object' &&
|
|
1168
|
-
!Array.isArray(effortCfg.routing_tier_defaults)) {
|
|
1169
|
-
const v = effortCfg.routing_tier_defaults[agentTier];
|
|
1170
|
-
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
|
|
1171
|
-
} else if (!effortCfg) {
|
|
1172
|
-
// No effort config — use manifest tier defaults
|
|
1173
|
-
const v = EFFORT_MANIFEST_TIER_DEFAULTS[agentTier];
|
|
1174
|
-
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
|
|
1175
|
-
}
|
|
1176
|
-
// effortCfg exists but has no routing_tier_defaults — fall through
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
// Step 3: effort.default
|
|
1180
|
-
if (effortCfg) {
|
|
1181
|
-
const d = effortCfg.default;
|
|
1182
|
-
if (typeof d === 'string' && GSD_EFFORT_SET.has(d)) return d;
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
// Step 4: manifest default (sourced from config-defaults.manifest.json effort.default)
|
|
1186
|
-
// If even the manifest default is invalid, fall back to 'high'.
|
|
1187
|
-
if (typeof EFFORT_MANIFEST_DEFAULT === 'string' && GSD_EFFORT_SET.has(EFFORT_MANIFEST_DEFAULT)) {
|
|
1188
|
-
return EFFORT_MANIFEST_DEFAULT;
|
|
1189
|
-
}
|
|
1190
|
-
return 'high';
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
1172
|
/**
|
|
1194
1173
|
* #443 — Inject `effort: <value>` into YAML frontmatter of a Claude .md agent
|
|
1195
1174
|
* file in a newline-agnostic way (LF and CRLF source files are both handled).
|
|
@@ -1295,29 +1274,6 @@ const READONLY_AGENT_DISALLOWED_TOOLS = {
|
|
|
1295
1274
|
'gsd-ui-auditor': 'Edit, MultiEdit',
|
|
1296
1275
|
};
|
|
1297
1276
|
|
|
1298
|
-
/**
|
|
1299
|
-
* #2517 — Read a single GSD config file (defaults.json or per-project
|
|
1300
|
-
* config.json) into a plain object, returning null on missing/empty files
|
|
1301
|
-
* and warning to stderr on JSON parse failures so silent corruption can't
|
|
1302
|
-
* mask broken configs (review finding #5).
|
|
1303
|
-
*/
|
|
1304
|
-
function _readGsdConfigFile(absPath, label) {
|
|
1305
|
-
if (!fs.existsSync(absPath)) return null;
|
|
1306
|
-
let raw;
|
|
1307
|
-
try {
|
|
1308
|
-
raw = fs.readFileSync(absPath, 'utf-8');
|
|
1309
|
-
} catch (err) {
|
|
1310
|
-
process.stderr.write(`gsd: warning — could not read ${label} (${absPath}): ${err.message}\n`);
|
|
1311
|
-
return null;
|
|
1312
|
-
}
|
|
1313
|
-
try {
|
|
1314
|
-
return JSON.parse(raw);
|
|
1315
|
-
} catch (err) {
|
|
1316
|
-
process.stderr.write(`gsd: warning — invalid JSON in ${label} (${absPath}): ${err.message}\n`);
|
|
1317
|
-
return null;
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
1277
|
/**
|
|
1322
1278
|
* #2517 — Build a runtime-aware tier resolver for the install path.
|
|
1323
1279
|
*
|
|
@@ -1420,15 +1376,14 @@ function getCommitAttribution(runtime) {
|
|
|
1420
1376
|
|
|
1421
1377
|
let result;
|
|
1422
1378
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
: resolveKiloConfigPath;
|
|
1379
|
+
const _attrResolverKey = _hostBehaviors(runtime).attributionConfigResolver;
|
|
1380
|
+
if (_attrResolverKey && ATTRIBUTION_CONFIG_RESOLVERS[_attrResolverKey]) {
|
|
1381
|
+
const resolveConfigPath = ATTRIBUTION_CONFIG_RESOLVERS[_attrResolverKey];
|
|
1427
1382
|
const config = readSettings(resolveConfigPath(getGlobalConfigDir(runtime, null)));
|
|
1428
1383
|
result = (config && config.disable_ai_attribution === true) ? null : undefined;
|
|
1429
|
-
} else if (runtime === '
|
|
1384
|
+
} else if (_hostBehaviors(runtime).attributionSource === 'settings-json-commit') {
|
|
1430
1385
|
// Claude Code
|
|
1431
|
-
const settings = readSettings(path.join(getGlobalConfigDir(
|
|
1386
|
+
const settings = readSettings(path.join(getGlobalConfigDir(runtime, explicitConfigDir), 'settings.json'));
|
|
1432
1387
|
if (!settings || !settings.attribution || settings.attribution.commit === undefined) {
|
|
1433
1388
|
result = undefined;
|
|
1434
1389
|
} else if (settings.attribution.commit === '') {
|
|
@@ -1896,7 +1851,7 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
|
|
|
1896
1851
|
// Hermes' SKILL.md spec lists `version` as a required frontmatter field.
|
|
1897
1852
|
// Track GSD's package version so Hermes' skill_view() reports a stable
|
|
1898
1853
|
// identifier per install.
|
|
1899
|
-
if (runtime
|
|
1854
|
+
if (_hostBehaviors(runtime).skillFrontmatterVersion) fm += `version: ${yamlQuote(pkg.version)}\n`;
|
|
1900
1855
|
// #778 (b) — Qwen-only numeric priority for /skills ordering. Scoped to qwen
|
|
1901
1856
|
// so Claude/Hermes skill frontmatter is unchanged (they ignore the field, but
|
|
1902
1857
|
// we keep their output byte-stable). skillName is the `gsd-<stem>` dir name.
|
|
@@ -3265,6 +3220,77 @@ function cleanupWindsurfLegacyDevinSkills(workspaceDir) {
|
|
|
3265
3220
|
return removed;
|
|
3266
3221
|
}
|
|
3267
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
|
+
|
|
3268
3294
|
/**
|
|
3269
3295
|
* Generate the GSD config block for Codex config.toml.
|
|
3270
3296
|
* @param {Array<{name: string, description: string}>} agents
|
|
@@ -3280,6 +3306,17 @@ function generateCodexConfigBlock(agents, targetDir) {
|
|
|
3280
3306
|
'',
|
|
3281
3307
|
];
|
|
3282
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
|
+
|
|
3283
3320
|
for (const { name, description } of agents) {
|
|
3284
3321
|
// #2727 — Codex 0.124.0 requires [agents.<name>] struct format, not [[agents]] sequence.
|
|
3285
3322
|
// [[agents]] (introduced in #2645) is rejected by codex-cli 0.124.0 with
|
|
@@ -3293,6 +3330,52 @@ function generateCodexConfigBlock(agents, targetDir) {
|
|
|
3293
3330
|
return lines.join('\n');
|
|
3294
3331
|
}
|
|
3295
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
|
+
|
|
3296
3379
|
/**
|
|
3297
3380
|
* Strip any managed GSD agent sections from a TOML string.
|
|
3298
3381
|
*
|
|
@@ -3315,6 +3398,16 @@ function stripCodexGsdAgentSections(content) {
|
|
|
3315
3398
|
return true;
|
|
3316
3399
|
}
|
|
3317
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
|
+
|
|
3318
3411
|
// Legacy `[[agents]]` array-of-tables (#2645) — only strip blocks whose
|
|
3319
3412
|
// `name = "gsd-..."`, preserving user-authored [[agents]] entries.
|
|
3320
3413
|
if (section.array && section.path === 'agents') {
|
|
@@ -3342,7 +3435,11 @@ function stripGsdFromCodexConfig(content) {
|
|
|
3342
3435
|
const codexHooksOwnership = getManagedCodexHooksOwnership(content);
|
|
3343
3436
|
|
|
3344
3437
|
if (markerIndex !== -1) {
|
|
3345
|
-
// 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));
|
|
3346
3443
|
let before = content.substring(0, markerIndex);
|
|
3347
3444
|
before = stripCodexHooksFeatureAssignments(before, codexHooksOwnership);
|
|
3348
3445
|
// Also strip GSD-injected feature keys above the marker (Case 3 inject)
|
|
@@ -3351,6 +3448,9 @@ function stripGsdFromCodexConfig(content) {
|
|
|
3351
3448
|
before = before.replace(/^\[features\]\s*\n(?=\[|$)/m, '');
|
|
3352
3449
|
before = before.replace(/^\[agents\]\s*\n(?=\[|$)/m, '');
|
|
3353
3450
|
before = before.replace(/^(?:\r?\n)+/, '').trimEnd();
|
|
3451
|
+
if (preservedScalars.length > 0) {
|
|
3452
|
+
before = (before ? before + eol + eol : '') + '[agents]' + eol + preservedScalars.join(eol);
|
|
3453
|
+
}
|
|
3354
3454
|
if (!before) return null;
|
|
3355
3455
|
return before + eol;
|
|
3356
3456
|
}
|
|
@@ -3361,7 +3461,11 @@ function stripGsdFromCodexConfig(content) {
|
|
|
3361
3461
|
cleaned = cleaned.replace(/^multi_agent\s*=\s*true\s*(?:\r?\n)?/m, '');
|
|
3362
3462
|
cleaned = cleaned.replace(/^default_mode_request_user_input\s*=\s*true\s*(?:\r?\n)?/m, '');
|
|
3363
3463
|
|
|
3364
|
-
//
|
|
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.
|
|
3365
3469
|
cleaned = stripCodexGsdAgentSections(cleaned);
|
|
3366
3470
|
|
|
3367
3471
|
// Remove [features] section if now empty (only header, no keys before next section)
|
|
@@ -3372,6 +3476,10 @@ function stripGsdFromCodexConfig(content) {
|
|
|
3372
3476
|
|
|
3373
3477
|
cleaned = cleaned.replace(/^(?:\r?\n)+/, '').trimEnd();
|
|
3374
3478
|
|
|
3479
|
+
if (preservedScalars.length > 0) {
|
|
3480
|
+
cleaned = (cleaned ? cleaned + eol + eol : '') + '[agents]' + eol + preservedScalars.join(eol);
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3375
3483
|
if (!cleaned) return null;
|
|
3376
3484
|
return cleaned + eol;
|
|
3377
3485
|
}
|
|
@@ -4843,6 +4951,33 @@ function parseTomlToObject(content) {
|
|
|
4843
4951
|
* - `hooks.<Event>` MUST be an array of tables when present (Codex ≥0.124
|
|
4844
4952
|
* rejects bare `[hooks.<Event>]` single-bracket maps).
|
|
4845
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
|
+
|
|
4846
4981
|
function validateCodexConfigSchema(content) {
|
|
4847
4982
|
let parsed;
|
|
4848
4983
|
try {
|
|
@@ -4871,10 +5006,21 @@ function validateCodexConfigSchema(content) {
|
|
|
4871
5006
|
}
|
|
4872
5007
|
|
|
4873
5008
|
if (!section.array && section.path === 'agents') {
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
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
|
+
}
|
|
4878
5024
|
}
|
|
4879
5025
|
|
|
4880
5026
|
// hooks.state.* is Codex's persistent hook-trust namespace (added in
|
|
@@ -5150,7 +5296,13 @@ function mergeCodexConfig(configPath, gsdBlock) {
|
|
|
5150
5296
|
|
|
5151
5297
|
const existing = fs.readFileSync(configPath, 'utf8');
|
|
5152
5298
|
const eol = detectLineEnding(existing);
|
|
5153
|
-
|
|
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);
|
|
5154
5306
|
const markerIndex = existing.indexOf(GSD_CODEX_MARKER);
|
|
5155
5307
|
|
|
5156
5308
|
// Case 2: Has GSD marker — truncate and re-append
|
|
@@ -6104,62 +6256,14 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
|
|
|
6104
6256
|
// convertClaudeCommandToKiloSkill: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
6105
6257
|
// Imported from installEngine above.
|
|
6106
6258
|
|
|
6107
|
-
/**
|
|
6108
|
-
* Copy commands to a flat structure for OpenCode
|
|
6109
|
-
* OpenCode expects: command/gsd-help.md (invoked as /gsd-help)
|
|
6110
|
-
* Source structure: commands/gsd/help.md
|
|
6111
|
-
*
|
|
6112
|
-
* @param {string} srcDir - Source directory (e.g., commands/gsd/)
|
|
6113
|
-
* @param {string} destDir - Destination directory (e.g., command/)
|
|
6114
|
-
* @param {string} prefix - Prefix for filenames (e.g., 'gsd')
|
|
6115
|
-
* @param {string} pathPrefix - Path prefix for file references
|
|
6116
|
-
* @param {string} runtime - Target runtime ('claude', 'opencode', or 'kilo')
|
|
6117
|
-
*/
|
|
6118
6259
|
// applyOpencodeFamilyPathPrefix: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
6119
6260
|
// Imported from installEngine above.
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
// Remove old gsd-*.md files before copying new ones
|
|
6127
|
-
if (fs.existsSync(destDir)) {
|
|
6128
|
-
for (const file of fs.readdirSync(destDir)) {
|
|
6129
|
-
if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
|
|
6130
|
-
fs.unlinkSync(path.join(destDir, file));
|
|
6131
|
-
}
|
|
6132
|
-
}
|
|
6133
|
-
} else {
|
|
6134
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
6135
|
-
}
|
|
6136
|
-
|
|
6137
|
-
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
6138
|
-
|
|
6139
|
-
for (const entry of entries) {
|
|
6140
|
-
const srcPath = path.join(srcDir, entry.name);
|
|
6141
|
-
|
|
6142
|
-
if (entry.isDirectory()) {
|
|
6143
|
-
// Recurse into subdirectories, adding to prefix
|
|
6144
|
-
// e.g., commands/gsd/debug/start.md -> command/gsd-debug-start.md
|
|
6145
|
-
copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
|
|
6146
|
-
} else if (entry.name.endsWith('.md')) {
|
|
6147
|
-
// Flatten: help.md -> gsd-help.md
|
|
6148
|
-
const baseName = entry.name.replace('.md', '');
|
|
6149
|
-
const destName = `${prefix}-${baseName}.md`;
|
|
6150
|
-
const destPath = path.join(destDir, destName);
|
|
6151
|
-
|
|
6152
|
-
let content = fs.readFileSync(srcPath, 'utf8');
|
|
6153
|
-
content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix);
|
|
6154
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6155
|
-
content = runtime === 'kilo'
|
|
6156
|
-
? convertClaudeToKiloFrontmatter(content)
|
|
6157
|
-
: convertClaudeToOpencodeFrontmatter(content);
|
|
6158
|
-
|
|
6159
|
-
fs.writeFileSync(destPath, content);
|
|
6160
|
-
}
|
|
6161
|
-
}
|
|
6162
|
-
}
|
|
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.
|
|
6163
6267
|
|
|
6164
6268
|
function listCodexSkillNames(skillsDir, prefix = 'gsd-') {
|
|
6165
6269
|
if (!fs.existsSync(skillsDir)) return [];
|
|
@@ -6461,7 +6565,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
6461
6565
|
// copyWithPathReplacement is the emit path for gsd-core/workflows/*.md;
|
|
6462
6566
|
// _applyRuntimeRewrites is NOT invoked here, so this is what makes the fix
|
|
6463
6567
|
// live in real installs (it is a no-op for files without those lines).
|
|
6464
|
-
if (runtime
|
|
6568
|
+
if (!_hostBehaviors(runtime).authorsCanonicalWorkflow) {
|
|
6465
6569
|
content = _stampNonClaudeRuntimeDefaults(content, runtime);
|
|
6466
6570
|
}
|
|
6467
6571
|
|
|
@@ -6663,16 +6767,20 @@ const GSD_UNINSTALL_HOOKS = [
|
|
|
6663
6767
|
* @param {boolean} isGlobal - Whether to uninstall from global or local
|
|
6664
6768
|
* @param {string} runtime - Target runtime ('claude', 'opencode', 'codex', 'copilot')
|
|
6665
6769
|
*/
|
|
6666
|
-
function uninstall(isGlobal, runtime =
|
|
6770
|
+
function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
6667
6771
|
const { isOpencode, isKilo, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
6668
6772
|
const dirName = getDirName(runtime);
|
|
6669
6773
|
|
|
6670
6774
|
// Get the target directory based on runtime and install type. Cline local
|
|
6671
6775
|
// installs write to the project root (.clinerules/ lives at the root, not in
|
|
6672
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.
|
|
6673
6781
|
const targetDir = isGlobal
|
|
6674
6782
|
? getGlobalConfigDir(runtime, explicitConfigDir)
|
|
6675
|
-
: runtime
|
|
6783
|
+
: _hostBehaviors(runtime).localTargetIsProjectRoot
|
|
6676
6784
|
? process.cwd()
|
|
6677
6785
|
: path.join(process.cwd(), dirName);
|
|
6678
6786
|
|
|
@@ -6721,11 +6829,34 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6721
6829
|
|
|
6722
6830
|
// 1. Remove GSD commands/skills (layout-driven)
|
|
6723
6831
|
const scope = isGlobal ? 'global' : 'local';
|
|
6724
|
-
|
|
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
|
+
}
|
|
6725
6840
|
removedCount++;
|
|
6726
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
|
+
|
|
6727
6858
|
// 1a. Non-layout Codex side-effects: agent .toml files, config.toml sections, hooks.json
|
|
6728
|
-
if (
|
|
6859
|
+
if (_hostBehaviors(runtime).tomlConfigInstall) {
|
|
6729
6860
|
const codexAgentsDir = path.join(targetDir, 'agents');
|
|
6730
6861
|
if (fs.existsSync(codexAgentsDir)) {
|
|
6731
6862
|
const tomlFiles = fs.readdirSync(codexAgentsDir);
|
|
@@ -6764,8 +6895,10 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6764
6895
|
console.log(` ${green}✓${reset} Removed managed Codex SessionStart hook from hooks.json`);
|
|
6765
6896
|
}
|
|
6766
6897
|
|
|
6767
|
-
// #772: remove
|
|
6768
|
-
|
|
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) {
|
|
6769
6902
|
const eventCleanup = removeCodexHooksJsonEvent(targetDir, eventName);
|
|
6770
6903
|
if (eventCleanup.changed) {
|
|
6771
6904
|
removedCount++;
|
|
@@ -6812,7 +6945,9 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6812
6945
|
// 1b-cline. Non-layout Cline side-effects (issue #787): remove the
|
|
6813
6946
|
// directory-form rules + PreToolUse hook, and strip the GSD block from the
|
|
6814
6947
|
// global cross-tool ~/.agents/AGENTS.md target.
|
|
6815
|
-
|
|
6948
|
+
// Descriptor-driven (ADR-1239 / #2090): folded from `runtime === 'cline'`
|
|
6949
|
+
// into hostBehaviors.clineRulesSurface.
|
|
6950
|
+
if (_hostBehaviors(runtime).clineRulesSurface) {
|
|
6816
6951
|
const clinerulesDir = path.join(targetDir, '.clinerules');
|
|
6817
6952
|
for (const rel of ['gsd.md', path.join('hooks', 'PreToolUse')]) {
|
|
6818
6953
|
const p = path.join(clinerulesDir, rel);
|
|
@@ -6858,17 +6993,20 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6858
6993
|
}
|
|
6859
6994
|
}
|
|
6860
6995
|
|
|
6861
|
-
// 1b-cursor.
|
|
6862
|
-
// hook entries from hooks.json and clean up the managed hook
|
|
6863
|
-
|
|
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) {
|
|
6864
7001
|
const hooksJsonCleanup = removeCursorHooksJson(targetDir);
|
|
6865
7002
|
if (hooksJsonCleanup.changed) {
|
|
6866
7003
|
removedCount++;
|
|
6867
7004
|
console.log(` ${green}✓${reset} Removed GSD-managed Cursor hooks from hooks.json`);
|
|
6868
7005
|
}
|
|
6869
|
-
// Remove
|
|
7006
|
+
// Remove all GSD-managed hook scripts (sessionStart, postToolUse, preToolUse,
|
|
7007
|
+
// stop, subagentStart, subagentStop — AC4a, #2089).
|
|
6870
7008
|
const hooksDir = path.join(targetDir, 'hooks');
|
|
6871
|
-
for (const script of
|
|
7009
|
+
for (const script of GSD_CURSOR_HOOK_SCRIPTS) {
|
|
6872
7010
|
const p = path.join(hooksDir, script);
|
|
6873
7011
|
try {
|
|
6874
7012
|
if (fs.existsSync(p)) {
|
|
@@ -6887,7 +7025,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6887
7025
|
|
|
6888
7026
|
// 1c. Claude local: remove flat gsd-*.md commands from commands/ (current layout,
|
|
6889
7027
|
// #1367 fix). Also remove legacy commands/gsd/ subdirectory from prior installs.
|
|
6890
|
-
if (!isGlobal && runtime === '
|
|
7028
|
+
if (!isGlobal && _hostBehaviors(runtime).localInstallStyle === 'legacy-flat') {
|
|
6891
7029
|
const commandsDir = path.join(targetDir, 'commands');
|
|
6892
7030
|
// Remove flat gsd-*.md files (current layout after #1367 fix)
|
|
6893
7031
|
if (fs.existsSync(commandsDir)) {
|
|
@@ -6929,7 +7067,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6929
7067
|
// removes the directory; we must preserve/restore user artifacts before that path.
|
|
6930
7068
|
// This block runs AFTER uninstallRuntimeArtifacts, so we check if the directory
|
|
6931
7069
|
// was already removed and skip if so (idempotent).
|
|
6932
|
-
if (isQwen ||
|
|
7070
|
+
if (isQwen || _hostBehaviors(runtime).legacyCommandsGsdCleanup === true) {
|
|
6933
7071
|
// dev-preferences may have survived in skills/ as SKILL.md — nothing to do for
|
|
6934
7072
|
// that case. If a stale commands/gsd/ still exists (e.g. legacy was not removed),
|
|
6935
7073
|
// attempt migration. In practice _runLegacyUninstallCleanup removes it first,
|
|
@@ -7042,9 +7180,10 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
7042
7180
|
// 4z. Remove the OpenCode native plugin adapter (#1914). Only GSD's own
|
|
7043
7181
|
// plugin file is removed; the plugins/ dir is pruned only if it becomes
|
|
7044
7182
|
// empty, preserving any user-authored OpenCode plugins.
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
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);
|
|
7048
7187
|
if (fs.existsSync(pluginPath)) {
|
|
7049
7188
|
try {
|
|
7050
7189
|
fs.unlinkSync(pluginPath);
|
|
@@ -7190,7 +7329,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
7190
7329
|
// to preserve any user-added allow/deny entries.
|
|
7191
7330
|
// Uses a local flag to avoid the shared `settingsModified` producing a false
|
|
7192
7331
|
// "Removed GSD permissions" message when only hooks/statusline changed.
|
|
7193
|
-
if (runtime === 'claude' && settings.permissions) {
|
|
7332
|
+
if (_hostBehaviors(runtime).permissionsSchema === 'claude' && settings.permissions) {
|
|
7194
7333
|
let permissionsModified = false;
|
|
7195
7334
|
if (Array.isArray(settings.permissions.allow)) {
|
|
7196
7335
|
const before = settings.permissions.allow.length;
|
|
@@ -7223,7 +7362,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
7223
7362
|
}
|
|
7224
7363
|
|
|
7225
7364
|
// 6. For OpenCode, clean up permissions from opencode.json or opencode.jsonc
|
|
7226
|
-
if (
|
|
7365
|
+
if (resolveInstallPlan(runtime).finishPermissionWriter === 'opencode') {
|
|
7227
7366
|
const configPath = resolveOpencodeConfigPath(targetDir);
|
|
7228
7367
|
if (fs.existsSync(configPath)) {
|
|
7229
7368
|
try {
|
|
@@ -7665,19 +7804,24 @@ function resolveInstallRelativePath(baseDir, relPath) {
|
|
|
7665
7804
|
/**
|
|
7666
7805
|
* Write file manifest after installation for future modification detection
|
|
7667
7806
|
*/
|
|
7668
|
-
function writeManifest(configDir, runtime =
|
|
7807
|
+
function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
7669
7808
|
const { isOpencode, isKilo, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
7670
7809
|
const gsdDir = path.join(configDir, 'gsd-core');
|
|
7671
7810
|
// #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/).
|
|
7672
7811
|
// Claude local uses flatCommandsDir instead for manifest recording.
|
|
7673
7812
|
const flatCommandsDir = path.join(configDir, 'commands');
|
|
7674
|
-
const opencodeCommandDir = path.join(configDir, 'command');
|
|
7675
|
-
// 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').
|
|
7676
7816
|
// All other runtimes that use the Codex-style skills layout use a flat skills/ root.
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
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/';
|
|
7681
7825
|
const agentsDir = path.join(configDir, 'agents');
|
|
7682
7826
|
const manifest = {
|
|
7683
7827
|
version: pkg.version,
|
|
@@ -7703,21 +7847,21 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
7703
7847
|
// Claude local (#1367): flat gsd-*.md files at commands/ level.
|
|
7704
7848
|
// Only claude local writes gsd-*.md here; global installs don't emit commands,
|
|
7705
7849
|
// so this branch is a no-op for global (no matching files to find).
|
|
7706
|
-
if (runtime === '
|
|
7850
|
+
if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && fs.existsSync(flatCommandsDir)) {
|
|
7707
7851
|
for (const file of fs.readdirSync(flatCommandsDir)) {
|
|
7708
7852
|
if (file.startsWith('gsd-') && file.endsWith('.md')) {
|
|
7709
7853
|
manifest.files['commands/' + file] = fileHash(path.join(flatCommandsDir, file));
|
|
7710
7854
|
}
|
|
7711
7855
|
}
|
|
7712
7856
|
}
|
|
7713
|
-
if ((
|
|
7857
|
+
if (_hostBehaviors(runtime).flatCommandDir && fs.existsSync(opencodeCommandDir)) {
|
|
7714
7858
|
for (const file of fs.readdirSync(opencodeCommandDir)) {
|
|
7715
7859
|
if (file.startsWith('gsd-') && file.endsWith('.md')) {
|
|
7716
7860
|
manifest.files['command/' + file] = fileHash(path.join(opencodeCommandDir, file));
|
|
7717
7861
|
}
|
|
7718
7862
|
}
|
|
7719
7863
|
}
|
|
7720
|
-
if ((
|
|
7864
|
+
if (!_hostBehaviors(runtime).skipCodexSkillsManifest && fs.existsSync(codexSkillsDir)) {
|
|
7721
7865
|
// All runtimes (including Hermes post-#947) use the canonical 'gsd-' prefix.
|
|
7722
7866
|
const skillListPrefix = 'gsd-';
|
|
7723
7867
|
for (const skillName of listCodexSkillNames(codexSkillsDir, skillListPrefix)) {
|
|
@@ -7727,8 +7871,8 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
7727
7871
|
manifest.files[`${codexSkillsManifestPrefix}${skillName}/${rel}`] = hash;
|
|
7728
7872
|
}
|
|
7729
7873
|
}
|
|
7730
|
-
//
|
|
7731
|
-
if (
|
|
7874
|
+
// Descriptor-driven (#2090): hash the category DESCRIPTION.md so reinstall detects drift.
|
|
7875
|
+
if (_hostBehaviors(runtime).trackCategoryDescription) {
|
|
7732
7876
|
const descPath = path.join(codexSkillsDir, 'DESCRIPTION.md');
|
|
7733
7877
|
if (fs.existsSync(descPath)) {
|
|
7734
7878
|
manifest.files['skills/gsd/DESCRIPTION.md'] = fileHash(descPath);
|
|
@@ -7754,7 +7898,9 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
7754
7898
|
// Track Cline directory-form artifacts in the manifest (issue #787): the
|
|
7755
7899
|
// rules file and the PreToolUse hook. (~/.agents/AGENTS.md is tracked via its
|
|
7756
7900
|
// marker block, not the per-configDir manifest, since it lives outside it.)
|
|
7757
|
-
|
|
7901
|
+
// Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
|
|
7902
|
+
// hostBehaviors.clineRulesSurface.
|
|
7903
|
+
if (_hostBehaviors(runtime).clineRulesSurface) {
|
|
7758
7904
|
for (const rel of ['.clinerules/gsd.md', '.clinerules/hooks/PreToolUse']) {
|
|
7759
7905
|
const dest = path.join(configDir, rel);
|
|
7760
7906
|
if (fs.existsSync(dest)) {
|
|
@@ -7765,7 +7911,9 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
7765
7911
|
|
|
7766
7912
|
// Track hook files so saveLocalPatches() can detect user modifications
|
|
7767
7913
|
// Hooks are only installed for runtimes that use settings.json (not Codex/Copilot/Cline)
|
|
7768
|
-
|
|
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) {
|
|
7769
7917
|
const hooksDir = path.join(configDir, 'hooks');
|
|
7770
7918
|
if (fs.existsSync(hooksDir)) {
|
|
7771
7919
|
// Drive from INSTALLED_HOOK_FILES (the canonical HOOKS_TO_COPY set from
|
|
@@ -7829,10 +7977,11 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
7829
7977
|
|
|
7830
7978
|
// Track the OpenCode native plugin adapter (#1914) so update/drift detection
|
|
7831
7979
|
// and uninstall can account for it.
|
|
7832
|
-
|
|
7833
|
-
|
|
7980
|
+
const _npM = _hostBehaviors(runtime).nativePlugin;
|
|
7981
|
+
if (_npM) {
|
|
7982
|
+
const pluginInstallPath = path.join(configDir, _npM.dir, _npM.file);
|
|
7834
7983
|
if (fs.existsSync(pluginInstallPath)) {
|
|
7835
|
-
manifest.files[
|
|
7984
|
+
manifest.files[`${_npM.dir}/${_npM.file}`] = fileHash(pluginInstallPath);
|
|
7836
7985
|
}
|
|
7837
7986
|
}
|
|
7838
7987
|
|
|
@@ -8116,7 +8265,7 @@ function saveLocalPatches(configDir, pristineCtx) {
|
|
|
8116
8265
|
/**
|
|
8117
8266
|
* After install, report backed-up patches for user to reapply.
|
|
8118
8267
|
*/
|
|
8119
|
-
function reportLocalPatches(configDir, runtime =
|
|
8268
|
+
function reportLocalPatches(configDir, runtime = DEFAULT_RUNTIME) {
|
|
8120
8269
|
const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
|
|
8121
8270
|
const metaPath = path.join(patchesDir, 'backup-meta.json');
|
|
8122
8271
|
if (!fs.existsSync(metaPath)) return [];
|
|
@@ -8125,15 +8274,11 @@ function reportLocalPatches(configDir, runtime = 'claude') {
|
|
|
8125
8274
|
try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; }
|
|
8126
8275
|
|
|
8127
8276
|
if (meta.files && meta.files.length > 0) {
|
|
8128
|
-
const reapplyCommand = (runtime
|
|
8129
|
-
?
|
|
8130
|
-
: runtime === '
|
|
8131
|
-
? '
|
|
8132
|
-
:
|
|
8133
|
-
? 'gsd-update --reapply (mention the skill name)'
|
|
8134
|
-
: runtime === 'kimi'
|
|
8135
|
-
? '/skill:gsd-update --reapply'
|
|
8136
|
-
: '/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';
|
|
8137
8282
|
console.log('');
|
|
8138
8283
|
console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
|
|
8139
8284
|
for (const f of meta.files) {
|
|
@@ -8159,8 +8304,8 @@ function reportInstallerMigrationResult(result) {
|
|
|
8159
8304
|
}
|
|
8160
8305
|
}
|
|
8161
8306
|
|
|
8162
|
-
function install(isGlobal, runtime =
|
|
8163
|
-
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);
|
|
8164
8309
|
const plan = resolveInstallPlan(runtime);
|
|
8165
8310
|
const dirName = getDirName(runtime);
|
|
8166
8311
|
const src = path.join(__dirname, '..');
|
|
@@ -8213,15 +8358,17 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8213
8358
|
};
|
|
8214
8359
|
|
|
8215
8360
|
// Get the target directory based on runtime and install type.
|
|
8216
|
-
//
|
|
8217
|
-
// 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.
|
|
8218
8365
|
// #791: antigravity local installs write to .agents/ (canonical). The legacy .agent/
|
|
8219
8366
|
// directory is recognized by RUNTIME_DIRS (update-context) and _LEGACY_SCAN_SUBDIR_NAMES
|
|
8220
8367
|
// but NOT auto-removed here; legacy .agent/ gsd artifacts are recognized but not
|
|
8221
8368
|
// auto-removed on reinstall (dual-read fallback per issue #791 spec).
|
|
8222
8369
|
const targetDir = isGlobal
|
|
8223
8370
|
? getGlobalConfigDir(runtime, explicitConfigDir)
|
|
8224
|
-
:
|
|
8371
|
+
: _hostBehaviors(runtime).localTargetIsProjectRoot
|
|
8225
8372
|
? process.cwd()
|
|
8226
8373
|
: path.join(process.cwd(), dirName);
|
|
8227
8374
|
|
|
@@ -8300,7 +8447,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8300
8447
|
const isWindowsHost = process.platform === 'win32';
|
|
8301
8448
|
const pathPrefix = computePathPrefix({
|
|
8302
8449
|
isGlobal,
|
|
8303
|
-
isOpencode,
|
|
8450
|
+
isOpencode: _hostBehaviors(runtime).skipHomePrefixSubstitution === true,
|
|
8304
8451
|
isWindowsHost,
|
|
8305
8452
|
resolvedTarget,
|
|
8306
8453
|
homeDir,
|
|
@@ -8366,8 +8513,8 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8366
8513
|
// Map<filename, Buffer> — content snapshot of each pre-existing gsd-* agent file.
|
|
8367
8514
|
const codexPreInstallAgentContents = new Map();
|
|
8368
8515
|
let codexPreInstallVersionBytes = null;
|
|
8369
|
-
if (
|
|
8370
|
-
const _preSkillsDir =
|
|
8516
|
+
if (_hostBehaviors(runtime).tomlConfigInstall && !isMinimalMode(_effectiveInstallMode)) {
|
|
8517
|
+
const _preSkillsDir = _resolveSkillsRootDir(runtime, targetDir, isGlobal ? 'global' : 'local');
|
|
8371
8518
|
if (fs.existsSync(_preSkillsDir)) {
|
|
8372
8519
|
for (const entry of fs.readdirSync(_preSkillsDir, { withFileTypes: true })) {
|
|
8373
8520
|
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
|
@@ -8420,10 +8567,10 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8420
8567
|
// atomic-write temp files. It is safe to call before any writes have happened.
|
|
8421
8568
|
// The full restoreCodexSnapshot() (defined inside the config block) additionally
|
|
8422
8569
|
// handles config.toml, which is not yet touched at this point in the pipeline.
|
|
8423
|
-
const _codexPreConfigRollback = !
|
|
8570
|
+
const _codexPreConfigRollback = !_hostBehaviors(runtime).tomlConfigInstall || isMinimalMode(_effectiveInstallMode) ? null : () => {
|
|
8424
8571
|
rollbackInstallerMigrations();
|
|
8425
8572
|
// skills/gsd-* — pass 1: restore snapshot entries (may be absent if deleted mid-install).
|
|
8426
|
-
const _earlySkillsDir =
|
|
8573
|
+
const _earlySkillsDir = _resolveSkillsRootDir(runtime, targetDir, isGlobal ? 'global' : 'local');
|
|
8427
8574
|
for (const skillName of codexPreInstallSkillNames) {
|
|
8428
8575
|
const skillDirPath = path.join(_earlySkillsDir, skillName);
|
|
8429
8576
|
const fileMap = codexPreInstallSkillContents.get(skillName);
|
|
@@ -8586,23 +8733,58 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8586
8733
|
// Hermes: writeHermesCategoryDescription (not a layout kind)
|
|
8587
8734
|
// Cline global: skills emitted via layout; .clinerules still written below (#782)
|
|
8588
8735
|
// Cline local: no skills (only .clinerules) — falls through to cline-rules surface
|
|
8589
|
-
// OpenCode/Kilo: copyFlattenedCommands (frontmatter conversion not in commandsKind)
|
|
8590
8736
|
// Claude local: copyWithPathReplacement + stale-skills cleanup
|
|
8591
8737
|
|
|
8592
8738
|
// Layout-driven path for all skills-based runtimes (full and minimal modes).
|
|
8593
8739
|
// applyRuntimeContentRewritesInPlace (called inside installRuntimeArtifacts)
|
|
8594
8740
|
// handles per-runtime path + branding rewrites, including Qwen/Hermes.
|
|
8595
8741
|
// Cline global: emit skills to ~/.cline/skills/ (Cline >= v3.48.0 — #782).
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8742
|
+
// Descriptor-driven (ADR-1016 / ADR-1239): a runtime takes the layout-driven
|
|
8743
|
+
// installRuntimeArtifacts path when its scoped artifactLayout is non-empty
|
|
8744
|
+
// (it declares any skills/commands/agents/kimi-agents kind for this scope).
|
|
8745
|
+
// This replaces the prior hardcoded `isCodex || isCopilot || ...` roster so a
|
|
8746
|
+
// newly-added runtime with an artifact layout installs without a per-runtime
|
|
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
|
|
8753
|
+
// (copyWithPathReplacement + stale-skills cleanup).
|
|
8754
|
+
const _isSkillsRuntime = (() => {
|
|
8755
|
+
if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && !isGlobal) return false; // legacy flat local path (descriptor-driven; #2086)
|
|
8756
|
+
const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime];
|
|
8757
|
+
const layout = cap && cap.runtime && cap.runtime.artifactLayout;
|
|
8758
|
+
if (!layout) return false;
|
|
8759
|
+
const scopeLayout = isGlobal ? layout.global : layout.local;
|
|
8760
|
+
return Array.isArray(scopeLayout) && scopeLayout.length > 0;
|
|
8761
|
+
})();
|
|
8601
8762
|
|
|
8602
8763
|
if (_isSkillsRuntime) {
|
|
8603
8764
|
// Layout-driven install for skills-based runtimes (full and minimal modes)
|
|
8604
8765
|
const scope = isGlobal ? 'global' : 'local';
|
|
8605
|
-
|
|
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
|
+
}
|
|
8606
8788
|
|
|
8607
8789
|
// #1326 — Codex only: remove stale agents/openai.yaml sidecars from managed
|
|
8608
8790
|
// gsd-* skill dirs. Prior installs wrote these files so Codex would show a
|
|
@@ -8610,8 +8792,23 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8610
8792
|
// index BOTH SKILL.md and the sidecar, causing each GSD skill to appear twice
|
|
8611
8793
|
// in autocomplete. Cleaning them up fixes the duplication; SKILL.md alone is
|
|
8612
8794
|
// sufficient for Codex discovery. User-owned dirs are never touched.
|
|
8613
|
-
if (
|
|
8614
|
-
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
|
+
}
|
|
8615
8812
|
}
|
|
8616
8813
|
|
|
8617
8814
|
// #1629 Finding B: Windsurf local only — remove legacy .devin/skills/gsd-*
|
|
@@ -8625,13 +8822,13 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8625
8822
|
}
|
|
8626
8823
|
}
|
|
8627
8824
|
|
|
8628
|
-
//
|
|
8629
|
-
if (
|
|
8825
|
+
// Descriptor-driven (#2090): write DESCRIPTION.md for the gsd/ category after layout install
|
|
8826
|
+
if (_hostBehaviors(runtime).writeCategoryDescription) {
|
|
8630
8827
|
writeHermesCategoryDescription(path.join(targetDir, 'skills', 'gsd'));
|
|
8631
8828
|
}
|
|
8632
8829
|
|
|
8633
8830
|
// Verify installed artifacts and report
|
|
8634
|
-
if (
|
|
8831
|
+
if (_hostBehaviors(runtime).reportSkillsCount) {
|
|
8635
8832
|
const hermesSkillsDir = path.join(targetDir, 'skills', 'gsd');
|
|
8636
8833
|
if (fs.existsSync(hermesSkillsDir)) {
|
|
8637
8834
|
// Hermes layout uses prefix: 'gsd-' (#947) — skill dirs have gsd-<stem> names
|
|
@@ -8683,7 +8880,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8683
8880
|
}
|
|
8684
8881
|
}
|
|
8685
8882
|
} else {
|
|
8686
|
-
const skillsDir =
|
|
8883
|
+
const skillsDir = _skillsRootDir;
|
|
8687
8884
|
if (fs.existsSync(skillsDir)) {
|
|
8688
8885
|
const count = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
8689
8886
|
.filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length;
|
|
@@ -8711,8 +8908,9 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8711
8908
|
}
|
|
8712
8909
|
}
|
|
8713
8910
|
|
|
8714
|
-
//
|
|
8715
|
-
|
|
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) {
|
|
8716
8914
|
const commandsDir = path.join(targetDir, 'commands');
|
|
8717
8915
|
if (fs.existsSync(commandsDir)) {
|
|
8718
8916
|
const cmdCount = fs.readdirSync(commandsDir)
|
|
@@ -8743,68 +8941,12 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8743
8941
|
}
|
|
8744
8942
|
}
|
|
8745
8943
|
}
|
|
8746
|
-
} else if (
|
|
8747
|
-
// OpenCode/Kilo: flat structure in command/ directory
|
|
8748
|
-
const commandDir = path.join(targetDir, 'command');
|
|
8749
|
-
fs.mkdirSync(commandDir, { recursive: true });
|
|
8750
|
-
|
|
8751
|
-
// Copy commands/gsd/*.md as command/gsd-*.md (flatten structure)
|
|
8752
|
-
const gsdSrc = _stageSkills(_commandsDir);
|
|
8753
|
-
copyFlattenedCommands(gsdSrc, commandDir, 'gsd', pathPrefix, runtime);
|
|
8754
|
-
if (verifyInstalled(commandDir, 'command/gsd-*')) {
|
|
8755
|
-
const count = fs.readdirSync(commandDir).filter(f => f.startsWith('gsd-')).length;
|
|
8756
|
-
console.log(` ${green}✓${reset} Installed ${count} commands to command/`);
|
|
8757
|
-
} else {
|
|
8758
|
-
failures.push('command/gsd-*');
|
|
8759
|
-
}
|
|
8760
|
-
|
|
8761
|
-
// Also emit OpenCode-family skills (skills/<name>/SKILL.md). OpenCode and
|
|
8762
|
-
// Kilo support native, on-demand skills in addition to flat commands — see
|
|
8763
|
-
// resolveRuntimeArtifactLayout's opencode/kilo entries. Derive skills from
|
|
8764
|
-
// the SAME staged command set (gsdSrc) so both surfaces match exactly. (#784)
|
|
8765
|
-
const _skillCount = installOpencodeFamilySkills(runtime, targetDir, gsdSrc, pathPrefix, getCommitAttribution);
|
|
8766
|
-
if (_skillCount > 0) {
|
|
8767
|
-
console.log(` ${green}✓${reset} Installed ${_skillCount} skills to skills/`);
|
|
8768
|
-
} else {
|
|
8769
|
-
failures.push('skills/gsd-*');
|
|
8770
|
-
}
|
|
8771
|
-
|
|
8772
|
-
// OpenCode-only: install the native plugin adapter (#1914). OpenCode
|
|
8773
|
-
// declares hooksSurface: 'none', so GSD's lifecycle hooks are never
|
|
8774
|
-
// registered as settings.json hooks the way Claude Code does — the hook
|
|
8775
|
-
// *scripts* ship to <configDir>/hooks/ but nothing invokes them. This
|
|
8776
|
-
// plugin bridges OpenCode's event bus onto those existing hook scripts
|
|
8777
|
-
// (prompt guard, read guard, injection scanner, context monitor, ...),
|
|
8778
|
-
// spawning them as subprocesses. OpenCode auto-discovers plugin files under
|
|
8779
|
-
// <configDir>/plugins/ at startup — no opencode.json registration needed
|
|
8780
|
-
// (its `plugin` array is for npm packages, not local file paths).
|
|
8781
|
-
//
|
|
8782
|
-
// The file MUST land as `.js`: OpenCode's loader globs
|
|
8783
|
-
// `{plugin,plugins}/*.{ts,js}` (verified against its source) — a `.cjs`
|
|
8784
|
-
// extension would never be discovered. The config dir carries a
|
|
8785
|
-
// `{"type":"commonjs"}` package.json (written above), so the `.js` file is
|
|
8786
|
-
// interpreted as CommonJS, matching the adapter's module.exports/require.
|
|
8787
|
-
// Kilo has no plugin surface, so this is gated to OpenCode only.
|
|
8788
|
-
if (isOpencode) {
|
|
8789
|
-
const pluginSrc = path.join(src, '.opencode', 'plugins', 'gsd-core.js');
|
|
8790
|
-
const pluginDestDir = path.join(targetDir, 'plugins');
|
|
8791
|
-
const pluginDest = path.join(pluginDestDir, 'gsd-core.js');
|
|
8792
|
-
if (fs.existsSync(pluginSrc)) {
|
|
8793
|
-
fs.mkdirSync(pluginDestDir, { recursive: true });
|
|
8794
|
-
fs.copyFileSync(pluginSrc, pluginDest);
|
|
8795
|
-
if (fs.existsSync(pluginDest)) {
|
|
8796
|
-
console.log(` ${green}✓${reset} Installed OpenCode plugin (bridges GSD hooks)`);
|
|
8797
|
-
} else {
|
|
8798
|
-
failures.push('plugins/gsd-core.js');
|
|
8799
|
-
}
|
|
8800
|
-
} else {
|
|
8801
|
-
failures.push('plugins/gsd-core.js');
|
|
8802
|
-
}
|
|
8803
|
-
}
|
|
8804
|
-
} else if (isCline) {
|
|
8944
|
+
} else if (_hostBehaviors(runtime).localCommandsViaRules) {
|
|
8805
8945
|
// Cline local install: rules-based only — commands are embedded in .clinerules (generated below).
|
|
8806
8946
|
// No skills/commands directory needed for local installs.
|
|
8807
8947
|
// Global installs are handled above by _isSkillsRuntime (#782).
|
|
8948
|
+
// Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
|
|
8949
|
+
// hostBehaviors.localCommandsViaRules.
|
|
8808
8950
|
console.log(` ${green}✓${reset} Cline: commands will be available via .clinerules`);
|
|
8809
8951
|
} else {
|
|
8810
8952
|
// Claude Code local: flat gsd-<cmd>.md layout — Claude Code registers
|
|
@@ -8902,11 +9044,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8902
9044
|
// other runtime/scope deploys commands/gsd, so its walk-up already resolves
|
|
8903
9045
|
// and needs no marker. Guarded on source presence so a half-published
|
|
8904
9046
|
// package never writes a dangling marker.
|
|
8905
|
-
if (runtime
|
|
9047
|
+
if (_hostBehaviors(runtime).sourceMarkerFile && isGlobal) {
|
|
8906
9048
|
const gsdSourceCommands = path.join(src, 'commands', 'gsd');
|
|
8907
9049
|
if (fs.existsSync(gsdSourceCommands)) {
|
|
8908
9050
|
try {
|
|
8909
|
-
|
|
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');
|
|
8910
9055
|
} catch (err) {
|
|
8911
9056
|
// Non-fatal: install proceeds. But on the Claude-global layout walk-up
|
|
8912
9057
|
// also fails (no commands/gsd source tree), so a silent write failure
|
|
@@ -8978,9 +9123,11 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8978
9123
|
// (by installRuntimeArtifacts at line 8912), which also performs its own
|
|
8979
9124
|
// stale-file prune pass. The inline stale-removal + inline loop both skip them.
|
|
8980
9125
|
// Trivial group (cursor/windsurf/augment/trae/codebuddy) cut over together.
|
|
8981
|
-
//
|
|
8982
|
-
//
|
|
8983
|
-
|
|
9126
|
+
// #1575: copilot and antigravity cut over — copilot gets .agent.md filename
|
|
9127
|
+
// rename via _copyStaged(runtime); antigravity uses scope-aware converter.
|
|
9128
|
+
// cline remains excluded: rules-only local branch + local/global complication
|
|
9129
|
+
// that the descriptor-driven path does not handle correctly.
|
|
9130
|
+
const _DESCRIPTOR_AGENTS_RUNTIMES = new Set(['cursor', 'windsurf', 'augment', 'trae', 'codebuddy', 'copilot', 'antigravity']);
|
|
8984
9131
|
|
|
8985
9132
|
// Always remove stale gsd-* agents first so re-installing with
|
|
8986
9133
|
// `--minimal` actually shrinks a previously-full install.
|
|
@@ -8991,7 +9138,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8991
9138
|
for (const file of fs.readdirSync(agentsDest)) {
|
|
8992
9139
|
if (
|
|
8993
9140
|
file.startsWith('gsd-') &&
|
|
8994
|
-
(file.endsWith('.md') || (
|
|
9141
|
+
(file.endsWith('.md') || (_hostBehaviors(runtime).agentTomlFiles && file.endsWith('.toml')))
|
|
8995
9142
|
) {
|
|
8996
9143
|
fs.unlinkSync(path.join(agentsDest, file));
|
|
8997
9144
|
}
|
|
@@ -9009,7 +9156,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9009
9156
|
// Without stripping them here, a full → minimal reinstall would leave the
|
|
9010
9157
|
// runtime advertising the old full agent surface even though the agent
|
|
9011
9158
|
// files are gone. Reuse the same helper that powers `--uninstall`.
|
|
9012
|
-
if (
|
|
9159
|
+
if (_hostBehaviors(runtime).tomlConfigInstall) {
|
|
9013
9160
|
const codexConfigPath = path.join(targetDir, 'config.toml');
|
|
9014
9161
|
if (fs.existsSync(codexConfigPath)) {
|
|
9015
9162
|
const existing = fs.readFileSync(codexConfigPath, 'utf8');
|
|
@@ -9044,7 +9191,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9044
9191
|
}
|
|
9045
9192
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
9046
9193
|
// Convert frontmatter for runtime compatibility (agents need different handling)
|
|
9047
|
-
if (
|
|
9194
|
+
if (_hostBehaviors(runtime).frontmatterDialect === 'opencode') {
|
|
9048
9195
|
// Resolve per-agent model for OpenCode agents.
|
|
9049
9196
|
// Precedence: model_overrides[agent] > model_profile_overrides.opencode.<tier> > omit.
|
|
9050
9197
|
// model_overrides (#2256): explicit per-agent override, highest precedence.
|
|
@@ -9063,16 +9210,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9063
9210
|
}
|
|
9064
9211
|
}
|
|
9065
9212
|
content = convertClaudeToOpencodeFrontmatter(content, { isAgent: true, modelOverride: _ocModelOverride });
|
|
9066
|
-
} else if (
|
|
9213
|
+
} else if (_hostBehaviors(runtime).frontmatterDialect === 'kilo') {
|
|
9067
9214
|
content = convertClaudeToKiloFrontmatter(content, { isAgent: true });
|
|
9068
|
-
} else if (
|
|
9215
|
+
} else if (_hostBehaviors(runtime).frontmatterDialect === 'codex') {
|
|
9069
9216
|
content = convertClaudeAgentToCodexAgent(content);
|
|
9070
9217
|
} else if (isCopilot) {
|
|
9071
9218
|
content = convertClaudeAgentToCopilotAgent(content, isGlobal);
|
|
9072
9219
|
} else if (isAntigravity) {
|
|
9073
9220
|
content = convertClaudeAgentToAntigravityAgent(content, isGlobal);
|
|
9074
|
-
} else if (isCursor) {
|
|
9075
|
-
content = convertClaudeAgentToCursorAgent(content);
|
|
9076
9221
|
} else if (isWindsurf) {
|
|
9077
9222
|
content = convertClaudeAgentToWindsurfAgent(content);
|
|
9078
9223
|
} else if (isAugment) {
|
|
@@ -9081,13 +9226,15 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9081
9226
|
content = convertClaudeAgentToTraeAgent(content);
|
|
9082
9227
|
} else if (isCodebuddy) {
|
|
9083
9228
|
content = convertClaudeAgentToCodebuddyAgent(content);
|
|
9084
|
-
} else if (
|
|
9229
|
+
} else if (_hostBehaviors(runtime).frontmatterDialect === 'cline') {
|
|
9230
|
+
// Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
|
|
9231
|
+
// hostBehaviors.frontmatterDialect === 'cline'.
|
|
9085
9232
|
content = convertClaudeAgentToClineAgent(content);
|
|
9086
9233
|
} else if (isQwen) {
|
|
9087
9234
|
content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
|
|
9088
9235
|
content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
|
|
9089
9236
|
content = content.replace(/\.claude\//g, '.qwen/');
|
|
9090
|
-
} else if (
|
|
9237
|
+
} else if (_hostBehaviors(runtime).brandingRewrites) {
|
|
9091
9238
|
content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
|
|
9092
9239
|
content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
|
|
9093
9240
|
content = content.replace(/\.claude\//g, '.hermes/');
|
|
@@ -9099,11 +9246,11 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9099
9246
|
// Claude Code reads per-subagent `effort:` frontmatter (anthropics/claude-code #31536).
|
|
9100
9247
|
// Injection is per-runtime at install time because the canonical source
|
|
9101
9248
|
// agents/*.md must stay runtime-safe (no effort: key in source).
|
|
9102
|
-
if (runtime
|
|
9249
|
+
if ((_hostBehaviors(runtime).agentFrontmatterExtensions || []).includes('effort')) {
|
|
9103
9250
|
const _effortCfg = readGsdEffectiveEffortConfig(targetDir);
|
|
9104
9251
|
const _agentName = entry.name.replace(/\.md$/, '');
|
|
9105
9252
|
const _universalEffort = resolveInstallTimeEffort(_effortCfg, _agentName);
|
|
9106
|
-
const _renderedEffort = _getGsdEffortCatalog().renderEffortForRuntime(
|
|
9253
|
+
const _renderedEffort = _getGsdEffortCatalog().renderEffortForRuntime(runtime, _universalEffort).value;
|
|
9107
9254
|
content = injectEffortFrontmatter(content, _renderedEffort);
|
|
9108
9255
|
const _disallowedTools = READONLY_AGENT_DISALLOWED_TOOLS[_agentName];
|
|
9109
9256
|
if (_disallowedTools) content = injectDisallowedToolsFrontmatter(content, _disallowedTools);
|
|
@@ -9147,7 +9294,17 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9147
9294
|
failures.push('VERSION');
|
|
9148
9295
|
}
|
|
9149
9296
|
|
|
9150
|
-
|
|
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) {
|
|
9151
9308
|
// Write package.json to force CommonJS mode for GSD scripts
|
|
9152
9309
|
// Prevents "require is not defined" errors when project has "type": "module"
|
|
9153
9310
|
// Node.js walks up looking for package.json - this stops inheritance from project
|
|
@@ -9176,7 +9333,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9176
9333
|
content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
|
|
9177
9334
|
content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
|
|
9178
9335
|
}
|
|
9179
|
-
if (
|
|
9336
|
+
if (_hostBehaviors(runtime).brandingRewrites) {
|
|
9180
9337
|
content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
|
|
9181
9338
|
content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
|
|
9182
9339
|
}
|
|
@@ -9238,12 +9395,16 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9238
9395
|
|
|
9239
9396
|
// Gate hooks/lib/ install on the same runtimes that receive hooks (see line ~8702).
|
|
9240
9397
|
// Codex/Copilot/Cursor/Windsurf/Trae/Cline do not use the shared hooks/lib/ helpers
|
|
9241
|
-
// (Cursor uses standalone .js hook scripts registered via hooks.json
|
|
9242
|
-
//
|
|
9243
|
-
//
|
|
9244
|
-
//
|
|
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.
|
|
9245
9406
|
const hooksLibSrc = path.join(src, 'hooks', 'lib');
|
|
9246
|
-
if (!isCodex && !isCopilot &&
|
|
9407
|
+
if (!isCodex && !isCopilot && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf && !isTrae && !isKimi && !isKilo && !isZcode && fs.existsSync(hooksLibSrc)) {
|
|
9247
9408
|
const hooksLibDest = path.join(targetDir, 'hooks', 'lib');
|
|
9248
9409
|
fs.mkdirSync(hooksLibDest, { recursive: true });
|
|
9249
9410
|
copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES);
|
|
@@ -9370,14 +9531,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9370
9531
|
}
|
|
9371
9532
|
|
|
9372
9533
|
// Write file manifest for future modification detection
|
|
9373
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode });
|
|
9534
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' });
|
|
9374
9535
|
console.log(` ${green}✓${reset} Wrote file manifest (${MANIFEST_NAME})`);
|
|
9375
9536
|
|
|
9376
9537
|
// Report any backed-up local patches
|
|
9377
9538
|
reportLocalPatches(targetDir, runtime);
|
|
9378
9539
|
|
|
9379
9540
|
// Verify no leaked .claude paths in non-Claude runtimes (manifest-scoped)
|
|
9380
|
-
if (runtime
|
|
9541
|
+
if (!_hostBehaviors(runtime).ownsClaudePaths) {
|
|
9381
9542
|
const leakedPaths = [];
|
|
9382
9543
|
// Only scan files that were written by this install (manifest-tracked).
|
|
9383
9544
|
// Scanning the entire targetDir can match user-authored content that
|
|
@@ -9513,7 +9674,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9513
9674
|
// (copyCommandsAsCodexSkills removes pre-existing gsd-* dirs before re-writing)
|
|
9514
9675
|
// are restored even when they are absent from disk at rollback time (#3245 CR).
|
|
9515
9676
|
// • Dirs that did not pre-exist: remove entirely.
|
|
9516
|
-
const _rollbackSkillsDir =
|
|
9677
|
+
const _rollbackSkillsDir = _resolveSkillsRootDir(runtime, targetDir, isGlobal ? 'global' : 'local');
|
|
9517
9678
|
// Pass 1 — restore snapshot entries (may be absent from disk if deleted mid-install).
|
|
9518
9679
|
for (const skillName of codexPreInstallSkillNames) {
|
|
9519
9680
|
const skillDirPath = path.join(_rollbackSkillsDir, skillName);
|
|
@@ -9624,7 +9785,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9624
9785
|
// Re-write the manifest now that .toml agent files exist on disk.
|
|
9625
9786
|
// The initial writeManifest call (before Codex config generation) could
|
|
9626
9787
|
// not include agents/gsd-*.toml because those files did not yet exist.
|
|
9627
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode });
|
|
9788
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' });
|
|
9628
9789
|
} else {
|
|
9629
9790
|
console.log(` ${dim}↳${reset} Skipping Codex agent config generation (minimal install)`);
|
|
9630
9791
|
}
|
|
@@ -9764,24 +9925,23 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9764
9925
|
}
|
|
9765
9926
|
}
|
|
9766
9927
|
|
|
9767
|
-
// ── Codex extended hook events (#772)
|
|
9768
|
-
// Codex CLI stabilised a full hook-event set in rust-v0.137.0.
|
|
9769
|
-
//
|
|
9770
|
-
//
|
|
9771
|
-
//
|
|
9772
|
-
//
|
|
9773
|
-
//
|
|
9774
|
-
//
|
|
9775
|
-
//
|
|
9776
|
-
//
|
|
9777
|
-
//
|
|
9778
|
-
// 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.
|
|
9779
9939
|
//
|
|
9780
9940
|
// Guard: only register when the context-monitor file exists and the node
|
|
9781
9941
|
// runner is available — same guards as the SessionStart path above.
|
|
9782
9942
|
const contextMonitorFile = path.join(targetDir, 'hooks', 'gsd-context-monitor.js');
|
|
9783
9943
|
if (codexNodeRunner && fs.existsSync(contextMonitorFile)) {
|
|
9784
|
-
for (const codexEvent of
|
|
9944
|
+
for (const codexEvent of CODEX_EXTENDED_HOOK_EVENTS) {
|
|
9785
9945
|
const eventWrite = ensureCodexHooksJsonEvent(targetDir, codexEvent, {
|
|
9786
9946
|
absoluteRunner: codexNodeRunner,
|
|
9787
9947
|
platform: process.platform,
|
|
@@ -9793,7 +9953,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9793
9953
|
}
|
|
9794
9954
|
}
|
|
9795
9955
|
} else if (!codexNodeRunner) {
|
|
9796
|
-
console.warn(` ${yellow}⚠${reset} Skipped Codex
|
|
9956
|
+
console.warn(` ${yellow}⚠${reset} Skipped Codex extended hook-event registration — Node runner unavailable.`);
|
|
9797
9957
|
}
|
|
9798
9958
|
// ── end Codex extended hook events ────────────────────────────────────
|
|
9799
9959
|
}
|
|
@@ -9858,16 +10018,20 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9858
10018
|
}
|
|
9859
10019
|
|
|
9860
10020
|
if (plan.installSurface === 'cursor-hooks-json') {
|
|
9861
|
-
// #
|
|
9862
|
-
//
|
|
9863
|
-
|
|
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
|
+
});
|
|
9864
10028
|
if (cursorHookResult.changed) {
|
|
9865
|
-
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)`);
|
|
9866
10030
|
} else {
|
|
9867
10031
|
console.log(` ${green}✓${reset} Cursor lifecycle hooks already up to date`);
|
|
9868
10032
|
}
|
|
9869
10033
|
// Re-run the manifest pass so the hook scripts + hooks.json are hash-tracked.
|
|
9870
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode });
|
|
10034
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' });
|
|
9871
10035
|
persistActiveProfileMarker();
|
|
9872
10036
|
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
9873
10037
|
}
|
|
@@ -9885,7 +10049,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9885
10049
|
writeClineArtifacts(targetDir, isGlobal);
|
|
9886
10050
|
// Re-run the manifest pass: these artifacts are written *after* the earlier
|
|
9887
10051
|
// writeManifest() call, so a second pass is needed to hash-track them.
|
|
9888
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode });
|
|
10052
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' });
|
|
9889
10053
|
persistActiveProfileMarker();
|
|
9890
10054
|
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
9891
10055
|
}
|
|
@@ -9900,9 +10064,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9900
10064
|
// #338: local Claude installs write to settings.local.json (Claude Code's per-user/gitignored slot)
|
|
9901
10065
|
// so engineer-specific absolute paths (Node binary, home dir) never land in the repo-shared
|
|
9902
10066
|
// settings.json. Global installs and all other runtimes continue to use settings.json.
|
|
9903
|
-
const
|
|
9904
|
-
const
|
|
9905
|
-
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);
|
|
9906
10075
|
|
|
9907
10076
|
// #338 migration: if a prior local Claude install wrote GSD-shaped entries to settings.json,
|
|
9908
10077
|
// relocate them to settings.local.json and clear them from the shared file in the same run.
|
|
@@ -10085,7 +10254,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
10085
10254
|
// installAllRuntimes can register it at finalize time when the user opts
|
|
10086
10255
|
// in (#2795). Computed here (not in finishInstall) so the same buildHookCommand
|
|
10087
10256
|
// / localCmd resolution logic is shared with the other JS hooks.
|
|
10088
|
-
const updateBannerCommand =
|
|
10257
|
+
const updateBannerCommand = _hostBehaviors(runtime).skipUpdateBannerCommand
|
|
10089
10258
|
? null
|
|
10090
10259
|
: (isGlobal
|
|
10091
10260
|
? buildHookCommand(targetDir, 'gsd-update-banner.js', hookOpts)
|
|
@@ -10168,11 +10337,11 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
10168
10337
|
/**
|
|
10169
10338
|
* Apply statusline config, then print completion message
|
|
10170
10339
|
*/
|
|
10171
|
-
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime =
|
|
10340
|
+
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = DEFAULT_RUNTIME, isGlobal = true, configDir = null, bannerOpts = {}) {
|
|
10172
10341
|
const { isOpencode, isKilo, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
10173
10342
|
const plan = resolveInstallPlan(runtime);
|
|
10174
10343
|
|
|
10175
|
-
if (shouldInstallStatusline && plan.writesSharedSettings && !
|
|
10344
|
+
if (shouldInstallStatusline && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {
|
|
10176
10345
|
if (!isGlobal && !forceStatusline) {
|
|
10177
10346
|
// Local installs skip statusLine by default: repo settings.json takes precedence over
|
|
10178
10347
|
// profile-level settings.json in Claude Code, so writing here would silently clobber
|
|
@@ -10198,7 +10367,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
10198
10367
|
// settings.json hooks block — opencode/kilo/codex/cursor/windsurf/trae/
|
|
10199
10368
|
// cline either lack the surface or use a different config schema.
|
|
10200
10369
|
const { shouldInstallBanner, bannerCommand } = bannerOpts;
|
|
10201
|
-
if (shouldInstallBanner && settings && plan.writesSharedSettings && !
|
|
10370
|
+
if (shouldInstallBanner && settings && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {
|
|
10202
10371
|
if (!bannerCommand) {
|
|
10203
10372
|
console.warn(` ${yellow}⚠${reset} Skipped update banner registration — Node executable path unavailable. See #2979 / #3002.`);
|
|
10204
10373
|
} else {
|
|
@@ -10227,7 +10396,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
10227
10396
|
// Merges GSD-owned entries non-destructively (preserves existing user permissions).
|
|
10228
10397
|
// Scoped to Claude only: antigravity/qwen/hermes/codebuddy also write
|
|
10229
10398
|
// settings.json but use different runtimes and do not use these permission strings.
|
|
10230
|
-
if (runtime === 'claude') {
|
|
10399
|
+
if (_hostBehaviors(runtime).permissionsSchema === 'claude') {
|
|
10231
10400
|
mergeClaudePermissions(settings);
|
|
10232
10401
|
}
|
|
10233
10402
|
|
|
@@ -10260,7 +10429,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
10260
10429
|
// chat model instead of pinning the resolved model. See #1156 (default-to-omit
|
|
10261
10430
|
// intent) and #1569 (preserve explicit true). Guard matches the #130-class pattern
|
|
10262
10431
|
// on configureOpencodePermissions above.
|
|
10263
|
-
if (runtime
|
|
10432
|
+
if (!_hostBehaviors(runtime).nativeModelAliases && !process.env.GSD_TEST_MODE) {
|
|
10264
10433
|
const gsdDir = path.join(os.homedir(), '.gsd');
|
|
10265
10434
|
const defaultsPath = path.join(gsdDir, 'defaults.json');
|
|
10266
10435
|
try {
|
|
@@ -10302,7 +10471,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
10302
10471
|
// Restart is required for CC to pick up newly-installed skills, and the
|
|
10303
10472
|
// slash-menu surface depends on CC version — so the instruction needs to
|
|
10304
10473
|
// cover both invocation paths to avoid #2957-style "no commands appear".
|
|
10305
|
-
if (runtime
|
|
10474
|
+
if (_hostBehaviors(runtime).skillsGlobalOnboarding && isGlobal) {
|
|
10306
10475
|
console.log(`
|
|
10307
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.
|
|
10308
10477
|
|
|
@@ -10401,10 +10570,11 @@ const runtimeMap = {
|
|
|
10401
10570
|
'12': 'opencode',
|
|
10402
10571
|
'13': 'qwen',
|
|
10403
10572
|
'14': 'trae',
|
|
10404
|
-
'15': 'windsurf'
|
|
10573
|
+
'15': 'windsurf',
|
|
10574
|
+
'16': 'zcode'
|
|
10405
10575
|
};
|
|
10406
|
-
const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'hermes', 'kimi', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf'];
|
|
10407
|
-
const ALL_RUNTIMES_OPTION = '
|
|
10576
|
+
const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'hermes', 'kimi', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf', 'zcode'];
|
|
10577
|
+
const ALL_RUNTIMES_OPTION = '17';
|
|
10408
10578
|
|
|
10409
10579
|
/**
|
|
10410
10580
|
* Build the runtime-selection prompt text shown by the interactive installer.
|
|
@@ -10427,7 +10597,8 @@ function buildRuntimePromptText() {
|
|
|
10427
10597
|
${cyan}13${reset}) Qwen Code ${dim}(~/.qwen)${reset}
|
|
10428
10598
|
${cyan}14${reset}) Trae ${dim}(~/.trae)${reset}
|
|
10429
10599
|
${cyan}15${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset}
|
|
10430
|
-
${cyan}16${reset})
|
|
10600
|
+
${cyan}16${reset}) ZCode ${dim}(~/.zcode)${reset}
|
|
10601
|
+
${cyan}17${reset}) All
|
|
10431
10602
|
|
|
10432
10603
|
${dim}Select multiple: 1,2,6 or 1 2 6${reset}
|
|
10433
10604
|
`;
|
|
@@ -10459,7 +10630,7 @@ function parseRuntimeInput(answer) {
|
|
|
10459
10630
|
}
|
|
10460
10631
|
}
|
|
10461
10632
|
|
|
10462
|
-
return selected.length > 0 ? selected : [
|
|
10633
|
+
return selected.length > 0 ? selected : [DEFAULT_RUNTIME];
|
|
10463
10634
|
}
|
|
10464
10635
|
|
|
10465
10636
|
function promptRuntime(callback) {
|
|
@@ -11078,7 +11249,7 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
11078
11249
|
throw error;
|
|
11079
11250
|
}
|
|
11080
11251
|
|
|
11081
|
-
const statuslineRuntimes = [
|
|
11252
|
+
const statuslineRuntimes = [DEFAULT_RUNTIME];
|
|
11082
11253
|
const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime));
|
|
11083
11254
|
|
|
11084
11255
|
const finalize = (shouldInstallStatusline, shouldInstallBanner) => {
|
|
@@ -11175,6 +11346,13 @@ module.exports = {
|
|
|
11175
11346
|
generateCodexAgentToml,
|
|
11176
11347
|
cleanupCodexSkillMetadataSidecars,
|
|
11177
11348
|
cleanupWindsurfLegacyDevinSkills,
|
|
11349
|
+
cleanupMovedSkillsOldLocation,
|
|
11350
|
+
_resolveMovedSkillsOldDir,
|
|
11351
|
+
_resolveSkillsRootDir,
|
|
11352
|
+
codexBareAgentsHasOnlyKnownScalars,
|
|
11353
|
+
extractCodexUserAgentsScalars,
|
|
11354
|
+
spliceCodexAgentsScalars,
|
|
11355
|
+
CODEX_EXTENDED_HOOK_EVENTS,
|
|
11178
11356
|
generateCodexConfigBlock,
|
|
11179
11357
|
stripGsdFromCodexConfig,
|
|
11180
11358
|
migrateCodexHooksMapFormat,
|
|
@@ -11194,6 +11372,9 @@ module.exports = {
|
|
|
11194
11372
|
install,
|
|
11195
11373
|
installAllRuntimes,
|
|
11196
11374
|
uninstall,
|
|
11375
|
+
// #2086 — host-behavior resolution + the #338 privacy fail-safe floor (exported for tests)
|
|
11376
|
+
_resolveHostBehaviors,
|
|
11377
|
+
FALLBACK_HOST_BEHAVIORS,
|
|
11197
11378
|
convertSlashCommandsToCodexSkillMentions,
|
|
11198
11379
|
convertClaudeCommandToCodexSkill,
|
|
11199
11380
|
convertClaudeCommandToKimiSkill,
|
|
@@ -11258,6 +11439,11 @@ module.exports = {
|
|
|
11258
11439
|
mergeGsdAgentsMd,
|
|
11259
11440
|
GSD_CURSOR_SESSION_HOOK_SCRIPT,
|
|
11260
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,
|
|
11261
11447
|
GSD_CURSOR_HOOK_MARKER,
|
|
11262
11448
|
buildCursorHookEntry,
|
|
11263
11449
|
isManagedCursorHookEntry,
|
|
@@ -11362,7 +11548,7 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
|
|
|
11362
11548
|
console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
|
|
11363
11549
|
process.exit(1);
|
|
11364
11550
|
}
|
|
11365
|
-
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : [
|
|
11551
|
+
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : [DEFAULT_RUNTIME];
|
|
11366
11552
|
for (const runtime of runtimes) {
|
|
11367
11553
|
uninstall(hasGlobal, runtime);
|
|
11368
11554
|
}
|
|
@@ -11374,12 +11560,12 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
|
|
|
11374
11560
|
}
|
|
11375
11561
|
} else if (hasGlobal || hasLocal) {
|
|
11376
11562
|
// Default to Claude if no runtime specified but location is
|
|
11377
|
-
installAllRuntimes([
|
|
11563
|
+
installAllRuntimes([DEFAULT_RUNTIME], hasGlobal, false);
|
|
11378
11564
|
} else {
|
|
11379
11565
|
// Interactive
|
|
11380
11566
|
if (!process.stdin.isTTY) {
|
|
11381
11567
|
console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
|
|
11382
|
-
installAllRuntimes([
|
|
11568
|
+
installAllRuntimes([DEFAULT_RUNTIME], true, false);
|
|
11383
11569
|
} else {
|
|
11384
11570
|
promptRuntime((runtimes) => {
|
|
11385
11571
|
promptLocation(runtimes);
|