@opengsd/gsd-core 1.7.0-rc.4 → 1.7.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +20 -0
- package/agents/gsd-doc-classifier.md +105 -0
- package/agents/gsd-doc-synthesizer.md +61 -0
- package/agents/gsd-ui-checker.md +30 -0
- package/agents/gsd-ui-researcher.md +1 -0
- package/bin/install.js +1568 -622
- package/gsd-core/bin/gsd-tools.cjs +40 -1
- package/gsd-core/bin/lib/api-coverage.cjs +466 -0
- package/gsd-core/bin/lib/audit.cjs +6 -3
- package/gsd-core/bin/lib/capability-loader.cjs +11 -9
- package/gsd-core/bin/lib/capability-registry.cjs +761 -84
- package/gsd-core/bin/lib/capability-validator.cjs +56 -18
- package/gsd-core/bin/lib/capability-writer.cjs +10 -1
- package/gsd-core/bin/lib/check-command-router.cjs +242 -3
- package/gsd-core/bin/lib/commands.cjs +7 -5
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +96 -0
- package/gsd-core/bin/lib/core-utils.cjs +4 -1
- package/gsd-core/bin/lib/host-integration-adapters/cline-sdk-binding.cjs +234 -0
- package/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs +145 -0
- package/gsd-core/bin/lib/host-integration.cjs +45 -4
- package/gsd-core/bin/lib/init.cjs +76 -39
- package/gsd-core/bin/lib/install-effort-resolver.cjs +213 -0
- package/gsd-core/bin/lib/install-engine.cjs +228 -18
- package/gsd-core/bin/lib/installer-migration-report.cjs +7 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +68 -17
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +50 -11
- package/gsd-core/bin/lib/mcp-server.cjs +18 -7
- package/gsd-core/bin/lib/milestone.cjs +3 -3
- package/gsd-core/bin/lib/normalize-test-command.cjs +187 -0
- package/gsd-core/bin/lib/phase-id.cjs +132 -3
- package/gsd-core/bin/lib/phase.cjs +78 -16
- package/gsd-core/bin/lib/planning-workspace.cjs +17 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
- 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 +248 -44
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +39 -23
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +30 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +534 -37
- package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
- package/gsd-core/bin/lib/security.cjs +6 -36
- package/gsd-core/bin/lib/shell-command-projection.cjs +115 -2
- package/gsd-core/bin/lib/spec-section.cjs +111 -0
- package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
- 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 +40 -6
- package/gsd-core/bin/lib/uat.cjs +4 -1
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
- 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 +14 -9
- package/gsd-core/references/api-coverage.md +104 -0
- package/gsd-core/references/model-profiles.md +2 -2
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/specless-probe-fallback.md +172 -0
- package/gsd-core/references/ui-consideration-probe.md +73 -0
- package/gsd-core/templates/UI-SPEC.md +25 -0
- package/gsd-core/templates/VALIDATION.md +2 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/workflows/audit-fix.md +9 -1
- package/gsd-core/workflows/audit-milestone.md +7 -4
- package/gsd-core/workflows/code-review-fix.md +7 -3
- package/gsd-core/workflows/code-review.md +4 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +8 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +42 -0
- package/gsd-core/workflows/execute-phase.md +1 -25
- package/gsd-core/workflows/plan-phase.md +37 -2
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/review.md +59 -13
- package/gsd-core/workflows/settings-advanced.md +12 -9
- package/gsd-core/workflows/settings.md +2 -2
- package/gsd-core/workflows/ui-phase.md +146 -1
- package/gsd-core/workflows/validate-phase.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/gsd-windsurf-pre-command.js +275 -0
- package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
- package/hooks/dist/managed-hooks-registry.cjs +6 -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/gsd-windsurf-pre-command.js +275 -0
- package/hooks/gsd-windsurf-pre-write.js +132 -0
- package/hooks/managed-hooks-registry.cjs +6 -0
- package/package.json +9 -4
- package/pi/gsd.cjs +354 -0
- package/scripts/build-hooks.js +8 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +11 -1
- package/scripts/gen-registry.cjs +128 -0
- package/scripts/lint-phase-id-drift.cjs +150 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/registry-schema.cjs +565 -0
- package/scripts/run-tests.cjs +21 -1
- package/scripts/validate-registry.cjs +117 -0
- package/vscode/browser.js +197 -0
- package/vscode/extension.js +383 -0
- package/vscode/host-binding.js +113 -0
- package/vscode/package.json +96 -0
package/bin/install.js
CHANGED
|
@@ -32,6 +32,7 @@ const {
|
|
|
32
32
|
resolveAntigravityGlobalDir,
|
|
33
33
|
getGlobalConfigDir,
|
|
34
34
|
getGlobalSkillsBase,
|
|
35
|
+
resolveKimiHooksTomlDir,
|
|
35
36
|
} = require('../gsd-core/bin/lib/runtime-homes.cjs');
|
|
36
37
|
// getDirName (runtime -> local config dir name) is relocated out of this
|
|
37
38
|
// installer to the runtime-name-policy leaf (ADR-1508 / #1510 Phase 1) so the
|
|
@@ -43,6 +44,7 @@ const {
|
|
|
43
44
|
readBaseRefFromSettings,
|
|
44
45
|
} = require('../gsd-core/bin/lib/worktree-base-ref.cjs');
|
|
45
46
|
const { resolveInstallPlan } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs');
|
|
47
|
+
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
|
|
46
48
|
const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
|
|
47
49
|
// Canonical set of hook files shipped to users. Imported here so writeManifest()
|
|
48
50
|
// records exactly the same set that build-hooks.js copies to hooks/dist/, making
|
|
@@ -57,25 +59,20 @@ const INSTALLED_HOOK_FILES = new Set(_HOOKS_TO_COPY);
|
|
|
57
59
|
const hooksSurface = require('../gsd-core/bin/lib/runtime-hooks-surface.cjs');
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
62
|
+
* #3677 predicate — true when an agent body needs `/gsd:<cmd>` → `/gsd-<cmd>`
|
|
63
|
+
* normalization at install time. Descriptor-driven
|
|
64
|
+
* (capabilities/<runtime>/capability.json -> runtime.hostBehaviors.hyphenNameAgentBody)
|
|
65
|
+
* instead of a hardcoded runtime allow-list (ADR-1239 / #2086). Sibling fixes
|
|
63
66
|
* #3583 / #3629 covered SKILL.md bodies, #3584 / #3606 covered runtime
|
|
64
67
|
* emissions — this is the agent-body surface (#3677).
|
|
65
68
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*/
|
|
70
|
-
const HYPHEN_NAME_AGENT_RUNTIMES = new Set(['claude', 'qwen', 'hermes']);
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* #3677 predicate — true when an agent body needs `/gsd:<cmd>` → `/gsd-<cmd>`
|
|
74
|
-
* normalization at install time.
|
|
69
|
+
* Unknown / future runtimes that don't declare the flag default to "no
|
|
70
|
+
* rewrite" (better to leak than to mangle a runtime whose namespace
|
|
71
|
+
* behavior we haven't verified).
|
|
75
72
|
*/
|
|
76
73
|
function shouldNormalizeHyphenNamespaceInAgentBody(runtime) {
|
|
77
74
|
if (typeof runtime !== 'string' || runtime === '') return false;
|
|
78
|
-
return
|
|
75
|
+
return _hostBehaviors(runtime).hyphenNameAgentBody === true;
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
/**
|
|
@@ -101,6 +98,45 @@ const reset = '\x1b[0m';
|
|
|
101
98
|
// Codex config.toml constants
|
|
102
99
|
const GSD_CODEX_MARKER = '# GSD Agent Configuration \u2014 managed by gsd-core installer';
|
|
103
100
|
const GSD_CODEX_HOOKS_OWNERSHIP_PREFIX = '# GSD codex_hooks ownership: ';
|
|
101
|
+
// Known scalar fields of Codex's `AgentsToml` struct (codex-rs/config/src/
|
|
102
|
+
// config_toml.rs \u2014 `[agents]` table). Codex marks the struct
|
|
103
|
+
// `#[schemars(deny_unknown_fields)]`, so a bare `[agents]` table is valid ONLY
|
|
104
|
+
// when every direct key is one of these (named agent roles live in the flattened
|
|
105
|
+
// `[agents.<name>]` sub-tables, a separate `AgentRoleToml`). GSD writes only
|
|
106
|
+
// `max_depth` (ADR-1239 upgrade 2 / #2088); the full set is enumerated so the
|
|
107
|
+
// schema check accepts a user's other legitimate AgentsToml scalars too.
|
|
108
|
+
const CODEX_AGENTS_TOML_SCALAR_KEYS = new Set([
|
|
109
|
+
'max_threads',
|
|
110
|
+
'max_depth',
|
|
111
|
+
'job_max_runtime_seconds',
|
|
112
|
+
'interrupt_message',
|
|
113
|
+
]);
|
|
114
|
+
// GSD's managed dispatch-depth value. Codex's implicit default is also 1 (root
|
|
115
|
+
// sessions start at depth 0); writing it EXPLICITLY pins the negotiated
|
|
116
|
+
// `dispatch.maxDepth: 1` axis instead of relying on codex-cli's implicit default
|
|
117
|
+
// (ADR-1239 upgrade 2 / #2088). Per the negotiated capability, GSD-hosted Codex
|
|
118
|
+
// dispatch is single-level (maxDepth === 1 \u2192 `degradationFor` flattens waves).
|
|
119
|
+
const GSD_CODEX_AGENTS_MAX_DEPTH = 1;
|
|
120
|
+
// Codex hooks.json lifecycle events GSD registers beyond SessionStart (which has
|
|
121
|
+
// its own dedicated path). This is Codex's OWN hook-event vocabulary (per
|
|
122
|
+
// developers.openai.com/codex/config-reference), distinct from the cross-runtime
|
|
123
|
+
// settings.json `extendedHookEvents` descriptor field (a claude/gemini-family
|
|
124
|
+
// allowlist consumed only by hooksSurface==='settings-json' runtimes — Codex is
|
|
125
|
+
// codex-hooks-json). All route through gsd-context-monitor.js. #772 wired the
|
|
126
|
+
// first three; #2088 adds the remaining six documented events so GSD's monitor
|
|
127
|
+
// fires at the same lifecycle points as in Claude Code. Install and uninstall
|
|
128
|
+
// share this list so the registered set and the removed set never diverge.
|
|
129
|
+
const CODEX_EXTENDED_HOOK_EVENTS = [
|
|
130
|
+
'SubagentStart',
|
|
131
|
+
'Stop',
|
|
132
|
+
'PostToolUse',
|
|
133
|
+
'PreToolUse',
|
|
134
|
+
'PermissionRequest',
|
|
135
|
+
'PreCompact',
|
|
136
|
+
'PostCompact',
|
|
137
|
+
'SubagentStop',
|
|
138
|
+
'UserPromptSubmit',
|
|
139
|
+
];
|
|
104
140
|
// Codex's hook-enabling feature flag (issue #3566). Codex itself marks
|
|
105
141
|
// `codex_hooks` as a `legacy_key` in codex-rs/features/src/legacy.rs; the
|
|
106
142
|
// canonical current key under [features] is `hooks`. The installer always
|
|
@@ -126,6 +162,9 @@ function isCodexHooksFeatureKey(key) {
|
|
|
126
162
|
//
|
|
127
163
|
// Merge policy: additive, non-destructive \u2014 existing user entries are preserved;
|
|
128
164
|
// GSD entries are appended only when not already present (idempotent).
|
|
165
|
+
// The reference/default runtime (ADR-1239 reference host). Single-sourced here
|
|
166
|
+
// instead of scattered literal 'claude' defaults/rosters (#2086).
|
|
167
|
+
const DEFAULT_RUNTIME = 'claude';
|
|
129
168
|
const GSD_CLAUDE_ALLOW_PERMISSIONS = Object.freeze([
|
|
130
169
|
'Bash(npx gsd-core *)',
|
|
131
170
|
'Read(.planning/*)',
|
|
@@ -212,15 +251,55 @@ const GSD_COPILOT_SESSION_HOOK_PWSH =
|
|
|
212
251
|
// Cursor reads hook configs from <project-root>/.cursor/hooks.json (local) or
|
|
213
252
|
// ~/.cursor/hooks.json (global) with the shape { version: 1, hooks: { <event>: [...] } }.
|
|
214
253
|
// Events use camelCase: sessionStart, postToolUse, preToolUse, etc.
|
|
215
|
-
// A `command` hook entry runs an external script. GSD registers
|
|
216
|
-
//
|
|
217
|
-
//
|
|
254
|
+
// A `command` hook entry runs an external script. GSD registers six managed hooks
|
|
255
|
+
// (AC4a upgrade, #2089 — ADR-1239):
|
|
256
|
+
// sessionStart → gsd-cursor-session-start.js (context injection)
|
|
257
|
+
// postToolUse → gsd-cursor-post-tool.js (STATE.md update monitor)
|
|
258
|
+
// preToolUse → gsd-cursor-pre-tool.js (write-path guard)
|
|
259
|
+
// stop → gsd-cursor-stop.js (verify-work reminder)
|
|
260
|
+
// subagentStart → gsd-cursor-subagent-start.js (subagent context injection)
|
|
261
|
+
// subagentStop → gsd-cursor-subagent-stop.js (subagent completion reminder)
|
|
218
262
|
// Cursor docs: https://cursor.com/docs/hooks
|
|
219
263
|
const GSD_CURSOR_SESSION_HOOK_SCRIPT = 'gsd-cursor-session-start.js';
|
|
220
264
|
const GSD_CURSOR_POST_TOOL_HOOK_SCRIPT = 'gsd-cursor-post-tool.js';
|
|
265
|
+
const GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT = 'gsd-cursor-pre-tool.js';
|
|
266
|
+
const GSD_CURSOR_STOP_HOOK_SCRIPT = 'gsd-cursor-stop.js';
|
|
267
|
+
const GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT = 'gsd-cursor-subagent-start.js';
|
|
268
|
+
const GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT = 'gsd-cursor-subagent-stop.js';
|
|
269
|
+
// All GSD-managed Cursor hook scripts (used by uninstall cleanup).
|
|
270
|
+
const GSD_CURSOR_HOOK_SCRIPTS = [
|
|
271
|
+
GSD_CURSOR_SESSION_HOOK_SCRIPT,
|
|
272
|
+
GSD_CURSOR_POST_TOOL_HOOK_SCRIPT,
|
|
273
|
+
GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT,
|
|
274
|
+
GSD_CURSOR_STOP_HOOK_SCRIPT,
|
|
275
|
+
GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT,
|
|
276
|
+
GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT,
|
|
277
|
+
];
|
|
221
278
|
// Marker comment embedded in managed hook entries so GSD can find+remove them.
|
|
222
279
|
const GSD_CURSOR_HOOK_MARKER = 'gsd-managed';
|
|
223
280
|
|
|
281
|
+
// #2100 Stage 2 — Windsurf/Cascade lifecycle hook constants.
|
|
282
|
+
// Windsurf/Cascade reads hook configs from <project-root>/.windsurf/hooks.json
|
|
283
|
+
// (local) or ~/.codeium/windsurf/hooks.json (global) with the shape
|
|
284
|
+
// { hooks: { <event>: [ { command, ... } ] } } — note: no top-level `version`
|
|
285
|
+
// field, and each entry carries a bare `command` shell string (no `type`
|
|
286
|
+
// field), unlike Cursor's hooks.json. GSD registers two managed BLOCKING
|
|
287
|
+
// hooks (exit code 2 to block, vs. Cursor's stdout-JSON form):
|
|
288
|
+
// pre_write_code → gsd-windsurf-pre-write.js (write-path guard)
|
|
289
|
+
// pre_run_command → gsd-windsurf-pre-command.js (destructive-command guard)
|
|
290
|
+
// Cascade has no context-injection channel, so the 4 advisory hooks GSD
|
|
291
|
+
// registers on Cursor (sessionStart, postToolUse, stop, subagentStart/Stop)
|
|
292
|
+
// have no Windsurf counterpart and are deliberately NOT ported.
|
|
293
|
+
// Cascade hooks docs (reference): https://docs.windsurf.com/llms-full.txt ,
|
|
294
|
+
// https://docs.devin.ai/desktop/cascade/hooks
|
|
295
|
+
const GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT = 'gsd-windsurf-pre-write.js';
|
|
296
|
+
const GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT = 'gsd-windsurf-pre-command.js';
|
|
297
|
+
// All GSD-managed Windsurf hook scripts (used by uninstall cleanup).
|
|
298
|
+
const GSD_WINDSURF_HOOK_SCRIPTS = [
|
|
299
|
+
GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT,
|
|
300
|
+
GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT,
|
|
301
|
+
];
|
|
302
|
+
|
|
224
303
|
// GSD-managed files under hooks/lib/ (helpers required by gsd-*.sh hooks).
|
|
225
304
|
// git-cmd.js does not start with "gsd-" (shared classifier for #3129), gsd-graphify-rebuild.sh does.
|
|
226
305
|
const GSD_HOOK_LIB_FILES = ['git-cmd.js', 'gsd-graphify-rebuild.sh'];
|
|
@@ -273,60 +352,20 @@ const {
|
|
|
273
352
|
} = require(path.join(_gsdLibDir, 'model-catalog.cjs'));
|
|
274
353
|
const {
|
|
275
354
|
resolveTierEntry: gsdResolveTierEntry,
|
|
276
|
-
EFFORT_SET: GSD_EFFORT_SET,
|
|
277
355
|
} = require(path.join(_gsdLibDir, 'model-resolver.cjs'));
|
|
278
356
|
|
|
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
|
-
}
|
|
357
|
+
// #2071 — install-time effort resolution (readGsdEffectiveEffortConfig /
|
|
358
|
+
// resolveInstallTimeEffort, plus their _getGsdEffortCatalog + _readGsdConfigFile
|
|
359
|
+
// helpers) was extracted into the shipped gsd-core/bin/lib/install-effort-resolver.cjs
|
|
360
|
+
// so `gsd-tools effort sync` can require it from the installed runtime instead of this
|
|
361
|
+
// package-root bin/install.js, which the installer never copies (#2071 crash). The
|
|
362
|
+
// installer imports it back here — single source of truth for both surfaces.
|
|
363
|
+
const {
|
|
364
|
+
readGsdEffectiveEffortConfig,
|
|
365
|
+
resolveInstallTimeEffort,
|
|
366
|
+
_getGsdEffortCatalog,
|
|
367
|
+
_readGsdConfigFile,
|
|
368
|
+
} = require(path.join(_gsdLibDir, 'install-effort-resolver.cjs'));
|
|
330
369
|
|
|
331
370
|
const {
|
|
332
371
|
MINIMAL_SKILL_ALLOWLIST,
|
|
@@ -350,6 +389,89 @@ try {
|
|
|
350
389
|
} catch (_) {
|
|
351
390
|
_capabilityRegistry = undefined;
|
|
352
391
|
}
|
|
392
|
+
|
|
393
|
+
// Fail-safe floor for the reference host's #338-privacy-critical behaviors, used
|
|
394
|
+
// ONLY when the first-party capability registry cannot be loaded (a broken bundle).
|
|
395
|
+
// Without it, a registry-load failure would make `_hostBehaviors('claude')` return
|
|
396
|
+
// {} and silently route a claude LOCAL install to the repo-shared, committed
|
|
397
|
+
// `settings.json` instead of the gitignored `settings.local.json` (#338) — leaking
|
|
398
|
+
// engineer-specific absolute paths. Keyed by runtime id (a DATA lookup, not a
|
|
399
|
+
// hardcoded string-equality branch) so behavior degrades CLOSED (safe), never open.
|
|
400
|
+
// The live descriptor (capabilities/claude/capability.json) remains the source of
|
|
401
|
+
// truth; this mirrors only the privacy-load-bearing subset. (ADR-1239 / #2086)
|
|
402
|
+
const FALLBACK_HOST_BEHAVIORS = Object.freeze({
|
|
403
|
+
claude: Object.freeze({
|
|
404
|
+
settingsFileByScope: Object.freeze({ local: 'settings.local.json', global: 'settings.json' }),
|
|
405
|
+
permissionsSchema: 'claude',
|
|
406
|
+
sourceMarkerFile: '.gsd-source',
|
|
407
|
+
hyphenNameAgentBody: true,
|
|
408
|
+
legacyCommandsGsdInstallMigration: true,
|
|
409
|
+
legacyCommandsGsdUninstall: 'global',
|
|
410
|
+
}),
|
|
411
|
+
// antigravity's global config dir is resolved dynamically (env-overridable,
|
|
412
|
+
// multi-segment) via resolveAntigravityGlobalDir in getConfigDirFromHome. If the
|
|
413
|
+
// registry fails to load, this floor keeps that routing intact instead of
|
|
414
|
+
// silently falling through to the generic getGlobalConfigHomeFragment default
|
|
415
|
+
// (which would return the wrong '.claude' fragment). (ADR-1239 / #2096)
|
|
416
|
+
antigravity: Object.freeze({ globalDirResolver: 'antigravity' }),
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Resolve a runtime's host behaviors from a capability registry, with the
|
|
421
|
+
* #338-privacy fail-safe floor when the registry (or the runtime's descriptor)
|
|
422
|
+
* is unavailable. Registry is passed in so this is unit-testable under a
|
|
423
|
+
* simulated registry-load failure. (ADR-1239 / #2086)
|
|
424
|
+
*/
|
|
425
|
+
function _resolveHostBehaviors(runtime, registry) {
|
|
426
|
+
const cap = registry && registry.runtimes && registry.runtimes[runtime];
|
|
427
|
+
const declared = cap && cap.runtime && cap.runtime.hostBehaviors;
|
|
428
|
+
if (declared) return declared;
|
|
429
|
+
return FALLBACK_HOST_BEHAVIORS[runtime] || {};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Host-specific install behaviors, declared on the runtime descriptor
|
|
434
|
+
* (capabilities/<runtime>/capability.json -> runtime.hostBehaviors) instead of
|
|
435
|
+
* scattered `runtime === '<id>'` string checks (ADR-1239 / #2086). Returns {}
|
|
436
|
+
* for runtimes that declare none, so every behavior branch degrades to the
|
|
437
|
+
* generic path by default — EXCEPT the reference host's #338-critical keys, which
|
|
438
|
+
* fall back to FALLBACK_HOST_BEHAVIORS if the registry failed to load.
|
|
439
|
+
*/
|
|
440
|
+
function _hostBehaviors(runtime) {
|
|
441
|
+
return _resolveHostBehaviors(runtime, _capabilityRegistry);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Resolve the ACTUAL on-disk skills-install directory for a runtime, honoring a
|
|
446
|
+
* skills-kind `home` override (ADR-1239 upgrade 3 / #2088: e.g. Codex skills ->
|
|
447
|
+
* $HOME/.agents/skills instead of the runtime's configDir). Descriptor-driven
|
|
448
|
+
* (no runtime === '<id>' check) so the snapshot/rollback machinery and post-install
|
|
449
|
+
* verification look where the skills actually landed. Falls back to <targetDir>/skills.
|
|
450
|
+
*/
|
|
451
|
+
function _resolveSkillsRootDir(runtime, targetDir, scope) {
|
|
452
|
+
try {
|
|
453
|
+
const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope);
|
|
454
|
+
const skillsKind = layout.kinds.find((k) => k.kind === 'skills');
|
|
455
|
+
if (skillsKind) return path.join(skillsKind.home || targetDir, skillsKind.destSubpath);
|
|
456
|
+
} catch (_e) { /* fall through to the configDir default */ }
|
|
457
|
+
return path.join(targetDir, 'skills');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Construct the imperative Host-Integration adapter (ADR-1239 / #2086), FAIL-OPEN.
|
|
462
|
+
* `createImperativeAdapter` composes the capability registry via
|
|
463
|
+
* `loadRegistry({includeInstalled:true})`, which require()s several capability
|
|
464
|
+
* modules. If any is unavailable (e.g. a packaging regression), return null so
|
|
465
|
+
* the caller degrades to the engine directly rather than hard-crashing install/
|
|
466
|
+
* uninstall — matching the optional `capability-registry.cjs` load posture above.
|
|
467
|
+
*/
|
|
468
|
+
function _runtimeAdapter(runtime) {
|
|
469
|
+
try {
|
|
470
|
+
return createImperativeAdapter({ runtime });
|
|
471
|
+
} catch {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
353
475
|
const {
|
|
354
476
|
applyInstallerMigrationPlan,
|
|
355
477
|
discoverInstallerMigrations,
|
|
@@ -385,6 +507,7 @@ const {
|
|
|
385
507
|
installRuntimeArtifacts,
|
|
386
508
|
uninstallRuntimeArtifacts,
|
|
387
509
|
installOpencodeFamilySkills,
|
|
510
|
+
_installNativePluginIfDeclared,
|
|
388
511
|
_copyStaged,
|
|
389
512
|
hasExistingSymlinkBetween,
|
|
390
513
|
preserveUserArtifacts,
|
|
@@ -434,7 +557,7 @@ if (hasMinimal && _profileArgRaw) {
|
|
|
434
557
|
|
|
435
558
|
function selectRuntimesFromArgs(runtimeArgs) {
|
|
436
559
|
if (runtimeArgs.includes('--all')) {
|
|
437
|
-
return ['claude', 'kimi', 'kilo', 'opencode', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'zcode'];
|
|
560
|
+
return ['claude', 'kimi', 'kilo', 'opencode', 'pi', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'zcode'];
|
|
438
561
|
}
|
|
439
562
|
if (runtimeArgs.includes('--both')) {
|
|
440
563
|
return ['claude', 'opencode'];
|
|
@@ -443,6 +566,7 @@ function selectRuntimesFromArgs(runtimeArgs) {
|
|
|
443
566
|
const selected = [];
|
|
444
567
|
if (runtimeArgs.includes('--claude')) selected.push('claude');
|
|
445
568
|
if (runtimeArgs.includes('--opencode')) selected.push('opencode');
|
|
569
|
+
if (runtimeArgs.includes('--pi')) selected.push('pi');
|
|
446
570
|
if (runtimeArgs.includes('--kilo')) selected.push('kilo');
|
|
447
571
|
if (runtimeArgs.includes('--codex')) selected.push('codex');
|
|
448
572
|
if (runtimeArgs.includes('--copilot')) selected.push('copilot');
|
|
@@ -546,7 +670,15 @@ function getConfigDirFromHome(runtime, isGlobal) {
|
|
|
546
670
|
// multi-segment via resolveAntigravityGlobalDir + path.relative) — not a table
|
|
547
671
|
// entry. (The prior inner `if (!isGlobal) return "'.agents'"` was unreachable:
|
|
548
672
|
// !isGlobal returns at the top of this function.)
|
|
549
|
-
|
|
673
|
+
// Descriptor-driven (ADR-1239 / #2096): folded from a hardcoded
|
|
674
|
+
// `runtime === 'antigravity'` literal into a read of the runtime's
|
|
675
|
+
// `hostBehaviors.globalDirResolver` descriptor field (via _hostBehaviors, which
|
|
676
|
+
// also degrades to FALLBACK_HOST_BEHAVIORS on registry-load failure). This is
|
|
677
|
+
// antigravity-unique: unlike `configHome.kind === 'dot-home-nested'` (which
|
|
678
|
+
// windsurf also declares — see capabilities/windsurf/capability.json — and
|
|
679
|
+
// would wrongly route windsurf's global dir through
|
|
680
|
+
// resolveAntigravityGlobalDir), `globalDirResolver` is only set by antigravity.
|
|
681
|
+
if (_hostBehaviors(runtime).globalDirResolver === 'antigravity') {
|
|
550
682
|
const antigravityDir = resolveAntigravityGlobalDir();
|
|
551
683
|
const rel = path.relative(os.homedir(), antigravityDir);
|
|
552
684
|
const segments = rel.split(path.sep).filter(Boolean);
|
|
@@ -580,7 +712,7 @@ const banner = '\n' +
|
|
|
580
712
|
' GSD Core ' + dim + 'v' + pkg.version + reset + '\n' +
|
|
581
713
|
' Git. Ship. Done.\n' +
|
|
582
714
|
' A meta-prompting, context engineering and spec-driven\n' +
|
|
583
|
-
' development workflows for Claude Code, OpenCode, Kimi CLI, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline, CodeBuddy and
|
|
715
|
+
' development workflows for Claude Code, OpenCode, Kimi CLI, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline, CodeBuddy, ZCode and pi.\n';
|
|
584
716
|
|
|
585
717
|
// Pure seam: parse --config-dir / -c from an arbitrary args array.
|
|
586
718
|
// Returns the path string, '' for an empty equals-form value, or null when the
|
|
@@ -661,6 +793,10 @@ const referencesHook = hooksSurface.referencesHook;
|
|
|
661
793
|
// applySettingsJsonHooks: mutates settings.hooks.* in place with all GSD-managed
|
|
662
794
|
// hook registrations for settings.json-surface runtimes (ADR-857 phase 5f-1b).
|
|
663
795
|
const applySettingsJsonHooks = hooksSurface.applySettingsJsonHooks;
|
|
796
|
+
// writeKimiHooksToml / removeKimiHooksToml: kimi's native config.toml [[hooks]]
|
|
797
|
+
// surface (#2095 EoS/kimi Upgrade 1) — separate from settings.json entirely.
|
|
798
|
+
const writeKimiHooksToml = hooksSurface.writeKimiHooksToml;
|
|
799
|
+
const removeKimiHooksToml = hooksSurface.removeKimiHooksToml;
|
|
664
800
|
// processAttribution: pure Co-Authored-By content transform, relocated to the
|
|
665
801
|
// conversion module (ADR-1508 / #1510 Phase 1). Bound here so install.js
|
|
666
802
|
// callers continue to work and there is a single implementation. (All call
|
|
@@ -910,6 +1046,9 @@ function resolveKiloConfigPath(configDir) {
|
|
|
910
1046
|
return path.join(configDir, 'kilo.json');
|
|
911
1047
|
}
|
|
912
1048
|
|
|
1049
|
+
// #2087 — attribution config-path resolvers, keyed by descriptor (hostBehaviors.attributionConfigResolver)
|
|
1050
|
+
const ATTRIBUTION_CONFIG_RESOLVERS = { opencode: resolveOpencodeConfigPath, kilo: resolveKiloConfigPath };
|
|
1051
|
+
|
|
913
1052
|
/**
|
|
914
1053
|
* Strip JSONC comments (// and /* */) from a string to produce valid JSON.
|
|
915
1054
|
* Handles comments inside strings correctly (does not strip them).
|
|
@@ -1071,126 +1210,6 @@ function readGsdEffectiveModelOverrides(targetDir = null) {
|
|
|
1071
1210
|
return { ...(global || {}), ...(projectOverrides || {}) };
|
|
1072
1211
|
}
|
|
1073
1212
|
|
|
1074
|
-
/**
|
|
1075
|
-
* #443 — Read the merged `effort` config block for install-time effort resolution.
|
|
1076
|
-
*
|
|
1077
|
-
* Probes the same config sources as readGsdRuntimeProfileResolver (per-project
|
|
1078
|
-
* `.planning/config.json` wins over `~/.gsd/defaults.json`) but extracts the
|
|
1079
|
-
* `effort` object instead of the model-profile fields.
|
|
1080
|
-
*
|
|
1081
|
-
* Returns the merged `effort` object or null when neither source defines one.
|
|
1082
|
-
* The caller can pass this to resolveInstallTimeEffort() which is pure and
|
|
1083
|
-
* requires no filesystem access beyond what this helper already performs.
|
|
1084
|
-
*
|
|
1085
|
-
* @param {string|null} targetDir Runtime install root (walks up to find .planning/).
|
|
1086
|
-
* @returns {object|null}
|
|
1087
|
-
*/
|
|
1088
|
-
function readGsdEffectiveEffortConfig(targetDir = null) {
|
|
1089
|
-
const homeDefaults = _readGsdConfigFile(
|
|
1090
|
-
path.join(os.homedir(), '.gsd', 'defaults.json'),
|
|
1091
|
-
'~/.gsd/defaults.json'
|
|
1092
|
-
);
|
|
1093
|
-
|
|
1094
|
-
let projectConfig = null;
|
|
1095
|
-
if (targetDir) {
|
|
1096
|
-
let probeDir = path.resolve(targetDir);
|
|
1097
|
-
for (let depth = 0; depth < 8; depth += 1) {
|
|
1098
|
-
const candidate = path.join(probeDir, '.planning', 'config.json');
|
|
1099
|
-
if (fs.existsSync(candidate)) {
|
|
1100
|
-
projectConfig = _readGsdConfigFile(candidate, '.planning/config.json');
|
|
1101
|
-
break;
|
|
1102
|
-
}
|
|
1103
|
-
const parent = path.dirname(probeDir);
|
|
1104
|
-
if (parent === probeDir) break;
|
|
1105
|
-
probeDir = parent;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
const homeEffort = (homeDefaults && homeDefaults.effort && typeof homeDefaults.effort === 'object' && !Array.isArray(homeDefaults.effort))
|
|
1110
|
-
? homeDefaults.effort
|
|
1111
|
-
: null;
|
|
1112
|
-
const projectEffort = (projectConfig && projectConfig.effort && typeof projectConfig.effort === 'object' && !Array.isArray(projectConfig.effort))
|
|
1113
|
-
? projectConfig.effort
|
|
1114
|
-
: null;
|
|
1115
|
-
|
|
1116
|
-
if (!homeEffort && !projectEffort) return null;
|
|
1117
|
-
|
|
1118
|
-
// Per-project wins on conflict within each sub-field. Merge field-by-field so
|
|
1119
|
-
// a project config that only sets agent_overrides still inherits global
|
|
1120
|
-
// routing_tier_defaults and default.
|
|
1121
|
-
return {
|
|
1122
|
-
...(homeEffort || {}),
|
|
1123
|
-
...(projectEffort || {}),
|
|
1124
|
-
// Deep-merge agent_overrides (project wins per-key)
|
|
1125
|
-
agent_overrides: {
|
|
1126
|
-
...((homeEffort && homeEffort.agent_overrides) || {}),
|
|
1127
|
-
...((projectEffort && projectEffort.agent_overrides) || {}),
|
|
1128
|
-
},
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
/**
|
|
1134
|
-
* #443 — Resolve install-time effort for a given agent, using the same
|
|
1135
|
-
* precedence chain as resolveEffortInternal() in core.cjs, but operating
|
|
1136
|
-
* on a pre-loaded effortCfg object (no loadConfig side-effects at install).
|
|
1137
|
-
*
|
|
1138
|
-
* Precedence (mirrors resolveEffortInternal):
|
|
1139
|
-
* 1. effortCfg.agent_overrides[agentName]
|
|
1140
|
-
* 2. effortCfg.routing_tier_defaults[agentTier] (if effortCfg present)
|
|
1141
|
-
* — OR manifest tier defaults when effortCfg is null
|
|
1142
|
-
* 3. effortCfg.default
|
|
1143
|
-
* 4. 'high' (hardcoded fallback)
|
|
1144
|
-
*
|
|
1145
|
-
* @param {object|null} effortCfg Result of readGsdEffectiveEffortConfig().
|
|
1146
|
-
* @param {string} agentName e.g. 'gsd-planner'
|
|
1147
|
-
* @returns {string} Universal effort string (low/medium/high/xhigh/max/minimal)
|
|
1148
|
-
*/
|
|
1149
|
-
function resolveInstallTimeEffort(effortCfg, agentName) {
|
|
1150
|
-
// Validates each candidate against the canonical EFFORT_SET (sourced once
|
|
1151
|
-
// from core.cjs) before accepting it, mirroring resolveEffortInternal exactly.
|
|
1152
|
-
// Invalid values fall through to the next precedence layer; final fallback 'high'.
|
|
1153
|
-
|
|
1154
|
-
// Step 1: agent_overrides
|
|
1155
|
-
if (effortCfg) {
|
|
1156
|
-
const ao = effortCfg.agent_overrides;
|
|
1157
|
-
if (ao && typeof ao === 'object' && !Array.isArray(ao)) {
|
|
1158
|
-
const v = ao[agentName];
|
|
1159
|
-
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
// Step 2: routing_tier_defaults keyed by the agent's catalog tier
|
|
1164
|
-
const { AGENT_DEFAULT_TIERS, EFFORT_MANIFEST_TIER_DEFAULTS, EFFORT_MANIFEST_DEFAULT } = _getGsdEffortCatalog();
|
|
1165
|
-
const agentTier = AGENT_DEFAULT_TIERS[agentName];
|
|
1166
|
-
if (agentTier) {
|
|
1167
|
-
if (effortCfg && effortCfg.routing_tier_defaults &&
|
|
1168
|
-
typeof effortCfg.routing_tier_defaults === 'object' &&
|
|
1169
|
-
!Array.isArray(effortCfg.routing_tier_defaults)) {
|
|
1170
|
-
const v = effortCfg.routing_tier_defaults[agentTier];
|
|
1171
|
-
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
|
|
1172
|
-
} else if (!effortCfg) {
|
|
1173
|
-
// No effort config — use manifest tier defaults
|
|
1174
|
-
const v = EFFORT_MANIFEST_TIER_DEFAULTS[agentTier];
|
|
1175
|
-
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
|
|
1176
|
-
}
|
|
1177
|
-
// effortCfg exists but has no routing_tier_defaults — fall through
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
// Step 3: effort.default
|
|
1181
|
-
if (effortCfg) {
|
|
1182
|
-
const d = effortCfg.default;
|
|
1183
|
-
if (typeof d === 'string' && GSD_EFFORT_SET.has(d)) return d;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
// Step 4: manifest default (sourced from config-defaults.manifest.json effort.default)
|
|
1187
|
-
// If even the manifest default is invalid, fall back to 'high'.
|
|
1188
|
-
if (typeof EFFORT_MANIFEST_DEFAULT === 'string' && GSD_EFFORT_SET.has(EFFORT_MANIFEST_DEFAULT)) {
|
|
1189
|
-
return EFFORT_MANIFEST_DEFAULT;
|
|
1190
|
-
}
|
|
1191
|
-
return 'high';
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
1213
|
/**
|
|
1195
1214
|
* #443 — Inject `effort: <value>` into YAML frontmatter of a Claude .md agent
|
|
1196
1215
|
* file in a newline-agnostic way (LF and CRLF source files are both handled).
|
|
@@ -1296,29 +1315,6 @@ const READONLY_AGENT_DISALLOWED_TOOLS = {
|
|
|
1296
1315
|
'gsd-ui-auditor': 'Edit, MultiEdit',
|
|
1297
1316
|
};
|
|
1298
1317
|
|
|
1299
|
-
/**
|
|
1300
|
-
* #2517 — Read a single GSD config file (defaults.json or per-project
|
|
1301
|
-
* config.json) into a plain object, returning null on missing/empty files
|
|
1302
|
-
* and warning to stderr on JSON parse failures so silent corruption can't
|
|
1303
|
-
* mask broken configs (review finding #5).
|
|
1304
|
-
*/
|
|
1305
|
-
function _readGsdConfigFile(absPath, label) {
|
|
1306
|
-
if (!fs.existsSync(absPath)) return null;
|
|
1307
|
-
let raw;
|
|
1308
|
-
try {
|
|
1309
|
-
raw = fs.readFileSync(absPath, 'utf-8');
|
|
1310
|
-
} catch (err) {
|
|
1311
|
-
process.stderr.write(`gsd: warning — could not read ${label} (${absPath}): ${err.message}\n`);
|
|
1312
|
-
return null;
|
|
1313
|
-
}
|
|
1314
|
-
try {
|
|
1315
|
-
return JSON.parse(raw);
|
|
1316
|
-
} catch (err) {
|
|
1317
|
-
process.stderr.write(`gsd: warning — invalid JSON in ${label} (${absPath}): ${err.message}\n`);
|
|
1318
|
-
return null;
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
1318
|
/**
|
|
1323
1319
|
* #2517 — Build a runtime-aware tier resolver for the install path.
|
|
1324
1320
|
*
|
|
@@ -1421,15 +1417,14 @@ function getCommitAttribution(runtime) {
|
|
|
1421
1417
|
|
|
1422
1418
|
let result;
|
|
1423
1419
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
: resolveKiloConfigPath;
|
|
1420
|
+
const _attrResolverKey = _hostBehaviors(runtime).attributionConfigResolver;
|
|
1421
|
+
if (_attrResolverKey && ATTRIBUTION_CONFIG_RESOLVERS[_attrResolverKey]) {
|
|
1422
|
+
const resolveConfigPath = ATTRIBUTION_CONFIG_RESOLVERS[_attrResolverKey];
|
|
1428
1423
|
const config = readSettings(resolveConfigPath(getGlobalConfigDir(runtime, null)));
|
|
1429
1424
|
result = (config && config.disable_ai_attribution === true) ? null : undefined;
|
|
1430
|
-
} else if (runtime === '
|
|
1425
|
+
} else if (_hostBehaviors(runtime).attributionSource === 'settings-json-commit') {
|
|
1431
1426
|
// Claude Code
|
|
1432
|
-
const settings = readSettings(path.join(getGlobalConfigDir(
|
|
1427
|
+
const settings = readSettings(path.join(getGlobalConfigDir(runtime, explicitConfigDir), 'settings.json'));
|
|
1433
1428
|
if (!settings || !settings.attribution || settings.attribution.commit === undefined) {
|
|
1434
1429
|
result = undefined;
|
|
1435
1430
|
} else if (settings.attribution.commit === '') {
|
|
@@ -1897,11 +1892,13 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
|
|
|
1897
1892
|
// Hermes' SKILL.md spec lists `version` as a required frontmatter field.
|
|
1898
1893
|
// Track GSD's package version so Hermes' skill_view() reports a stable
|
|
1899
1894
|
// identifier per install.
|
|
1900
|
-
if (runtime
|
|
1901
|
-
// #778 (b) —
|
|
1902
|
-
//
|
|
1903
|
-
//
|
|
1904
|
-
|
|
1895
|
+
if (_hostBehaviors(runtime).skillFrontmatterVersion) fm += `version: ${yamlQuote(pkg.version)}\n`;
|
|
1896
|
+
// #778 (b) — numeric priority for /skills ordering, declared on the runtime
|
|
1897
|
+
// descriptor (runtime.hostBehaviors.skillPriorityFrontmatter). Scoped to
|
|
1898
|
+
// runtimes that declare the flag so Claude/Hermes skill frontmatter is
|
|
1899
|
+
// unchanged (they ignore the field, but we keep their output byte-stable).
|
|
1900
|
+
// skillName is the `gsd-<stem>` dir name. (ADR-1239 / #2086)
|
|
1901
|
+
if (_hostBehaviors(runtime).skillPriorityFrontmatter) {
|
|
1905
1902
|
const stem = typeof skillName === 'string' && skillName.startsWith('gsd-')
|
|
1906
1903
|
? skillName.slice(4)
|
|
1907
1904
|
: skillName;
|
|
@@ -1946,6 +1943,14 @@ function convertGsdCommandReferencesToKimiSkillInvocations(content, cmdNames) {
|
|
|
1946
1943
|
.replace(hyphenPattern, (_, cmd) => `/skill:gsd-${cmd}`);
|
|
1947
1944
|
}
|
|
1948
1945
|
|
|
1946
|
+
// DEFECT.GENERATIVE-FIX: this body is mirrored in
|
|
1947
|
+
// src/runtime-artifact-conversion.cts's convertClaudeCommandToKimiSkill (dead
|
|
1948
|
+
// for the live skills-install path, which routes here via
|
|
1949
|
+
// install-engine.cts's SKILLS_CONVERTER_REGISTRY through the kimi capability
|
|
1950
|
+
// descriptor's artifactLayout `converter: "convertClaudeCommandToKimiSkill"`;
|
|
1951
|
+
// kept for bin/install.js's own module-level export/test surface). Neither
|
|
1952
|
+
// copy re-exports the other — mirror any behavior change into both. Guarded
|
|
1953
|
+
// by the output-parity test in tests/runtime-converters.test.cjs (#2095).
|
|
1949
1954
|
function convertClaudeCommandToKimiSkill(content, skillName, _runtime = null, cmdNames = null) {
|
|
1950
1955
|
const { frontmatter, body } = extractFrontmatterAndBody(content);
|
|
1951
1956
|
const kimiSkillName = normalizeKimiSkillName(skillName);
|
|
@@ -2090,6 +2095,16 @@ function buildKimiSubagentYaml({ name, description, tools }) {
|
|
|
2090
2095
|
return `${lines.join('\n')}\n`;
|
|
2091
2096
|
}
|
|
2092
2097
|
|
|
2098
|
+
// DEFECT.GENERATIVE-FIX: this body is mirrored in
|
|
2099
|
+
// src/runtime-artifact-conversion.cts's buildKimiAgentArtifacts (dead for the
|
|
2100
|
+
// live install path, which routes here via runtime-artifact-layout.cts's
|
|
2101
|
+
// kimiAgentsKind — see its `conversionExports['buildKimiAgentArtifacts']`
|
|
2102
|
+
// dynamic lookup against the compiled runtime-artifact-conversion.cjs; kept
|
|
2103
|
+
// for bin/install.js's own module-level export/test surface). Neither copy
|
|
2104
|
+
// re-exports the other — mirror any behavior change into both, including the
|
|
2105
|
+
// kimi_cli.tools.agent:Agent grant that enables background dispatch
|
|
2106
|
+
// (#2095 Upgrade 2). Guarded by the output-parity test in
|
|
2107
|
+
// tests/runtime-converters.test.cjs (#2095).
|
|
2093
2108
|
function buildKimiAgentArtifacts({
|
|
2094
2109
|
rootAgent = '',
|
|
2095
2110
|
subagents = [],
|
|
@@ -2615,14 +2630,6 @@ function convertClaudeAgentToWindsurfAgent(content) {
|
|
|
2615
2630
|
// Augment uses a tool set similar to Cursor/Windsurf.
|
|
2616
2631
|
// Config lives in .augment/ (local) and ~/.augment/ (global).
|
|
2617
2632
|
|
|
2618
|
-
const claudeToAugmentTools = {
|
|
2619
|
-
Bash: 'launch-process',
|
|
2620
|
-
Edit: 'str-replace-editor',
|
|
2621
|
-
AskUserQuestion: null,
|
|
2622
|
-
SlashCommand: null,
|
|
2623
|
-
TodoWrite: 'add_tasks',
|
|
2624
|
-
};
|
|
2625
|
-
|
|
2626
2633
|
// #1675 (ADR-1508): the augment converter family below was a byte-identical
|
|
2627
2634
|
// duplicate of runtime-artifact-conversion.cjs:
|
|
2628
2635
|
// convertSlashCommandsToAugmentSkillMentions, convertClaudeToAugmentMarkdown,
|
|
@@ -2664,6 +2671,14 @@ function convertClaudeToTraeMarkdown(content) {
|
|
|
2664
2671
|
return converted;
|
|
2665
2672
|
}
|
|
2666
2673
|
|
|
2674
|
+
// DEFECT.GENERATIVE-FIX: this body is mirrored in
|
|
2675
|
+
// src/runtime-artifact-conversion.cts's convertClaudeCommandToTraeSkill (used
|
|
2676
|
+
// by src/install-engine.cts's skills-install path via
|
|
2677
|
+
// SKILLS_CONVERTER_REGISTRY). This bin/install.js copy is dead for the live
|
|
2678
|
+
// skills-install path — kept for this file's own module-level export/test
|
|
2679
|
+
// surface. Neither copy re-exports the other — mirror any behavior change
|
|
2680
|
+
// into both. Guarded by the output-parity test in
|
|
2681
|
+
// tests/runtime-converters.test.cjs (#2094).
|
|
2667
2682
|
function convertClaudeCommandToTraeSkill(content, skillName) {
|
|
2668
2683
|
const converted = convertClaudeToTraeMarkdown(content);
|
|
2669
2684
|
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
@@ -2678,7 +2693,16 @@ function convertClaudeCommandToTraeSkill(content, skillName) {
|
|
|
2678
2693
|
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
|
2679
2694
|
// #2876: quote so YAML flow indicators (`[BETA] …`) don't break Trae's
|
|
2680
2695
|
// frontmatter parser.
|
|
2681
|
-
|
|
2696
|
+
let fm = `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n`;
|
|
2697
|
+
// #2094: emit `stage:` so Trae's SOLO agent can auto-invoke GSD skills at
|
|
2698
|
+
// the corresponding stage (docs.trae.ai/ide/agent). The field name/schema
|
|
2699
|
+
// is not formally documented (thin SPA docs) — descriptor-driven, single
|
|
2700
|
+
// fixed GSD-side value (runtime.hostBehaviors.soloStageMetadata), inferred/
|
|
2701
|
+
// best-effort.
|
|
2702
|
+
const soloStage = _hostBehaviors('trae').soloStageMetadata;
|
|
2703
|
+
if (soloStage) fm += `stage: ${soloStage}\n`;
|
|
2704
|
+
fm += '---';
|
|
2705
|
+
return `${fm}\n${body}`;
|
|
2682
2706
|
}
|
|
2683
2707
|
|
|
2684
2708
|
function convertClaudeAgentToTraeAgent(content) {
|
|
@@ -3266,6 +3290,77 @@ function cleanupWindsurfLegacyDevinSkills(workspaceDir) {
|
|
|
3266
3290
|
return removed;
|
|
3267
3291
|
}
|
|
3268
3292
|
|
|
3293
|
+
/**
|
|
3294
|
+
* Migrate a skills kind that moved to an alternate `home` (ADR-1239 split-home):
|
|
3295
|
+
* remove now-stale `<prefix>*` skill dirs left at the OLD configDir-rooted
|
|
3296
|
+
* location by installs from before the move. Without this, upgrading (e.g. Codex
|
|
3297
|
+
* relocating skills to ~/.agents/skills) orphans the pre-move dirs at
|
|
3298
|
+
* ~/.codex/skills. Only managed `<prefix>*` dirs are touched; user-owned content
|
|
3299
|
+
* (non-prefixed dirs, gsd-dev-preferences, symlinks) is preserved. Fail-open.
|
|
3300
|
+
* @param {string} oldSkillsDir absolute path to the pre-move skills location
|
|
3301
|
+
* @param {string} prefix managed skill-dir prefix (e.g. 'gsd-')
|
|
3302
|
+
* @returns {number} count of stale dirs removed
|
|
3303
|
+
*/
|
|
3304
|
+
function cleanupMovedSkillsOldLocation(oldSkillsDir, prefix) {
|
|
3305
|
+
if (!fs.existsSync(oldSkillsDir)) return 0;
|
|
3306
|
+
|
|
3307
|
+
// Mirror the user-owned list from cleanupCodexSkillMetadataSidecars (#2973).
|
|
3308
|
+
const _userOwnedSkillDirs = new Set(['gsd-dev-preferences']);
|
|
3309
|
+
let removed = 0;
|
|
3310
|
+
|
|
3311
|
+
for (const entry of fs.readdirSync(oldSkillsDir, { withFileTypes: true })) {
|
|
3312
|
+
if (!entry.isDirectory() || !entry.name.startsWith(prefix)) continue;
|
|
3313
|
+
if (_userOwnedSkillDirs.has(entry.name)) continue;
|
|
3314
|
+
|
|
3315
|
+
const dirToRemove = path.join(oldSkillsDir, entry.name);
|
|
3316
|
+
try {
|
|
3317
|
+
// Symlink guard (mirrors cleanupWindsurfLegacyDevinSkills): never delete
|
|
3318
|
+
// through a symlinked gsd-* dir — it could escape the tree.
|
|
3319
|
+
const stat = fs.lstatSync(dirToRemove);
|
|
3320
|
+
if (stat.isSymbolicLink()) continue;
|
|
3321
|
+
|
|
3322
|
+
fs.rmSync(dirToRemove, { recursive: true, force: true });
|
|
3323
|
+
removed++;
|
|
3324
|
+
} catch (_err) {
|
|
3325
|
+
// Fail open — a single bad dir must not block install/uninstall.
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
// Prune the old skills dir if now empty — leaves the configHome clean.
|
|
3330
|
+
// Never remove a non-empty container (user may keep other content there).
|
|
3331
|
+
try {
|
|
3332
|
+
if (fs.existsSync(oldSkillsDir) && fs.readdirSync(oldSkillsDir).length === 0) {
|
|
3333
|
+
fs.rmdirSync(oldSkillsDir);
|
|
3334
|
+
}
|
|
3335
|
+
} catch (_err) {
|
|
3336
|
+
// best-effort container cleanup
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
return removed;
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
/**
|
|
3343
|
+
* When a runtime's skills kind declares an alternate `home` (split-home move),
|
|
3344
|
+
* return the now-stale configDir-rooted skills location that installs before the
|
|
3345
|
+
* move used; null when no move is in effect (no home override, or home resolves
|
|
3346
|
+
* to the same path). Descriptor-driven — no per-runtime hardcoding.
|
|
3347
|
+
* @returns {string|null}
|
|
3348
|
+
*/
|
|
3349
|
+
function _resolveMovedSkillsOldDir(runtime, targetDir, scope) {
|
|
3350
|
+
try {
|
|
3351
|
+
const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope);
|
|
3352
|
+
const skillsKind = layout.kinds.find((k) => k.kind === 'skills');
|
|
3353
|
+
if (skillsKind && skillsKind.home) {
|
|
3354
|
+
const oldDir = path.join(targetDir, skillsKind.destSubpath);
|
|
3355
|
+
const newDir = path.join(skillsKind.home, skillsKind.destSubpath);
|
|
3356
|
+
if (path.resolve(oldDir) !== path.resolve(newDir)) return oldDir;
|
|
3357
|
+
}
|
|
3358
|
+
} catch (_e) {
|
|
3359
|
+
// No migration when the layout can't resolve — never block on this.
|
|
3360
|
+
}
|
|
3361
|
+
return null;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3269
3364
|
/**
|
|
3270
3365
|
* Generate the GSD config block for Codex config.toml.
|
|
3271
3366
|
* @param {Array<{name: string, description: string}>} agents
|
|
@@ -3281,6 +3376,17 @@ function generateCodexConfigBlock(agents, targetDir) {
|
|
|
3281
3376
|
'',
|
|
3282
3377
|
];
|
|
3283
3378
|
|
|
3379
|
+
// ADR-1239 upgrade 2 / #2088 — explicit dispatch tuning. Pin `max_depth` on the
|
|
3380
|
+
// `[agents]` (AgentsToml) table rather than relying on codex-cli's implicit
|
|
3381
|
+
// default, realizing the negotiated `dispatch.maxDepth: 1` axis. This bare
|
|
3382
|
+
// `[agents]` scalar table coexists with the flattened `[agents.<name>]` role
|
|
3383
|
+
// sub-tables below (validated by validateCodexConfigSchema, which permits a
|
|
3384
|
+
// known-scalar-only `[agents]`). Emitted before the role tables so the parent
|
|
3385
|
+
// table is opened first.
|
|
3386
|
+
lines.push('[agents]');
|
|
3387
|
+
lines.push(`max_depth = ${GSD_CODEX_AGENTS_MAX_DEPTH}`);
|
|
3388
|
+
lines.push('');
|
|
3389
|
+
|
|
3284
3390
|
for (const { name, description } of agents) {
|
|
3285
3391
|
// #2727 — Codex 0.124.0 requires [agents.<name>] struct format, not [[agents]] sequence.
|
|
3286
3392
|
// [[agents]] (introduced in #2645) is rejected by codex-cli 0.124.0 with
|
|
@@ -3294,6 +3400,52 @@ function generateCodexConfigBlock(agents, targetDir) {
|
|
|
3294
3400
|
return lines.join('\n');
|
|
3295
3401
|
}
|
|
3296
3402
|
|
|
3403
|
+
/**
|
|
3404
|
+
* Extract a user's pre-existing AgentsToml scalar assignments from a bare
|
|
3405
|
+
* `[agents]` table — every known scalar EXCEPT `max_depth` (which GSD manages
|
|
3406
|
+
* and always re-emits as 1). Returned as raw `key = value` line strings so
|
|
3407
|
+
* mergeCodexConfig can PRESERVE them in the managed block instead of silently
|
|
3408
|
+
* dropping the user's tuning when the bare `[agents]` table is purged (#2088
|
|
3409
|
+
* review finding: the loosened validator declares such a table legitimate, so
|
|
3410
|
+
* install must not destroy it). Only the first bare `[agents]` section is read;
|
|
3411
|
+
* `[agents.<name>]` role tables are ignored. Fail-open → [].
|
|
3412
|
+
* @returns {string[]}
|
|
3413
|
+
*/
|
|
3414
|
+
function extractCodexUserAgentsScalars(content) {
|
|
3415
|
+
const preserved = [];
|
|
3416
|
+
let section;
|
|
3417
|
+
try {
|
|
3418
|
+
section = getTomlTableSections(content).find((s) => !s.array && s.path === 'agents');
|
|
3419
|
+
} catch (_e) {
|
|
3420
|
+
return preserved;
|
|
3421
|
+
}
|
|
3422
|
+
if (!section) return preserved;
|
|
3423
|
+
const body = content.slice(section.headerEnd, section.end);
|
|
3424
|
+
for (const record of getTomlLineRecords(body)) {
|
|
3425
|
+
if (record.startsInMultilineString || record.tableHeader) continue;
|
|
3426
|
+
const trimmed = record.text.trim();
|
|
3427
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
3428
|
+
if (!record.keySegments || record.keySegments.length !== 1) continue;
|
|
3429
|
+
const key = record.keySegments[0];
|
|
3430
|
+
if (key === 'max_depth') continue; // GSD-managed — GSD's value wins.
|
|
3431
|
+
if (!CODEX_AGENTS_TOML_SCALAR_KEYS.has(key)) continue;
|
|
3432
|
+
preserved.push(trimmed);
|
|
3433
|
+
}
|
|
3434
|
+
return preserved;
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
/**
|
|
3438
|
+
* Splice preserved user AgentsToml scalar lines into the managed GSD config
|
|
3439
|
+
* block, immediately after the `[agents]` header and before GSD's `max_depth`
|
|
3440
|
+
* line. Operates on the pre-EOL-normalization block (LF joins), matching only
|
|
3441
|
+
* the bare `[agents]` header (never `[agents.<name>]`). Returns the block
|
|
3442
|
+
* unchanged when there is nothing to preserve or the anchor is absent.
|
|
3443
|
+
*/
|
|
3444
|
+
function spliceCodexAgentsScalars(block, scalarLines) {
|
|
3445
|
+
if (!scalarLines || scalarLines.length === 0) return block;
|
|
3446
|
+
return block.replace(/(\n\[agents\]\n)(max_depth = )/, `$1${scalarLines.join('\n')}\n$2`);
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3297
3449
|
/**
|
|
3298
3450
|
* Strip any managed GSD agent sections from a TOML string.
|
|
3299
3451
|
*
|
|
@@ -3316,6 +3468,16 @@ function stripCodexGsdAgentSections(content) {
|
|
|
3316
3468
|
return true;
|
|
3317
3469
|
}
|
|
3318
3470
|
|
|
3471
|
+
// GSD's managed `[agents]` scalar block (ADR-1239 upgrade 2 / #2088 — the
|
|
3472
|
+
// `max_depth` dispatch-tuning table). Install purges any pre-existing bare
|
|
3473
|
+
// `[agents]` and writes its own, so a known-scalar-only bare `[agents]` is
|
|
3474
|
+
// GSD-owned; strip it on uninstall. (The marker path already removes it via
|
|
3475
|
+
// the marker-to-EOF cut; this covers the no-marker fallback.)
|
|
3476
|
+
if (!section.array && section.path === 'agents') {
|
|
3477
|
+
const body = content.slice(section.headerEnd, section.end);
|
|
3478
|
+
return codexBareAgentsHasOnlyKnownScalars(body);
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3319
3481
|
// Legacy `[[agents]]` array-of-tables (#2645) — only strip blocks whose
|
|
3320
3482
|
// `name = "gsd-..."`, preserving user-authored [[agents]] entries.
|
|
3321
3483
|
if (section.array && section.path === 'agents') {
|
|
@@ -3343,7 +3505,11 @@ function stripGsdFromCodexConfig(content) {
|
|
|
3343
3505
|
const codexHooksOwnership = getManagedCodexHooksOwnership(content);
|
|
3344
3506
|
|
|
3345
3507
|
if (markerIndex !== -1) {
|
|
3346
|
-
// Has GSD marker — remove everything from marker to EOF
|
|
3508
|
+
// Has GSD marker — remove everything from marker to EOF. First recover the
|
|
3509
|
+
// user's own AgentsToml scalars (max_threads etc.) that install folded into
|
|
3510
|
+
// the managed [agents] block (#2088), so a full install→uninstall cycle
|
|
3511
|
+
// round-trips the user's tuning. GSD-managed max_depth is dropped.
|
|
3512
|
+
const preservedScalars = extractCodexUserAgentsScalars(content.slice(markerIndex));
|
|
3347
3513
|
let before = content.substring(0, markerIndex);
|
|
3348
3514
|
before = stripCodexHooksFeatureAssignments(before, codexHooksOwnership);
|
|
3349
3515
|
// Also strip GSD-injected feature keys above the marker (Case 3 inject)
|
|
@@ -3352,6 +3518,9 @@ function stripGsdFromCodexConfig(content) {
|
|
|
3352
3518
|
before = before.replace(/^\[features\]\s*\n(?=\[|$)/m, '');
|
|
3353
3519
|
before = before.replace(/^\[agents\]\s*\n(?=\[|$)/m, '');
|
|
3354
3520
|
before = before.replace(/^(?:\r?\n)+/, '').trimEnd();
|
|
3521
|
+
if (preservedScalars.length > 0) {
|
|
3522
|
+
before = (before ? before + eol + eol : '') + '[agents]' + eol + preservedScalars.join(eol);
|
|
3523
|
+
}
|
|
3355
3524
|
if (!before) return null;
|
|
3356
3525
|
return before + eol;
|
|
3357
3526
|
}
|
|
@@ -3362,7 +3531,11 @@ function stripGsdFromCodexConfig(content) {
|
|
|
3362
3531
|
cleaned = cleaned.replace(/^multi_agent\s*=\s*true\s*(?:\r?\n)?/m, '');
|
|
3363
3532
|
cleaned = cleaned.replace(/^default_mode_request_user_input\s*=\s*true\s*(?:\r?\n)?/m, '');
|
|
3364
3533
|
|
|
3365
|
-
//
|
|
3534
|
+
// #2088: recover the user's own AgentsToml scalars before the [agents] table is
|
|
3535
|
+
// stripped, so they survive uninstall even in the no-marker fallback path.
|
|
3536
|
+
const preservedScalars = extractCodexUserAgentsScalars(cleaned);
|
|
3537
|
+
|
|
3538
|
+
// Remove [agents.gsd-*] sections + the managed known-scalar [agents] table.
|
|
3366
3539
|
cleaned = stripCodexGsdAgentSections(cleaned);
|
|
3367
3540
|
|
|
3368
3541
|
// Remove [features] section if now empty (only header, no keys before next section)
|
|
@@ -3373,6 +3546,10 @@ function stripGsdFromCodexConfig(content) {
|
|
|
3373
3546
|
|
|
3374
3547
|
cleaned = cleaned.replace(/^(?:\r?\n)+/, '').trimEnd();
|
|
3375
3548
|
|
|
3549
|
+
if (preservedScalars.length > 0) {
|
|
3550
|
+
cleaned = (cleaned ? cleaned + eol + eol : '') + '[agents]' + eol + preservedScalars.join(eol);
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3376
3553
|
if (!cleaned) return null;
|
|
3377
3554
|
return cleaned + eol;
|
|
3378
3555
|
}
|
|
@@ -4844,6 +5021,33 @@ function parseTomlToObject(content) {
|
|
|
4844
5021
|
* - `hooks.<Event>` MUST be an array of tables when present (Codex ≥0.124
|
|
4845
5022
|
* rejects bare `[hooks.<Event>]` single-bracket maps).
|
|
4846
5023
|
*/
|
|
5024
|
+
/**
|
|
5025
|
+
* True when a bare `[agents]` table body contains ONLY known AgentsToml scalar
|
|
5026
|
+
* keys (CODEX_AGENTS_TOML_SCALAR_KEYS) — i.e. it is a valid AgentsToml struct
|
|
5027
|
+
* that Codex's `deny_unknown_fields` will accept, not the break-causing form
|
|
5028
|
+
* (#2760) that carries an unknown key. Comments and blank lines are ignored; an
|
|
5029
|
+
* empty body is trivially valid. Mirrors isLegacyGsdAgentsSection's line scan.
|
|
5030
|
+
*/
|
|
5031
|
+
function codexBareAgentsHasOnlyKnownScalars(body) {
|
|
5032
|
+
const lineRecords = getTomlLineRecords(body);
|
|
5033
|
+
for (const record of lineRecords) {
|
|
5034
|
+
// Conservative reject of anything not positively a single known-scalar
|
|
5035
|
+
// assignment. A multiline-string value cannot be a valid AgentsToml scalar
|
|
5036
|
+
// (max_threads/max_depth/job_max_runtime_seconds are integers,
|
|
5037
|
+
// interrupt_message is a bool — none are strings), so codex would reject it
|
|
5038
|
+
// too; rejecting here is correct, not a false negative.
|
|
5039
|
+
if (record.startsInMultilineString) return false;
|
|
5040
|
+
if (record.tableHeader) return false;
|
|
5041
|
+
const trimmed = record.text.trim();
|
|
5042
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
5043
|
+
if (!record.keySegments || record.keySegments.length !== 1 ||
|
|
5044
|
+
!CODEX_AGENTS_TOML_SCALAR_KEYS.has(record.keySegments[0])) {
|
|
5045
|
+
return false;
|
|
5046
|
+
}
|
|
5047
|
+
}
|
|
5048
|
+
return true;
|
|
5049
|
+
}
|
|
5050
|
+
|
|
4847
5051
|
function validateCodexConfigSchema(content) {
|
|
4848
5052
|
let parsed;
|
|
4849
5053
|
try {
|
|
@@ -4872,10 +5076,21 @@ function validateCodexConfigSchema(content) {
|
|
|
4872
5076
|
}
|
|
4873
5077
|
|
|
4874
5078
|
if (!section.array && section.path === 'agents') {
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
5079
|
+
// #2760 rejected ALL bare `[agents]` tables because a bare table holding a
|
|
5080
|
+
// non-AgentsToml key (`default = "x"`, a role name, etc.) triggers Codex's
|
|
5081
|
+
// "invalid type: ..., expected struct AgentsToml" and breaks every CLI
|
|
5082
|
+
// invocation. But a bare `[agents]` whose keys are all valid AgentsToml
|
|
5083
|
+
// scalars (max_depth/max_threads/...) IS a valid struct — that is exactly
|
|
5084
|
+
// GSD's managed `max_depth` dispatch-tuning block (ADR-1239 upgrade 2 /
|
|
5085
|
+
// #2088), and a user's own scalar tuning. Permit known-scalar-only; still
|
|
5086
|
+
// reject any bare `[agents]` carrying an unknown key.
|
|
5087
|
+
const body = content.slice(section.headerEnd, section.end);
|
|
5088
|
+
if (!codexBareAgentsHasOnlyKnownScalars(body)) {
|
|
5089
|
+
return {
|
|
5090
|
+
ok: false,
|
|
5091
|
+
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)',
|
|
5092
|
+
};
|
|
5093
|
+
}
|
|
4879
5094
|
}
|
|
4880
5095
|
|
|
4881
5096
|
// hooks.state.* is Codex's persistent hook-trust namespace (added in
|
|
@@ -5151,7 +5366,13 @@ function mergeCodexConfig(configPath, gsdBlock) {
|
|
|
5151
5366
|
|
|
5152
5367
|
const existing = fs.readFileSync(configPath, 'utf8');
|
|
5153
5368
|
const eol = detectLineEnding(existing);
|
|
5154
|
-
|
|
5369
|
+
// #2088 review: the bare `[agents]` table is purged below (Case 2/3 via
|
|
5370
|
+
// stripLeakedGsdCodexSections) to keep a single managed `[agents]`. Preserve
|
|
5371
|
+
// the user's own AgentsToml scalar tuning (max_threads, job_max_runtime_seconds,
|
|
5372
|
+
// interrupt_message — everything except GSD-managed max_depth) by re-emitting
|
|
5373
|
+
// it inside the managed block, so install never silently drops it.
|
|
5374
|
+
const mergedGsdBlock = spliceCodexAgentsScalars(gsdBlock, extractCodexUserAgentsScalars(existing));
|
|
5375
|
+
const normalizedGsdBlock = mergedGsdBlock.replace(/\r?\n/g, eol);
|
|
5155
5376
|
const markerIndex = existing.indexOf(GSD_CODEX_MARKER);
|
|
5156
5377
|
|
|
5157
5378
|
// Case 2: Has GSD marker — truncate and re-append
|
|
@@ -5622,6 +5843,37 @@ function removeCursorHooksJson(targetDir) {
|
|
|
5622
5843
|
return hooksSurface.removeCursorHooksJson(targetDir);
|
|
5623
5844
|
}
|
|
5624
5845
|
|
|
5846
|
+
/**
|
|
5847
|
+
* #2100 Stage 2 — Write GSD-managed Windsurf/Cascade lifecycle hooks into
|
|
5848
|
+
* <targetDir>/hooks.json. Both managed hook scripts
|
|
5849
|
+
* (gsd-windsurf-pre-write.js, gsd-windsurf-pre-command.js) are copied from
|
|
5850
|
+
* the GSD hooks/ source to <targetDir>/hooks/ first, so the hooks.json
|
|
5851
|
+
* entries never reference a script that wasn't installed. Mirrors
|
|
5852
|
+
* writeCursorHooksJson's structure; Cascade's blocking protocol (exit code 2)
|
|
5853
|
+
* and entry shape (bare `command` string, no `type` field) are distinct from
|
|
5854
|
+
* Cursor's.
|
|
5855
|
+
*
|
|
5856
|
+
* @param {string} targetDir - The Windsurf config dir (global: ~/.codeium/windsurf; local: .windsurf)
|
|
5857
|
+
* @param {string} src - The GSD install source root (for copying hook scripts)
|
|
5858
|
+
* @param {{ platform?: string }} opts
|
|
5859
|
+
* @returns {{ hooksJsonPath: string, changed: boolean }}
|
|
5860
|
+
*/
|
|
5861
|
+
function writeWindsurfHooksJson(targetDir, src, opts) {
|
|
5862
|
+
return hooksSurface.writeWindsurfHooksJson(targetDir, src, opts);
|
|
5863
|
+
}
|
|
5864
|
+
|
|
5865
|
+
/**
|
|
5866
|
+
* Remove all GSD-managed Windsurf/Cascade lifecycle hook entries from
|
|
5867
|
+
* hooks.json. User-owned entries are preserved. If the file becomes empty,
|
|
5868
|
+
* it is removed.
|
|
5869
|
+
*
|
|
5870
|
+
* @param {string} targetDir - The Windsurf config dir
|
|
5871
|
+
* @returns {{ changed: boolean }}
|
|
5872
|
+
*/
|
|
5873
|
+
function removeWindsurfHooksJson(targetDir) {
|
|
5874
|
+
return hooksSurface.removeWindsurfHooksJson(targetDir);
|
|
5875
|
+
}
|
|
5876
|
+
|
|
5625
5877
|
/**
|
|
5626
5878
|
* #786 — Build the GSD-managed GitHub Copilot lifecycle hook config object.
|
|
5627
5879
|
*
|
|
@@ -5926,7 +6178,12 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
|
|
|
5926
6178
|
}
|
|
5927
6179
|
|
|
5928
6180
|
// Kilo CLI — same conversion logic as OpenCode, different config paths.
|
|
5929
|
-
|
|
6181
|
+
// DEFECT.GENERATIVE-FIX: this body is mirrored in
|
|
6182
|
+
// src/runtime-artifact-conversion.cts's convertClaudeToKiloFrontmatter (used by
|
|
6183
|
+
// src/install-engine.cts's install path). Neither copy re-exports the other —
|
|
6184
|
+
// mirror any behavior change into both. Guarded by the output-parity test in
|
|
6185
|
+
// tests/runtime-converters.test.cjs (#2093).
|
|
6186
|
+
function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverride = null } = {}) {
|
|
5930
6187
|
// Replace tool name references in content (applies to all files)
|
|
5931
6188
|
let convertedContent = content;
|
|
5932
6189
|
convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
|
|
@@ -6085,6 +6342,13 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
|
|
|
6085
6342
|
// For agents: add required Kilo agent fields
|
|
6086
6343
|
if (isAgent) {
|
|
6087
6344
|
newLines.push('mode: subagent');
|
|
6345
|
+
// Embed model override from ~/.gsd/defaults.json so model_overrides is
|
|
6346
|
+
// respected on Kilo (which uses static agent frontmatter, not inline
|
|
6347
|
+
// Task() model parameters) — mirrors convertClaudeToOpencodeFrontmatter's
|
|
6348
|
+
// model emission exactly (#2093 UPGRADE 2 / ADR-1239). See #2256.
|
|
6349
|
+
if (modelOverride) {
|
|
6350
|
+
newLines.push(['model:', modelOverride].join(' '));
|
|
6351
|
+
}
|
|
6088
6352
|
newLines.push(...buildKiloAgentPermissionBlock(agentTools));
|
|
6089
6353
|
}
|
|
6090
6354
|
|
|
@@ -6105,62 +6369,14 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
|
|
|
6105
6369
|
// convertClaudeCommandToKiloSkill: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
6106
6370
|
// Imported from installEngine above.
|
|
6107
6371
|
|
|
6108
|
-
/**
|
|
6109
|
-
* Copy commands to a flat structure for OpenCode
|
|
6110
|
-
* OpenCode expects: command/gsd-help.md (invoked as /gsd-help)
|
|
6111
|
-
* Source structure: commands/gsd/help.md
|
|
6112
|
-
*
|
|
6113
|
-
* @param {string} srcDir - Source directory (e.g., commands/gsd/)
|
|
6114
|
-
* @param {string} destDir - Destination directory (e.g., command/)
|
|
6115
|
-
* @param {string} prefix - Prefix for filenames (e.g., 'gsd')
|
|
6116
|
-
* @param {string} pathPrefix - Path prefix for file references
|
|
6117
|
-
* @param {string} runtime - Target runtime ('claude', 'opencode', or 'kilo')
|
|
6118
|
-
*/
|
|
6119
6372
|
// applyOpencodeFamilyPathPrefix: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
6120
6373
|
// Imported from installEngine above.
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
// Remove old gsd-*.md files before copying new ones
|
|
6128
|
-
if (fs.existsSync(destDir)) {
|
|
6129
|
-
for (const file of fs.readdirSync(destDir)) {
|
|
6130
|
-
if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
|
|
6131
|
-
fs.unlinkSync(path.join(destDir, file));
|
|
6132
|
-
}
|
|
6133
|
-
}
|
|
6134
|
-
} else {
|
|
6135
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
6136
|
-
}
|
|
6137
|
-
|
|
6138
|
-
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
6139
|
-
|
|
6140
|
-
for (const entry of entries) {
|
|
6141
|
-
const srcPath = path.join(srcDir, entry.name);
|
|
6142
|
-
|
|
6143
|
-
if (entry.isDirectory()) {
|
|
6144
|
-
// Recurse into subdirectories, adding to prefix
|
|
6145
|
-
// e.g., commands/gsd/debug/start.md -> command/gsd-debug-start.md
|
|
6146
|
-
copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
|
|
6147
|
-
} else if (entry.name.endsWith('.md')) {
|
|
6148
|
-
// Flatten: help.md -> gsd-help.md
|
|
6149
|
-
const baseName = entry.name.replace('.md', '');
|
|
6150
|
-
const destName = `${prefix}-${baseName}.md`;
|
|
6151
|
-
const destPath = path.join(destDir, destName);
|
|
6152
|
-
|
|
6153
|
-
let content = fs.readFileSync(srcPath, 'utf8');
|
|
6154
|
-
content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix);
|
|
6155
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6156
|
-
content = runtime === 'kilo'
|
|
6157
|
-
? convertClaudeToKiloFrontmatter(content)
|
|
6158
|
-
: convertClaudeToOpencodeFrontmatter(content);
|
|
6159
|
-
|
|
6160
|
-
fs.writeFileSync(destPath, content);
|
|
6161
|
-
}
|
|
6162
|
-
}
|
|
6163
|
-
}
|
|
6374
|
+
//
|
|
6375
|
+
// copyFlattenedCommands (OpenCode/Kilo flattened command/ writer): moved to
|
|
6376
|
+
// src/install-engine.cts as installOpencodeFamilyCommands (ADR-1239 / #2087).
|
|
6377
|
+
// OpenCode/Kilo installs now route through installRuntimeArtifacts's
|
|
6378
|
+
// combinedFamilyInstall path (installOpencodeFamilyArtifacts) instead of the
|
|
6379
|
+
// bespoke inline block that used to call this function.
|
|
6164
6380
|
|
|
6165
6381
|
function listCodexSkillNames(skillsDir, prefix = 'gsd-') {
|
|
6166
6382
|
if (!fs.existsSync(skillsDir)) return [];
|
|
@@ -6353,33 +6569,53 @@ const RUNTIME_CONTENT_DISPATCH = {
|
|
|
6353
6569
|
return content;
|
|
6354
6570
|
},
|
|
6355
6571
|
},
|
|
6572
|
+
// qwen/hermes: brand VALUES are descriptor-driven (ADR-1239 / #2092) via
|
|
6573
|
+
// _hostBehaviors(ctx.runtime).brandingRewrites — EXACT regexes/ordering
|
|
6574
|
+
// preserved from the prior hardcoded-literal versions (including the
|
|
6575
|
+
// qwen-specific `.claude/skills/` -> `.qwen/skills/` pre-rewrite, whose
|
|
6576
|
+
// target is derived as `${b['.claude/']}skills/`).
|
|
6356
6577
|
qwen: {
|
|
6357
|
-
md: (content) => {
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6578
|
+
md: (content, ctx) => {
|
|
6579
|
+
// Guarded (post-review #2092): degrade closed to a no-op if the
|
|
6580
|
+
// registry fails to load, instead of throwing on `b['CLAUDE.md']`.
|
|
6581
|
+
const b = _hostBehaviors(ctx.runtime).brandingRewrites;
|
|
6582
|
+
if (b) {
|
|
6583
|
+
content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
|
|
6584
|
+
content = content.replace(/\bClaude Code\b/g, b['Claude Code']);
|
|
6585
|
+
content = content.replace(/\.claude\//g, b['.claude/']);
|
|
6586
|
+
}
|
|
6361
6587
|
return content;
|
|
6362
6588
|
},
|
|
6363
|
-
js: (content) => {
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6589
|
+
js: (content, ctx) => {
|
|
6590
|
+
const b = _hostBehaviors(ctx.runtime).brandingRewrites;
|
|
6591
|
+
if (b) {
|
|
6592
|
+
content = content.replace(/\.claude\/skills\//g, `${b['.claude/']}skills/`);
|
|
6593
|
+
content = content.replace(/\.claude\//g, b['.claude/']);
|
|
6594
|
+
content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
|
|
6595
|
+
content = content.replace(/\bClaude Code\b/g, b['Claude Code']);
|
|
6596
|
+
}
|
|
6368
6597
|
return content;
|
|
6369
6598
|
},
|
|
6370
6599
|
},
|
|
6371
6600
|
hermes: {
|
|
6372
|
-
md: (content) => {
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6601
|
+
md: (content, ctx) => {
|
|
6602
|
+
// Guarded (post-review #2092): see qwen entry above.
|
|
6603
|
+
const b = _hostBehaviors(ctx.runtime).brandingRewrites;
|
|
6604
|
+
if (b) {
|
|
6605
|
+
content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
|
|
6606
|
+
content = content.replace(/\bClaude Code\b/g, b['Claude Code']);
|
|
6607
|
+
content = content.replace(/\.claude\//g, b['.claude/']);
|
|
6608
|
+
}
|
|
6376
6609
|
return content;
|
|
6377
6610
|
},
|
|
6378
|
-
js: (content) => {
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6611
|
+
js: (content, ctx) => {
|
|
6612
|
+
const b = _hostBehaviors(ctx.runtime).brandingRewrites;
|
|
6613
|
+
if (b) {
|
|
6614
|
+
content = content.replace(/\.claude\/skills\//g, `${b['.claude/']}skills/`);
|
|
6615
|
+
content = content.replace(/\.claude\//g, b['.claude/']);
|
|
6616
|
+
content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
|
|
6617
|
+
content = content.replace(/\bClaude Code\b/g, b['Claude Code']);
|
|
6618
|
+
}
|
|
6383
6619
|
return content;
|
|
6384
6620
|
},
|
|
6385
6621
|
},
|
|
@@ -6462,7 +6698,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
6462
6698
|
// copyWithPathReplacement is the emit path for gsd-core/workflows/*.md;
|
|
6463
6699
|
// _applyRuntimeRewrites is NOT invoked here, so this is what makes the fix
|
|
6464
6700
|
// live in real installs (it is a no-op for files without those lines).
|
|
6465
|
-
if (runtime
|
|
6701
|
+
if (!_hostBehaviors(runtime).authorsCanonicalWorkflow) {
|
|
6466
6702
|
content = _stampNonClaudeRuntimeDefaults(content, runtime);
|
|
6467
6703
|
}
|
|
6468
6704
|
|
|
@@ -6638,25 +6874,21 @@ function validateHookFields(settings) {
|
|
|
6638
6874
|
* GSD hook filenames removed during uninstall.
|
|
6639
6875
|
* Module-level so tests can assert structurally instead of regex-parsing source
|
|
6640
6876
|
* (retires pending-migration-to-typed-ir on hooks-opt-in.test.cjs, per #455).
|
|
6877
|
+
*
|
|
6878
|
+
* Derived from _HOOKS_TO_COPY (scripts/build-hooks.js — the SAME single source
|
|
6879
|
+
* of truth INSTALLED_HOOK_FILES uses for manifest-tracking above) instead of a
|
|
6880
|
+
* separately hand-maintained literal array. The hand-maintained array had
|
|
6881
|
+
* silently drifted out of sync with the install-time set — missing
|
|
6882
|
+
* gsd-check-update-worker.js, gsd-ensure-canonical-path.js,
|
|
6883
|
+
* managed-hooks-registry.cjs, gsd-cursor-pre-tool.js, gsd-cursor-stop.js,
|
|
6884
|
+
* gsd-cursor-subagent-start.js, gsd-cursor-subagent-stop.js, and
|
|
6885
|
+
* gsd-worktree-path-guard.js — so every one of those files (and the hooks/ dir
|
|
6886
|
+
* itself, via the non-empty-dir rmdir guard) was left behind on uninstall for
|
|
6887
|
+
* every settings-json-hook runtime. `gsd-check-update.cmd` is added on top: a
|
|
6888
|
+
* Windows-only SessionStart shim generated at install time (not copied from
|
|
6889
|
+
* hooks/dist/, so it is not in _HOOKS_TO_COPY).
|
|
6641
6890
|
*/
|
|
6642
|
-
const GSD_UNINSTALL_HOOKS = [
|
|
6643
|
-
'gsd-statusline.js',
|
|
6644
|
-
'gsd-check-update.js',
|
|
6645
|
-
'gsd-check-update.cmd',
|
|
6646
|
-
'gsd-config-reload.js',
|
|
6647
|
-
'gsd-context-monitor.js',
|
|
6648
|
-
'gsd-cursor-session-start.js',
|
|
6649
|
-
'gsd-cursor-post-tool.js',
|
|
6650
|
-
'gsd-prompt-guard.js',
|
|
6651
|
-
'gsd-read-guard.js',
|
|
6652
|
-
'gsd-read-injection-scanner.js',
|
|
6653
|
-
'gsd-update-banner.js',
|
|
6654
|
-
'gsd-workflow-guard.js',
|
|
6655
|
-
'gsd-session-state.sh',
|
|
6656
|
-
'gsd-validate-commit.sh',
|
|
6657
|
-
'gsd-phase-boundary.sh',
|
|
6658
|
-
'gsd-graphify-update.sh',
|
|
6659
|
-
];
|
|
6891
|
+
const GSD_UNINSTALL_HOOKS = [..._HOOKS_TO_COPY, 'gsd-check-update.cmd'];
|
|
6660
6892
|
|
|
6661
6893
|
/**
|
|
6662
6894
|
* Uninstall GSD from the specified directory for a specific runtime
|
|
@@ -6664,16 +6896,32 @@ const GSD_UNINSTALL_HOOKS = [
|
|
|
6664
6896
|
* @param {boolean} isGlobal - Whether to uninstall from global or local
|
|
6665
6897
|
* @param {string} runtime - Target runtime ('claude', 'opencode', 'codex', 'copilot')
|
|
6666
6898
|
*/
|
|
6667
|
-
function uninstall(isGlobal, runtime =
|
|
6668
|
-
|
|
6899
|
+
function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
6900
|
+
// #2093: isKilo dropped — the Kilo permission-cleanup branch below is
|
|
6901
|
+
// descriptor-driven (resolveInstallPlan(runtime).finishPermissionWriter),
|
|
6902
|
+
// not gated on this flag.
|
|
6903
|
+
// #2094: isTrae dropped — unused in this function after the
|
|
6904
|
+
// skipSharedHooksInstall fold (was never referenced here besides the
|
|
6905
|
+
// destructure). #2095: isKimi likewise dropped — kimi is now a hooks/
|
|
6906
|
+
// consumer, so its former `&& !isKimi` uninstall guards were removed.
|
|
6907
|
+
// #2096: isAntigravity dropped — unused in this function.
|
|
6908
|
+
// #2098: isCodebuddy dropped — unused in this function.
|
|
6909
|
+
// #2099: isCopilot dropped — both Copilot side-effect branches below are now
|
|
6910
|
+
// gated on resolveInstallPlan(runtime).installSurface === 'copilot-instructions'.
|
|
6911
|
+
// #2100: isWindsurf dropped — unused in this function.
|
|
6912
|
+
const { isOpencode, isCodex, isCursor, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
|
6669
6913
|
const dirName = getDirName(runtime);
|
|
6670
6914
|
|
|
6671
6915
|
// Get the target directory based on runtime and install type. Cline local
|
|
6672
6916
|
// installs write to the project root (.clinerules/ lives at the root, not in
|
|
6673
6917
|
// a .cline/ subdir), mirroring the install() path resolution (#787).
|
|
6918
|
+
// Descriptor-driven (ADR-1239 / #2090): cline local installs write to the
|
|
6919
|
+
// project root (.clinerules/ lives at the root, not in a .cline/ subdir),
|
|
6920
|
+
// mirroring the install() path resolution (#787). Folded from a hardcoded
|
|
6921
|
+
// `runtime === 'cline'` branch into hostBehaviors.localTargetIsProjectRoot.
|
|
6674
6922
|
const targetDir = isGlobal
|
|
6675
6923
|
? getGlobalConfigDir(runtime, explicitConfigDir)
|
|
6676
|
-
: runtime
|
|
6924
|
+
: _hostBehaviors(runtime).localTargetIsProjectRoot
|
|
6677
6925
|
? process.cwd()
|
|
6678
6926
|
: path.join(process.cwd(), dirName);
|
|
6679
6927
|
|
|
@@ -6690,7 +6938,13 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6690
6938
|
// #786: AGENTS.md lives at the repo root (outside targetDir) for local Copilot
|
|
6691
6939
|
// installs, so its cleanup must run even when .github (targetDir) was already
|
|
6692
6940
|
// removed — i.e. BEFORE the "target directory missing" early-return below.
|
|
6693
|
-
|
|
6941
|
+
// #2099: descriptor-driven via resolveInstallPlan(runtime).installSurface ===
|
|
6942
|
+
// 'copilot-instructions' (was hardcoded `isCopilot`). Mirrors the install-time
|
|
6943
|
+
// gate at the 'copilot-instructions' branch below (~line 10471 equivalent),
|
|
6944
|
+
// which writes this same repo-root AGENTS.md only for local ('!isGlobal')
|
|
6945
|
+
// installs — 'copilot-instructions' is unique to copilot's descriptor, so
|
|
6946
|
+
// this is byte-parity.
|
|
6947
|
+
if (resolveInstallPlan(runtime).installSurface === 'copilot-instructions' && !isGlobal) {
|
|
6694
6948
|
const agentsMdPath = path.join(process.cwd(), 'AGENTS.md');
|
|
6695
6949
|
if (fs.existsSync(agentsMdPath)) {
|
|
6696
6950
|
const content = fs.readFileSync(agentsMdPath, 'utf8');
|
|
@@ -6722,11 +6976,34 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6722
6976
|
|
|
6723
6977
|
// 1. Remove GSD commands/skills (layout-driven)
|
|
6724
6978
|
const scope = isGlobal ? 'global' : 'local';
|
|
6725
|
-
|
|
6979
|
+
// ADR-1239 / #2086: drive uninstall through the public Host-Integration Interface.
|
|
6980
|
+
// Fail-open to the engine directly if the composed-registry adapter can't load.
|
|
6981
|
+
const _uninstallAdapter = _runtimeAdapter(runtime);
|
|
6982
|
+
if (_uninstallAdapter) {
|
|
6983
|
+
_uninstallAdapter.uninstall({ configDir: targetDir, scope });
|
|
6984
|
+
} else {
|
|
6985
|
+
uninstallRuntimeArtifacts(runtime, targetDir, scope);
|
|
6986
|
+
}
|
|
6726
6987
|
removedCount++;
|
|
6727
6988
|
|
|
6989
|
+
// ADR-1239 split-home migration: the adapter/plan uninstall targets the new
|
|
6990
|
+
// `home` location (e.g. Codex → ~/.agents/skills). A user who installed
|
|
6991
|
+
// BEFORE the move and never reinstalled still has managed gsd-* skill dirs at
|
|
6992
|
+
// the old configDir-rooted location (~/.codex/skills) — remove those too so
|
|
6993
|
+
// uninstall leaves nothing behind. User-owned content is preserved.
|
|
6994
|
+
{
|
|
6995
|
+
const _movedOldSkillsDir = _resolveMovedSkillsOldDir(runtime, targetDir, scope);
|
|
6996
|
+
if (_movedOldSkillsDir) {
|
|
6997
|
+
const migrated = cleanupMovedSkillsOldLocation(_movedOldSkillsDir, 'gsd-');
|
|
6998
|
+
if (migrated > 0) {
|
|
6999
|
+
removedCount++;
|
|
7000
|
+
console.log(` ${green}✓${reset} Removed ${migrated} legacy skill dir(s) from ${_movedOldSkillsDir}`);
|
|
7001
|
+
}
|
|
7002
|
+
}
|
|
7003
|
+
}
|
|
7004
|
+
|
|
6728
7005
|
// 1a. Non-layout Codex side-effects: agent .toml files, config.toml sections, hooks.json
|
|
6729
|
-
if (
|
|
7006
|
+
if (_hostBehaviors(runtime).tomlConfigInstall) {
|
|
6730
7007
|
const codexAgentsDir = path.join(targetDir, 'agents');
|
|
6731
7008
|
if (fs.existsSync(codexAgentsDir)) {
|
|
6732
7009
|
const tomlFiles = fs.readdirSync(codexAgentsDir);
|
|
@@ -6765,8 +7042,10 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6765
7042
|
console.log(` ${green}✓${reset} Removed managed Codex SessionStart hook from hooks.json`);
|
|
6766
7043
|
}
|
|
6767
7044
|
|
|
6768
|
-
// #772: remove
|
|
6769
|
-
|
|
7045
|
+
// #772/#2088: remove every managed Codex extended hook-event registration.
|
|
7046
|
+
// Shares CODEX_EXTENDED_HOOK_EVENTS with the install loop — removal set ==
|
|
7047
|
+
// registration set, so no managed event is ever orphaned.
|
|
7048
|
+
for (const eventName of CODEX_EXTENDED_HOOK_EVENTS) {
|
|
6770
7049
|
const eventCleanup = removeCodexHooksJsonEvent(targetDir, eventName);
|
|
6771
7050
|
if (eventCleanup.changed) {
|
|
6772
7051
|
removedCount++;
|
|
@@ -6775,8 +7054,84 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6775
7054
|
}
|
|
6776
7055
|
}
|
|
6777
7056
|
|
|
7057
|
+
// 1a-kimi. Non-layout Kimi side-effect (#2095 EoS/kimi Upgrade 1): kimi's
|
|
7058
|
+
// native config.toml lives outside targetDir entirely (resolveKimiHooksTomlDir
|
|
7059
|
+
// resolves ~/.kimi, a sibling of targetDir's ~/.config/agents), so its
|
|
7060
|
+
// cleanup can't be driven by anything under targetDir the way every other
|
|
7061
|
+
// hook surface above is.
|
|
7062
|
+
if (resolveInstallPlan(runtime).hooksSurface === 'kimi-hooks-toml') {
|
|
7063
|
+
const kimiHooksRoot = resolveKimiHooksTomlDir();
|
|
7064
|
+
const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
|
|
7065
|
+
const kimiHooksCleanup = removeKimiHooksToml(kimiHooksTomlPath);
|
|
7066
|
+
if (kimiHooksCleanup.changed) {
|
|
7067
|
+
removedCount++;
|
|
7068
|
+
console.log(` ${green}✓${reset} Removed GSD hooks from ${kimiHooksTomlPath}`);
|
|
7069
|
+
}
|
|
7070
|
+
|
|
7071
|
+
// Kimi's shared hook scripts + CommonJS package.json marker are installed
|
|
7072
|
+
// into this SAME ~/.kimi root (installSharedHooksBundle, install()'s
|
|
7073
|
+
// kimi-hooks-toml branch) rather than under targetDir — mirror steps "4.
|
|
7074
|
+
// Remove GSD hooks" / "5. Remove GSD package.json" below, but scoped to
|
|
7075
|
+
// kimiHooksRoot. ~/.kimi is Kimi's own native config home (shared space —
|
|
7076
|
+
// may hold the user's real config.toml/providers), so only the exact
|
|
7077
|
+
// GSD-owned filenames are removed, and directories are pruned only if left
|
|
7078
|
+
// empty by that removal.
|
|
7079
|
+
const kimiHooksDir = path.join(kimiHooksRoot, 'hooks');
|
|
7080
|
+
if (fs.existsSync(kimiHooksDir)) {
|
|
7081
|
+
let kimiHookCount = 0;
|
|
7082
|
+
for (const hook of GSD_UNINSTALL_HOOKS) {
|
|
7083
|
+
const hookPath = path.join(kimiHooksDir, hook);
|
|
7084
|
+
if (fs.existsSync(hookPath)) {
|
|
7085
|
+
fs.unlinkSync(hookPath);
|
|
7086
|
+
kimiHookCount++;
|
|
7087
|
+
}
|
|
7088
|
+
}
|
|
7089
|
+
if (kimiHookCount > 0) {
|
|
7090
|
+
removedCount++;
|
|
7091
|
+
console.log(` ${green}✓${reset} Removed ${kimiHookCount} GSD hooks from ${kimiHooksDir}`);
|
|
7092
|
+
}
|
|
7093
|
+
|
|
7094
|
+
const kimiHooksLibDir = path.join(kimiHooksDir, 'lib');
|
|
7095
|
+
if (fs.existsSync(kimiHooksLibDir)) {
|
|
7096
|
+
let removedKimiLibFiles = 0;
|
|
7097
|
+
for (const file of GSD_HOOK_LIB_FILES) {
|
|
7098
|
+
try {
|
|
7099
|
+
fs.unlinkSync(path.join(kimiHooksLibDir, file));
|
|
7100
|
+
removedKimiLibFiles++;
|
|
7101
|
+
} catch (_) { /* best-effort */ }
|
|
7102
|
+
}
|
|
7103
|
+
try { fs.rmdirSync(kimiHooksLibDir); } catch (_) { /* not empty or other error — leave it */ }
|
|
7104
|
+
if (removedKimiLibFiles > 0) {
|
|
7105
|
+
removedCount++;
|
|
7106
|
+
console.log(` ${green}✓${reset} Removed ${removedKimiLibFiles} hooks/lib/ helper(s) from ${kimiHooksLibDir}`);
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
|
|
7110
|
+
try {
|
|
7111
|
+
if (fs.readdirSync(kimiHooksDir).length === 0) fs.rmdirSync(kimiHooksDir);
|
|
7112
|
+
} catch (_) { /* not empty — leave it */ }
|
|
7113
|
+
}
|
|
7114
|
+
|
|
7115
|
+
const kimiPkgJsonPath = path.join(kimiHooksRoot, 'package.json');
|
|
7116
|
+
if (fs.existsSync(kimiPkgJsonPath)) {
|
|
7117
|
+
try {
|
|
7118
|
+
const content = fs.readFileSync(kimiPkgJsonPath, 'utf8').trim();
|
|
7119
|
+
if (content === '{"type":"commonjs"}') {
|
|
7120
|
+
fs.unlinkSync(kimiPkgJsonPath);
|
|
7121
|
+
removedCount++;
|
|
7122
|
+
console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksRoot}`);
|
|
7123
|
+
}
|
|
7124
|
+
} catch (e) {
|
|
7125
|
+
// Ignore read errors
|
|
7126
|
+
}
|
|
7127
|
+
}
|
|
7128
|
+
}
|
|
7129
|
+
|
|
6778
7130
|
// 1b. Non-layout Copilot side-effect: copilot-instructions.md cleanup
|
|
6779
|
-
|
|
7131
|
+
// #2099: descriptor-driven via resolveInstallPlan(runtime).installSurface ===
|
|
7132
|
+
// 'copilot-instructions' (was hardcoded `isCopilot`), mirroring the same
|
|
7133
|
+
// gate used at the install-time 'copilot-instructions' branch.
|
|
7134
|
+
if (resolveInstallPlan(runtime).installSurface === 'copilot-instructions') {
|
|
6780
7135
|
const instructionsPath = path.join(targetDir, 'copilot-instructions.md');
|
|
6781
7136
|
if (fs.existsSync(instructionsPath)) {
|
|
6782
7137
|
const content = fs.readFileSync(instructionsPath, 'utf8');
|
|
@@ -6813,7 +7168,9 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6813
7168
|
// 1b-cline. Non-layout Cline side-effects (issue #787): remove the
|
|
6814
7169
|
// directory-form rules + PreToolUse hook, and strip the GSD block from the
|
|
6815
7170
|
// global cross-tool ~/.agents/AGENTS.md target.
|
|
6816
|
-
|
|
7171
|
+
// Descriptor-driven (ADR-1239 / #2090): folded from `runtime === 'cline'`
|
|
7172
|
+
// into hostBehaviors.clineRulesSurface.
|
|
7173
|
+
if (_hostBehaviors(runtime).clineRulesSurface) {
|
|
6817
7174
|
const clinerulesDir = path.join(targetDir, '.clinerules');
|
|
6818
7175
|
for (const rel of ['gsd.md', path.join('hooks', 'PreToolUse')]) {
|
|
6819
7176
|
const p = path.join(clinerulesDir, rel);
|
|
@@ -6859,17 +7216,20 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6859
7216
|
}
|
|
6860
7217
|
}
|
|
6861
7218
|
|
|
6862
|
-
// 1b-cursor.
|
|
6863
|
-
// hook entries from hooks.json and clean up the managed hook
|
|
6864
|
-
|
|
7219
|
+
// 1b-cursor. Descriptor-driven hook-bus cleanup (ADR-1239 / #2089): remove
|
|
7220
|
+
// GSD-managed hook entries from hooks.json and clean up the managed hook
|
|
7221
|
+
// scripts. Gated by the hostBehaviors.hooksJsonSurface descriptor axis, not a
|
|
7222
|
+
// hardcoded `isCursor` branch.
|
|
7223
|
+
if (_hostBehaviors(runtime).hooksJsonSurface) {
|
|
6865
7224
|
const hooksJsonCleanup = removeCursorHooksJson(targetDir);
|
|
6866
7225
|
if (hooksJsonCleanup.changed) {
|
|
6867
7226
|
removedCount++;
|
|
6868
7227
|
console.log(` ${green}✓${reset} Removed GSD-managed Cursor hooks from hooks.json`);
|
|
6869
7228
|
}
|
|
6870
|
-
// Remove
|
|
7229
|
+
// Remove all GSD-managed hook scripts (sessionStart, postToolUse, preToolUse,
|
|
7230
|
+
// stop, subagentStart, subagentStop — AC4a, #2089).
|
|
6871
7231
|
const hooksDir = path.join(targetDir, 'hooks');
|
|
6872
|
-
for (const script of
|
|
7232
|
+
for (const script of GSD_CURSOR_HOOK_SCRIPTS) {
|
|
6873
7233
|
const p = path.join(hooksDir, script);
|
|
6874
7234
|
try {
|
|
6875
7235
|
if (fs.existsSync(p)) {
|
|
@@ -6886,9 +7246,41 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6886
7246
|
} catch { /* best-effort */ }
|
|
6887
7247
|
}
|
|
6888
7248
|
|
|
7249
|
+
// 1b-windsurf. Descriptor-driven hook-bus cleanup (ADR-1239 / #2100 Stage 2):
|
|
7250
|
+
// remove GSD-managed Cascade hook entries from hooks.json and clean up the
|
|
7251
|
+
// managed hook scripts. Gated on resolveInstallPlan(runtime).hooksSurface
|
|
7252
|
+
// === 'windsurf-hooks-json' (mirrors the kimi-hooks-toml gate above) —
|
|
7253
|
+
// NOT the shared hostBehaviors.hooksJsonSurface flag the Cursor block above
|
|
7254
|
+
// uses, since that flag drives Cursor's own remove function + script list
|
|
7255
|
+
// and is not (and must not be) set for Windsurf.
|
|
7256
|
+
if (resolveInstallPlan(runtime).hooksSurface === 'windsurf-hooks-json') {
|
|
7257
|
+
const windsurfHooksJsonCleanup = removeWindsurfHooksJson(targetDir);
|
|
7258
|
+
if (windsurfHooksJsonCleanup.changed) {
|
|
7259
|
+
removedCount++;
|
|
7260
|
+
console.log(` ${green}✓${reset} Removed GSD-managed Windsurf hooks from hooks.json`);
|
|
7261
|
+
}
|
|
7262
|
+
// Remove all GSD-managed hook scripts (pre_write_code, pre_run_command).
|
|
7263
|
+
const windsurfHooksDir = path.join(targetDir, 'hooks');
|
|
7264
|
+
for (const script of GSD_WINDSURF_HOOK_SCRIPTS) {
|
|
7265
|
+
const p = path.join(windsurfHooksDir, script);
|
|
7266
|
+
try {
|
|
7267
|
+
if (fs.existsSync(p)) {
|
|
7268
|
+
fs.unlinkSync(p);
|
|
7269
|
+
removedCount++;
|
|
7270
|
+
}
|
|
7271
|
+
} catch { /* best-effort */ }
|
|
7272
|
+
}
|
|
7273
|
+
// Prune hooks/ if empty.
|
|
7274
|
+
try {
|
|
7275
|
+
if (fs.existsSync(windsurfHooksDir) && fs.readdirSync(windsurfHooksDir).length === 0) {
|
|
7276
|
+
fs.rmdirSync(windsurfHooksDir);
|
|
7277
|
+
}
|
|
7278
|
+
} catch { /* best-effort */ }
|
|
7279
|
+
}
|
|
7280
|
+
|
|
6889
7281
|
// 1c. Claude local: remove flat gsd-*.md commands from commands/ (current layout,
|
|
6890
7282
|
// #1367 fix). Also remove legacy commands/gsd/ subdirectory from prior installs.
|
|
6891
|
-
if (!isGlobal && runtime === '
|
|
7283
|
+
if (!isGlobal && _hostBehaviors(runtime).localInstallStyle === 'legacy-flat') {
|
|
6892
7284
|
const commandsDir = path.join(targetDir, 'commands');
|
|
6893
7285
|
// Remove flat gsd-*.md files (current layout after #1367 fix)
|
|
6894
7286
|
if (fs.existsSync(commandsDir)) {
|
|
@@ -6930,7 +7322,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
6930
7322
|
// removes the directory; we must preserve/restore user artifacts before that path.
|
|
6931
7323
|
// This block runs AFTER uninstallRuntimeArtifacts, so we check if the directory
|
|
6932
7324
|
// was already removed and skip if so (idempotent).
|
|
6933
|
-
if (
|
|
7325
|
+
if (_hostBehaviors(runtime).legacyCommandsGsdCleanup === true) {
|
|
6934
7326
|
// dev-preferences may have survived in skills/ as SKILL.md — nothing to do for
|
|
6935
7327
|
// that case. If a stale commands/gsd/ still exists (e.g. legacy was not removed),
|
|
6936
7328
|
// attempt migration. In practice _runLegacyUninstallCleanup removes it first,
|
|
@@ -7040,17 +7432,20 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
7040
7432
|
}
|
|
7041
7433
|
}
|
|
7042
7434
|
|
|
7043
|
-
// 4z. Remove the
|
|
7044
|
-
//
|
|
7045
|
-
//
|
|
7046
|
-
if
|
|
7047
|
-
|
|
7048
|
-
|
|
7435
|
+
// 4z. Remove the native plugin adapter (#1914, extended to Kilo by #2093).
|
|
7436
|
+
// Descriptor-driven via hostBehaviors.nativePlugin — covers every runtime
|
|
7437
|
+
// that declares the block (OpenCode, Kilo, ...), not just OpenCode. Only
|
|
7438
|
+
// GSD's own plugin file is removed; the plugins/ dir is pruned only if it
|
|
7439
|
+
// becomes empty, preserving any user-authored plugins for that host.
|
|
7440
|
+
const _np = _hostBehaviors(runtime).nativePlugin;
|
|
7441
|
+
if (_np) {
|
|
7442
|
+
const pluginsDir = path.join(targetDir, _np.dir);
|
|
7443
|
+
const pluginPath = path.join(pluginsDir, _np.file);
|
|
7049
7444
|
if (fs.existsSync(pluginPath)) {
|
|
7050
7445
|
try {
|
|
7051
7446
|
fs.unlinkSync(pluginPath);
|
|
7052
7447
|
removedCount++;
|
|
7053
|
-
console.log(` ${green}✓${reset} Removed
|
|
7448
|
+
console.log(` ${green}✓${reset} Removed native plugin adapter (${runtime})`);
|
|
7054
7449
|
} catch (_) { /* best-effort */ }
|
|
7055
7450
|
try { fs.rmdirSync(pluginsDir); } catch (_) { /* not empty — user plugins present */ }
|
|
7056
7451
|
}
|
|
@@ -7191,7 +7586,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
7191
7586
|
// to preserve any user-added allow/deny entries.
|
|
7192
7587
|
// Uses a local flag to avoid the shared `settingsModified` producing a false
|
|
7193
7588
|
// "Removed GSD permissions" message when only hooks/statusline changed.
|
|
7194
|
-
if (runtime === 'claude' && settings.permissions) {
|
|
7589
|
+
if (_hostBehaviors(runtime).permissionsSchema === 'claude' && settings.permissions) {
|
|
7195
7590
|
let permissionsModified = false;
|
|
7196
7591
|
if (Array.isArray(settings.permissions.allow)) {
|
|
7197
7592
|
const before = settings.permissions.allow.length;
|
|
@@ -7217,6 +7612,47 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
7217
7612
|
}
|
|
7218
7613
|
}
|
|
7219
7614
|
|
|
7615
|
+
// #2096 Phase B Upgrade 1 — Remove GSD-owned Antigravity permissions.allow
|
|
7616
|
+
// rules from settings.json. Symmetric to the Claude branch above: filters
|
|
7617
|
+
// only the exact GSD-owned rule strings (regenerated from the current
|
|
7618
|
+
// configDir) to preserve any user-added allow entries and all deny/ask.
|
|
7619
|
+
if (resolveInstallPlan(runtime).finishPermissionWriter === 'antigravity' && settings.permissions) {
|
|
7620
|
+
let antigravityPermissionsModified = false;
|
|
7621
|
+
if (Array.isArray(settings.permissions.allow)) {
|
|
7622
|
+
const gsdRules = new Set(buildAntigravityAllowRules(targetDir));
|
|
7623
|
+
const before = settings.permissions.allow.length;
|
|
7624
|
+
settings.permissions.allow = settings.permissions.allow.filter((e) => !gsdRules.has(e));
|
|
7625
|
+
if (settings.permissions.allow.length !== before) {
|
|
7626
|
+
antigravityPermissionsModified = true;
|
|
7627
|
+
}
|
|
7628
|
+
if (settings.permissions.allow.length === 0) {
|
|
7629
|
+
delete settings.permissions.allow;
|
|
7630
|
+
}
|
|
7631
|
+
}
|
|
7632
|
+
if (Object.keys(settings.permissions).length === 0) {
|
|
7633
|
+
delete settings.permissions;
|
|
7634
|
+
}
|
|
7635
|
+
if (antigravityPermissionsModified) {
|
|
7636
|
+
settingsModified = true;
|
|
7637
|
+
console.log(` ${green}✓${reset} Removed GSD permissions from settings.json`);
|
|
7638
|
+
}
|
|
7639
|
+
}
|
|
7640
|
+
|
|
7641
|
+
// #2097 UPGRADE 3 — Remove the MCP companion entry from settings.json for
|
|
7642
|
+
// runtimes that host MCP there (Augment), symmetric to the mcp_config.json
|
|
7643
|
+
// removal for Antigravity below. Only the GSD-owned mcpServers.gsd key is
|
|
7644
|
+
// removed — any other user-configured MCP servers are preserved.
|
|
7645
|
+
if (_hostBehaviors(runtime).mcpCompanion === 'settings-json' &&
|
|
7646
|
+
settings.mcpServers && typeof settings.mcpServers === 'object' &&
|
|
7647
|
+
settings.mcpServers.gsd !== undefined) {
|
|
7648
|
+
delete settings.mcpServers.gsd;
|
|
7649
|
+
if (Object.keys(settings.mcpServers).length === 0) {
|
|
7650
|
+
delete settings.mcpServers;
|
|
7651
|
+
}
|
|
7652
|
+
settingsModified = true;
|
|
7653
|
+
console.log(` ${green}✓${reset} Removed GSD MCP companion server from settings.json`);
|
|
7654
|
+
}
|
|
7655
|
+
|
|
7220
7656
|
if (settingsModified) {
|
|
7221
7657
|
writeSettings(settingsPath, settings);
|
|
7222
7658
|
removedCount++;
|
|
@@ -7224,7 +7660,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
7224
7660
|
}
|
|
7225
7661
|
|
|
7226
7662
|
// 6. For OpenCode, clean up permissions from opencode.json or opencode.jsonc
|
|
7227
|
-
if (
|
|
7663
|
+
if (resolveInstallPlan(runtime).finishPermissionWriter === 'opencode') {
|
|
7228
7664
|
const configPath = resolveOpencodeConfigPath(targetDir);
|
|
7229
7665
|
if (fs.existsSync(configPath)) {
|
|
7230
7666
|
try {
|
|
@@ -7265,7 +7701,9 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
7265
7701
|
}
|
|
7266
7702
|
|
|
7267
7703
|
// 7. For Kilo, clean up permissions from kilo.json or kilo.jsonc
|
|
7268
|
-
|
|
7704
|
+
// #2093: descriptor-driven via resolveInstallPlan(runtime).finishPermissionWriter,
|
|
7705
|
+
// mirroring the OpenCode branch above (was hardcoded `isKilo`).
|
|
7706
|
+
if (resolveInstallPlan(runtime).finishPermissionWriter === 'kilo') {
|
|
7269
7707
|
const configPath = resolveKiloConfigPath(targetDir);
|
|
7270
7708
|
if (fs.existsSync(configPath)) {
|
|
7271
7709
|
try {
|
|
@@ -7305,6 +7743,29 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
7305
7743
|
}
|
|
7306
7744
|
}
|
|
7307
7745
|
|
|
7746
|
+
// 8. For Antigravity, remove the MCP companion entry from mcp_config.json
|
|
7747
|
+
// (#2096 Phase B Upgrade 2). Only the GSD-owned mcpServers.gsd key is
|
|
7748
|
+
// removed — any other user-configured MCP servers are preserved.
|
|
7749
|
+
if (resolveInstallPlan(runtime).finishPermissionWriter === 'antigravity') {
|
|
7750
|
+
const mcpConfigPath = path.join(targetDir, 'mcp_config.json');
|
|
7751
|
+
if (fs.existsSync(mcpConfigPath)) {
|
|
7752
|
+
try {
|
|
7753
|
+
const mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'));
|
|
7754
|
+
if (mcpConfig && typeof mcpConfig === 'object' && mcpConfig.mcpServers && mcpConfig.mcpServers.gsd !== undefined) {
|
|
7755
|
+
delete mcpConfig.mcpServers.gsd;
|
|
7756
|
+
if (Object.keys(mcpConfig.mcpServers).length === 0) {
|
|
7757
|
+
delete mcpConfig.mcpServers;
|
|
7758
|
+
}
|
|
7759
|
+
fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
7760
|
+
removedCount++;
|
|
7761
|
+
console.log(` ${green}✓${reset} Removed GSD MCP companion server from mcp_config.json`);
|
|
7762
|
+
}
|
|
7763
|
+
} catch (e) {
|
|
7764
|
+
// Ignore JSON parse errors
|
|
7765
|
+
}
|
|
7766
|
+
}
|
|
7767
|
+
}
|
|
7768
|
+
|
|
7308
7769
|
// Remove the file manifest that the installer wrote at install time.
|
|
7309
7770
|
// Without this step the metadata file persists after uninstall (#1908).
|
|
7310
7771
|
const manifestPath = path.join(targetDir, MANIFEST_NAME);
|
|
@@ -7558,6 +8019,193 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
|
|
|
7558
8019
|
console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
|
|
7559
8020
|
}
|
|
7560
8021
|
|
|
8022
|
+
/**
|
|
8023
|
+
* Convert an absolute path to a `~`-relative form when it lives under the
|
|
8024
|
+
* user's home directory (generalizes configureKiloPermissions'
|
|
8025
|
+
* single-default-dir shorthand to Antigravity's three probed sibling config
|
|
8026
|
+
* dirs — antigravity/antigravity-ide/antigravity-cli under ~/.gemini — none of
|
|
8027
|
+
* which is a single fixed "default").
|
|
8028
|
+
*/
|
|
8029
|
+
function toTildePosixPath(absPath) {
|
|
8030
|
+
const posixPath = absPath.replace(/\\/g, '/');
|
|
8031
|
+
const posixHome = os.homedir().replace(/\\/g, '/');
|
|
8032
|
+
return posixPath === posixHome || posixPath.startsWith(`${posixHome}/`)
|
|
8033
|
+
? `~${posixPath.slice(posixHome.length)}`
|
|
8034
|
+
: posixPath;
|
|
8035
|
+
}
|
|
8036
|
+
|
|
8037
|
+
/**
|
|
8038
|
+
* Antigravity permission rule strings this installer contributes.
|
|
8039
|
+
* Schema: antigravity.google/docs/cli/permissions — "action(target)" rule
|
|
8040
|
+
* strings in permissions.{allow,deny,ask}, evaluated deny > ask > allow. GSD
|
|
8041
|
+
* only ever contributes to `allow` — never deny/ask (those are user-owned risk
|
|
8042
|
+
* decisions this installer has no business making).
|
|
8043
|
+
*/
|
|
8044
|
+
function buildAntigravityAllowRules(configDir) {
|
|
8045
|
+
const gsdPath = toTildePosixPath(configDir);
|
|
8046
|
+
return [
|
|
8047
|
+
`read_file(${gsdPath}/gsd-core/*)`,
|
|
8048
|
+
`read_file(${gsdPath}/agents/gsd-*)`,
|
|
8049
|
+
`read_file(${gsdPath}/skills/gsd-*)`,
|
|
8050
|
+
`command(node ${gsdPath}/hooks/*)`,
|
|
8051
|
+
];
|
|
8052
|
+
}
|
|
8053
|
+
|
|
8054
|
+
/**
|
|
8055
|
+
* Configure Antigravity permissions to allow reading/executing GSD's installed
|
|
8056
|
+
* tree without per-call approval prompts (#2096 Phase B Upgrade 1 — mirrors
|
|
8057
|
+
* configureKiloPermissions/configureOpencodePermissions).
|
|
8058
|
+
*
|
|
8059
|
+
* Antigravity's permission schema (antigravity.google/docs/cli/permissions) is
|
|
8060
|
+
* `{"permissions":{"allow":[...],"deny":[...],"ask":[...]}}`, living in the
|
|
8061
|
+
* SAME settings.json GSD's own hook registration writes for this runtime
|
|
8062
|
+
* (installSurface: 'settings-json', writesSharedSettings: true) — unlike
|
|
8063
|
+
* Kilo/OpenCode, which write a separate native config file. This function
|
|
8064
|
+
* re-reads the file (already containing GSD's hooks by the time finishInstall
|
|
8065
|
+
* reaches this call) and only appends to permissions.allow.
|
|
8066
|
+
*
|
|
8067
|
+
* Non-destructive + idempotent: only `permissions.allow` is touched; an
|
|
8068
|
+
* existing user permissions block (including any deny/ask entries, or
|
|
8069
|
+
* unrelated allow entries) is preserved untouched.
|
|
8070
|
+
*
|
|
8071
|
+
* @param {boolean} isGlobal - Whether this is a global or local install
|
|
8072
|
+
* @param {string|null} configDir - Resolved config directory when already known
|
|
8073
|
+
*/
|
|
8074
|
+
function configureAntigravityPermissions(isGlobal = true, configDir = null) {
|
|
8075
|
+
// For local installs, use ./.agents/ (GSD's antigravity localConfigDir)
|
|
8076
|
+
// For global installs, use the resolved ~/.gemini/antigravity{,-ide,-cli}
|
|
8077
|
+
const antigravityConfigDir = configDir || (isGlobal
|
|
8078
|
+
? getGlobalConfigDir('antigravity', explicitConfigDir)
|
|
8079
|
+
: path.join(process.cwd(), '.agents'));
|
|
8080
|
+
// Ensure config directory exists
|
|
8081
|
+
fs.mkdirSync(antigravityConfigDir, { recursive: true });
|
|
8082
|
+
|
|
8083
|
+
const configPath = path.join(antigravityConfigDir, 'settings.json');
|
|
8084
|
+
|
|
8085
|
+
// Read existing settings.json (readSettings tolerates JSONC + missing file;
|
|
8086
|
+
// returns null — and warns — only when the file exists but fails to parse).
|
|
8087
|
+
const config = readSettings(configPath);
|
|
8088
|
+
if (config === null) {
|
|
8089
|
+
// Cannot parse — DO NOT overwrite user's config (readSettings already warned).
|
|
8090
|
+
return;
|
|
8091
|
+
}
|
|
8092
|
+
|
|
8093
|
+
// Ensure permission structure exists
|
|
8094
|
+
if (!config.permissions || typeof config.permissions !== 'object' || Array.isArray(config.permissions)) {
|
|
8095
|
+
config.permissions = {};
|
|
8096
|
+
}
|
|
8097
|
+
if (!Array.isArray(config.permissions.allow)) {
|
|
8098
|
+
config.permissions.allow = [];
|
|
8099
|
+
}
|
|
8100
|
+
|
|
8101
|
+
let modified = false;
|
|
8102
|
+
for (const rule of buildAntigravityAllowRules(antigravityConfigDir)) {
|
|
8103
|
+
if (!config.permissions.allow.includes(rule)) {
|
|
8104
|
+
config.permissions.allow.push(rule);
|
|
8105
|
+
modified = true;
|
|
8106
|
+
}
|
|
8107
|
+
}
|
|
8108
|
+
|
|
8109
|
+
if (!modified) {
|
|
8110
|
+
return; // Already configured
|
|
8111
|
+
}
|
|
8112
|
+
|
|
8113
|
+
writeSettings(configPath, config);
|
|
8114
|
+
console.log(` ${green}✓${reset} Configured Antigravity permissions for GSD paths`);
|
|
8115
|
+
}
|
|
8116
|
+
|
|
8117
|
+
/**
|
|
8118
|
+
* Configure Antigravity's MCP companion server config (#2096 Phase B
|
|
8119
|
+
* Upgrade 2).
|
|
8120
|
+
*
|
|
8121
|
+
* Antigravity CLI manages MCP servers via standalone `mcp_config.json`
|
|
8122
|
+
* profiles rather than nesting them in settings.json (antigravity.google/docs/
|
|
8123
|
+
* cli/gcli-migration: "Antigravity CLI uses standalone mcp_config.json
|
|
8124
|
+
* profiles in ~/.gemini/config/ for global servers and .agents/mcp_config.json
|
|
8125
|
+
* for workspace servers"). The raw schema for the Antigravity IDE surface
|
|
8126
|
+
* itself is unpublished (docs are JS-rendered), so this follows the CLI's
|
|
8127
|
+
* documented standalone-profile convention plus the standard Gemini/MCP
|
|
8128
|
+
* `mcpServers` shape.
|
|
8129
|
+
*
|
|
8130
|
+
* BEST-EFFORT PATH CHOICE: rather than the CLI doc's separate `~/.gemini/config/`
|
|
8131
|
+
* directory for global scope, this writes `<configDir>/mcp_config.json` — the
|
|
8132
|
+
* SAME resolved configDir as settings.json (configureAntigravityPermissions) —
|
|
8133
|
+
* because (1) GSD's own antigravity configDir resolution already varies
|
|
8134
|
+
* per-user across three sibling dirs (antigravity/antigravity-ide/
|
|
8135
|
+
* antigravity-cli — see resolveAntigravityGlobalDir), so a hardcoded separate
|
|
8136
|
+
* shared path would not track that resolution, and (2) it matches the doc's
|
|
8137
|
+
* OWN workspace-scope convention exactly (`.agents/mcp_config.json`, which IS
|
|
8138
|
+
* GSD's local configDir for antigravity), keeping global/local symmetric and
|
|
8139
|
+
* consistent with the configDir-relative convention every other GSD
|
|
8140
|
+
* permission writer (kilo/opencode) already uses.
|
|
8141
|
+
*
|
|
8142
|
+
* Non-destructive + idempotent: only adds mcpServers.gsd when entirely absent;
|
|
8143
|
+
* any other user-configured mcpServers entries (or a user's OWN "gsd" override)
|
|
8144
|
+
* are preserved untouched (Hyrum's Law — mirrors OpenCode's config.mcp.gsd guard).
|
|
8145
|
+
*
|
|
8146
|
+
* @param {boolean} isGlobal - Whether this is a global or local install
|
|
8147
|
+
* @param {string|null} configDir - Resolved config directory when already known
|
|
8148
|
+
*/
|
|
8149
|
+
function configureAntigravityMcpConfig(isGlobal = true, configDir = null) {
|
|
8150
|
+
const antigravityConfigDir = configDir || (isGlobal
|
|
8151
|
+
? getGlobalConfigDir('antigravity', explicitConfigDir)
|
|
8152
|
+
: path.join(process.cwd(), '.agents'));
|
|
8153
|
+
fs.mkdirSync(antigravityConfigDir, { recursive: true });
|
|
8154
|
+
|
|
8155
|
+
const configPath = path.join(antigravityConfigDir, 'mcp_config.json');
|
|
8156
|
+
|
|
8157
|
+
let config = {};
|
|
8158
|
+
if (fs.existsSync(configPath)) {
|
|
8159
|
+
try {
|
|
8160
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
8161
|
+
config = (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {};
|
|
8162
|
+
} catch (e) {
|
|
8163
|
+
// Cannot parse - DO NOT overwrite user's config
|
|
8164
|
+
console.log(` ${yellow}⚠${reset} Could not parse mcp_config.json - skipping MCP companion config`);
|
|
8165
|
+
console.log(` ${dim}Reason: ${e.message}${reset}`);
|
|
8166
|
+
console.log(` ${dim}Your config was NOT modified. Fix the syntax manually if needed.${reset}`);
|
|
8167
|
+
return;
|
|
8168
|
+
}
|
|
8169
|
+
}
|
|
8170
|
+
|
|
8171
|
+
if (!config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)) {
|
|
8172
|
+
config.mcpServers = {};
|
|
8173
|
+
}
|
|
8174
|
+
|
|
8175
|
+
if (config.mcpServers.gsd !== undefined) {
|
|
8176
|
+
return; // Already configured (or a user-owned override) — never clobber.
|
|
8177
|
+
}
|
|
8178
|
+
|
|
8179
|
+
config.mcpServers.gsd = {
|
|
8180
|
+
command: 'npx',
|
|
8181
|
+
args: ['-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'],
|
|
8182
|
+
};
|
|
8183
|
+
|
|
8184
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
8185
|
+
console.log(` ${green}✓${reset} Configured Antigravity MCP companion server (gsd)`);
|
|
8186
|
+
}
|
|
8187
|
+
|
|
8188
|
+
/**
|
|
8189
|
+
* #2097 (ADR-1239 transport:mcp): register the GSD companion MCP server inside a
|
|
8190
|
+
* runtime's settings.json (Augment hosts MCP in settings.json.mcpServers, unlike
|
|
8191
|
+
* Antigravity's standalone mcp_config.json). Mutates the in-memory settings object
|
|
8192
|
+
* that finishInstall already writes — non-destructive + idempotent: only sets
|
|
8193
|
+
* mcpServers.gsd, preserving any user-defined servers (a user's own `gsd` override
|
|
8194
|
+
* is respected — Hyrum's Law).
|
|
8195
|
+
* @param {object} settings - the in-memory settings object finishInstall will write
|
|
8196
|
+
*/
|
|
8197
|
+
function mergeGsdMcpServerIntoSettings(settings) {
|
|
8198
|
+
if (!settings.mcpServers || typeof settings.mcpServers !== 'object' || Array.isArray(settings.mcpServers)) {
|
|
8199
|
+
settings.mcpServers = {};
|
|
8200
|
+
}
|
|
8201
|
+
if (settings.mcpServers.gsd === undefined) {
|
|
8202
|
+
settings.mcpServers.gsd = {
|
|
8203
|
+
command: 'npx',
|
|
8204
|
+
args: ['-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'],
|
|
8205
|
+
};
|
|
8206
|
+
}
|
|
8207
|
+
}
|
|
8208
|
+
|
|
7561
8209
|
/**
|
|
7562
8210
|
* Verify a directory exists and contains files
|
|
7563
8211
|
*/
|
|
@@ -7666,19 +8314,35 @@ function resolveInstallRelativePath(baseDir, relPath) {
|
|
|
7666
8314
|
/**
|
|
7667
8315
|
* Write file manifest after installation for future modification detection
|
|
7668
8316
|
*/
|
|
7669
|
-
function writeManifest(configDir, runtime =
|
|
7670
|
-
|
|
8317
|
+
function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
8318
|
+
// #2093: isKilo dropped — unused in this function.
|
|
8319
|
+
// #2094: isTrae dropped — was only used in the hooks-tracking conditional
|
|
8320
|
+
// above, now covered by hostBehaviors.skipSharedHooksInstall.
|
|
8321
|
+
// #2095: isKimi dropped — kimi is now a hooks/ consumer like every other
|
|
8322
|
+
// settings-json-adjacent runtime, so the `&& !isKimi` term below was removed.
|
|
8323
|
+
// #2096: isAntigravity dropped — unused in this function.
|
|
8324
|
+
// #2098: isCodebuddy dropped — unused in this function.
|
|
8325
|
+
// #2099: isCopilot dropped — was only used in the hooks-tracking conditional
|
|
8326
|
+
// above, now covered by hostBehaviors.skipSharedHooksInstall.
|
|
8327
|
+
// #2100: isWindsurf dropped — was only used in the hooks-tracking conditional
|
|
8328
|
+
// above, now covered by hostBehaviors.skipSharedHooksInstall.
|
|
8329
|
+
const { isOpencode, isCodex, isCursor, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
|
7671
8330
|
const gsdDir = path.join(configDir, 'gsd-core');
|
|
7672
8331
|
// #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/).
|
|
7673
8332
|
// Claude local uses flatCommandsDir instead for manifest recording.
|
|
7674
8333
|
const flatCommandsDir = path.join(configDir, 'commands');
|
|
7675
|
-
const opencodeCommandDir = path.join(configDir, 'command');
|
|
7676
|
-
// Hermes nests GSD skills under skills/gsd/ as a single category (#2841)
|
|
8334
|
+
const opencodeCommandDir = path.join(configDir, _hostBehaviors(runtime).flatCommandDir || 'command');
|
|
8335
|
+
// Hermes nests GSD skills under skills/gsd/ as a single category (#2841) —
|
|
8336
|
+
// already encoded in its layout descriptor's destSubpath ('skills/gsd').
|
|
7677
8337
|
// All other runtimes that use the Codex-style skills layout use a flat skills/ root.
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
8338
|
+
// ADR-1239 upgrade 3 (#2088): honor a skills-kind `home` override (e.g. Codex
|
|
8339
|
+
// skills -> $HOME/.agents/skills instead of configDir/skills) via the same
|
|
8340
|
+
// descriptor-driven helper used by the snapshot/rollback/verification paths,
|
|
8341
|
+
// so the manifest records what's actually on disk. _resolveSkillsRootDir already
|
|
8342
|
+
// resolves destSubpath (which includes hermes's 'skills/gsd' nesting) — do not
|
|
8343
|
+
// re-append 'gsd' or the hermes dir gets double-nested to skills/gsd/gsd.
|
|
8344
|
+
const codexSkillsDir = _resolveSkillsRootDir(runtime, configDir, options.scope === 'local' ? 'local' : 'global');
|
|
8345
|
+
const codexSkillsManifestPrefix = _hostBehaviors(runtime).skillsManifestPrefix || 'skills/';
|
|
7682
8346
|
const agentsDir = path.join(configDir, 'agents');
|
|
7683
8347
|
const manifest = {
|
|
7684
8348
|
version: pkg.version,
|
|
@@ -7704,21 +8368,21 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
7704
8368
|
// Claude local (#1367): flat gsd-*.md files at commands/ level.
|
|
7705
8369
|
// Only claude local writes gsd-*.md here; global installs don't emit commands,
|
|
7706
8370
|
// so this branch is a no-op for global (no matching files to find).
|
|
7707
|
-
if (runtime === '
|
|
8371
|
+
if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && fs.existsSync(flatCommandsDir)) {
|
|
7708
8372
|
for (const file of fs.readdirSync(flatCommandsDir)) {
|
|
7709
8373
|
if (file.startsWith('gsd-') && file.endsWith('.md')) {
|
|
7710
8374
|
manifest.files['commands/' + file] = fileHash(path.join(flatCommandsDir, file));
|
|
7711
8375
|
}
|
|
7712
8376
|
}
|
|
7713
8377
|
}
|
|
7714
|
-
if ((
|
|
8378
|
+
if (_hostBehaviors(runtime).flatCommandDir && fs.existsSync(opencodeCommandDir)) {
|
|
7715
8379
|
for (const file of fs.readdirSync(opencodeCommandDir)) {
|
|
7716
8380
|
if (file.startsWith('gsd-') && file.endsWith('.md')) {
|
|
7717
8381
|
manifest.files['command/' + file] = fileHash(path.join(opencodeCommandDir, file));
|
|
7718
8382
|
}
|
|
7719
8383
|
}
|
|
7720
8384
|
}
|
|
7721
|
-
if ((
|
|
8385
|
+
if (!_hostBehaviors(runtime).skipCodexSkillsManifest && fs.existsSync(codexSkillsDir)) {
|
|
7722
8386
|
// All runtimes (including Hermes post-#947) use the canonical 'gsd-' prefix.
|
|
7723
8387
|
const skillListPrefix = 'gsd-';
|
|
7724
8388
|
for (const skillName of listCodexSkillNames(codexSkillsDir, skillListPrefix)) {
|
|
@@ -7728,15 +8392,15 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
7728
8392
|
manifest.files[`${codexSkillsManifestPrefix}${skillName}/${rel}`] = hash;
|
|
7729
8393
|
}
|
|
7730
8394
|
}
|
|
7731
|
-
//
|
|
7732
|
-
if (
|
|
8395
|
+
// Descriptor-driven (#2090): hash the category DESCRIPTION.md so reinstall detects drift.
|
|
8396
|
+
if (_hostBehaviors(runtime).trackCategoryDescription) {
|
|
7733
8397
|
const descPath = path.join(codexSkillsDir, 'DESCRIPTION.md');
|
|
7734
8398
|
if (fs.existsSync(descPath)) {
|
|
7735
8399
|
manifest.files['skills/gsd/DESCRIPTION.md'] = fileHash(descPath);
|
|
7736
8400
|
}
|
|
7737
8401
|
}
|
|
7738
8402
|
}
|
|
7739
|
-
if (
|
|
8403
|
+
if (_hostBehaviors(runtime).agentManifestStyle === 'kimi-nested' && fs.existsSync(agentsDir)) {
|
|
7740
8404
|
const agentHashes = generateManifest(agentsDir);
|
|
7741
8405
|
for (const [rel, hash] of Object.entries(agentHashes)) {
|
|
7742
8406
|
const isRootAgent = rel === 'gsd.yaml' || rel === 'gsd.md';
|
|
@@ -7755,7 +8419,9 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
7755
8419
|
// Track Cline directory-form artifacts in the manifest (issue #787): the
|
|
7756
8420
|
// rules file and the PreToolUse hook. (~/.agents/AGENTS.md is tracked via its
|
|
7757
8421
|
// marker block, not the per-configDir manifest, since it lives outside it.)
|
|
7758
|
-
|
|
8422
|
+
// Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
|
|
8423
|
+
// hostBehaviors.clineRulesSurface.
|
|
8424
|
+
if (_hostBehaviors(runtime).clineRulesSurface) {
|
|
7759
8425
|
for (const rel of ['.clinerules/gsd.md', '.clinerules/hooks/PreToolUse']) {
|
|
7760
8426
|
const dest = path.join(configDir, rel);
|
|
7761
8427
|
if (fs.existsSync(dest)) {
|
|
@@ -7766,7 +8432,17 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
7766
8432
|
|
|
7767
8433
|
// Track hook files so saveLocalPatches() can detect user modifications
|
|
7768
8434
|
// Hooks are only installed for runtimes that use settings.json (not Codex/Copilot/Cline)
|
|
7769
|
-
|
|
8435
|
+
// Descriptor-driven (ADR-1239 / #2089+#2090): cline's exclusion is via
|
|
8436
|
+
// hostBehaviors.skipSharedHooksInstall (was hardcoded !isCline).
|
|
8437
|
+
// #2094: Trae's exclusion is likewise descriptor-driven (trae declares
|
|
8438
|
+
// skipSharedHooksInstall:true) — the redundant `&& !isTrae` was removed.
|
|
8439
|
+
// #2095: kimi is now a hooks/ consumer (native config.toml [[hooks]] bus) —
|
|
8440
|
+
// the redundant `&& !isKimi` was removed so its hook files are tracked too.
|
|
8441
|
+
// #2099: Copilot's exclusion is likewise descriptor-driven (copilot declares
|
|
8442
|
+
// skipSharedHooksInstall:true) — the redundant `&& !isCopilot` was removed.
|
|
8443
|
+
// #2100: Windsurf's exclusion is likewise descriptor-driven (windsurf declares
|
|
8444
|
+
// skipSharedHooksInstall:true) — the redundant `&& !isWindsurf` was removed.
|
|
8445
|
+
if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true) {
|
|
7770
8446
|
const hooksDir = path.join(configDir, 'hooks');
|
|
7771
8447
|
if (fs.existsSync(hooksDir)) {
|
|
7772
8448
|
// Drive from INSTALLED_HOOK_FILES (the canonical HOOKS_TO_COPY set from
|
|
@@ -7830,10 +8506,11 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
7830
8506
|
|
|
7831
8507
|
// Track the OpenCode native plugin adapter (#1914) so update/drift detection
|
|
7832
8508
|
// and uninstall can account for it.
|
|
7833
|
-
|
|
7834
|
-
|
|
8509
|
+
const _npM = _hostBehaviors(runtime).nativePlugin;
|
|
8510
|
+
if (_npM) {
|
|
8511
|
+
const pluginInstallPath = path.join(configDir, _npM.dir, _npM.file);
|
|
7835
8512
|
if (fs.existsSync(pluginInstallPath)) {
|
|
7836
|
-
manifest.files[
|
|
8513
|
+
manifest.files[`${_npM.dir}/${_npM.file}`] = fileHash(pluginInstallPath);
|
|
7837
8514
|
}
|
|
7838
8515
|
}
|
|
7839
8516
|
|
|
@@ -8117,7 +8794,7 @@ function saveLocalPatches(configDir, pristineCtx) {
|
|
|
8117
8794
|
/**
|
|
8118
8795
|
* After install, report backed-up patches for user to reapply.
|
|
8119
8796
|
*/
|
|
8120
|
-
function reportLocalPatches(configDir, runtime =
|
|
8797
|
+
function reportLocalPatches(configDir, runtime = DEFAULT_RUNTIME) {
|
|
8121
8798
|
const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
|
|
8122
8799
|
const metaPath = path.join(patchesDir, 'backup-meta.json');
|
|
8123
8800
|
if (!fs.existsSync(metaPath)) return [];
|
|
@@ -8126,15 +8803,7 @@ function reportLocalPatches(configDir, runtime = 'claude') {
|
|
|
8126
8803
|
try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; }
|
|
8127
8804
|
|
|
8128
8805
|
if (meta.files && meta.files.length > 0) {
|
|
8129
|
-
const reapplyCommand = (runtime
|
|
8130
|
-
? '/gsd-update --reapply'
|
|
8131
|
-
: runtime === 'codex'
|
|
8132
|
-
? '$gsd-update --reapply'
|
|
8133
|
-
: runtime === 'cursor'
|
|
8134
|
-
? 'gsd-update --reapply (mention the skill name)'
|
|
8135
|
-
: runtime === 'kimi'
|
|
8136
|
-
? '/skill:gsd-update --reapply'
|
|
8137
|
-
: '/gsd-update --reapply';
|
|
8806
|
+
const reapplyCommand = _hostBehaviors(runtime).reapplyCommand || '/gsd-update --reapply';
|
|
8138
8807
|
console.log('');
|
|
8139
8808
|
console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
|
|
8140
8809
|
for (const f of meta.files) {
|
|
@@ -8160,13 +8829,45 @@ function reportInstallerMigrationResult(result) {
|
|
|
8160
8829
|
}
|
|
8161
8830
|
}
|
|
8162
8831
|
|
|
8163
|
-
function install(isGlobal, runtime =
|
|
8164
|
-
|
|
8832
|
+
function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
8833
|
+
// #2093: isKilo dropped — Kilo's agent/model-override handling below reads
|
|
8834
|
+
// _hostBehaviors(runtime).frontmatterDialect === 'kilo' instead of this flag.
|
|
8835
|
+
// #2095: isKimi dropped — kimi is now a hooks/ consumer like every other
|
|
8836
|
+
// settings-json-adjacent runtime; the two `&& !isKimi` hooks-copy guards
|
|
8837
|
+
// below were removed, leaving isKimi unused in this function (the kimi
|
|
8838
|
+
// local-install-deferred branch above already reads
|
|
8839
|
+
// _hostBehaviors(runtime).localInstallDeferred instead of this flag).
|
|
8840
|
+
// #2096: isAntigravity dropped — antigravity is in
|
|
8841
|
+
// _DESCRIPTOR_AGENTS_RUNTIMES below, so its two legacy-agent-loop branches
|
|
8842
|
+
// (the path-rewrite skip and the converter dispatch) were unreachable dead
|
|
8843
|
+
// code; both were removed rather than re-gated on hostBehaviors.
|
|
8844
|
+
// #2098: isCodebuddy dropped — codebuddy is also in
|
|
8845
|
+
// _DESCRIPTOR_AGENTS_RUNTIMES below, so its legacy converter-dispatch branch
|
|
8846
|
+
// (the `isCodebuddy` arm calling convertClaudeAgentToCodebuddyAgent) was
|
|
8847
|
+
// unreachable dead code and was removed rather than re-gated.
|
|
8848
|
+
// #2099: isCopilot dropped — copilot is also in _DESCRIPTOR_AGENTS_RUNTIMES
|
|
8849
|
+
// below, so its three legacy-agent-loop branches (the path-rewrite skip,
|
|
8850
|
+
// the converter dispatch, and the .agent.md destName ternary) were
|
|
8851
|
+
// unreachable dead code and were removed rather than re-gated; the
|
|
8852
|
+
// .agent.md suffix now lives on hostBehaviors.agentFileExtension in
|
|
8853
|
+
// src/install-engine.cts, and the skipSharedHooksInstall check above no
|
|
8854
|
+
// longer needs `&& !isCopilot`.
|
|
8855
|
+
// #2100: isWindsurf dropped — its four former isWindsurf-gated branches
|
|
8856
|
+
// (legacy .devin/skills/gsd-* cleanup, the #1629 command-bodies copy, the
|
|
8857
|
+
// workflow-verification report, and the shared-hooks-install exclusion) are
|
|
8858
|
+
// now descriptor-driven via hostBehaviors.legacyDevinSkillsCleanup,
|
|
8859
|
+
// hostBehaviors.installsCommandBodiesForWorkflowDelegation,
|
|
8860
|
+
// hostBehaviors.verificationStyle === 'windsurf-workflows', and
|
|
8861
|
+
// hostBehaviors.skipSharedHooksInstall respectively; its legacy-agent-loop
|
|
8862
|
+
// converter arm was likewise unreachable dead code (windsurf is in
|
|
8863
|
+
// _DESCRIPTOR_AGENTS_RUNTIMES) and was removed above.
|
|
8864
|
+
// #2101: isZcode dropped — folded onto hostBehaviors.skipSharedHooksInstall.
|
|
8865
|
+
const { isOpencode, isCodex, isCursor, isAugment, isTrae, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
|
8165
8866
|
const plan = resolveInstallPlan(runtime);
|
|
8166
8867
|
const dirName = getDirName(runtime);
|
|
8167
8868
|
const src = path.join(__dirname, '..');
|
|
8168
8869
|
|
|
8169
|
-
if (
|
|
8870
|
+
if (_hostBehaviors(runtime).localInstallDeferred && !isGlobal) {
|
|
8170
8871
|
console.log(` ${yellow}⚠${reset} Kimi local install is deferred for Phase 2.`);
|
|
8171
8872
|
console.log(` No .kimi-code/skills or .agents/skills project artifacts were written.`);
|
|
8172
8873
|
console.log(` Project-level Kimi install semantics remain deferred.`);
|
|
@@ -8214,15 +8915,17 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8214
8915
|
};
|
|
8215
8916
|
|
|
8216
8917
|
// Get the target directory based on runtime and install type.
|
|
8217
|
-
//
|
|
8218
|
-
// lives at the root, not inside
|
|
8918
|
+
// Descriptor-driven (ADR-1239 / #2090): cline local installs write to the
|
|
8919
|
+
// project root (like Claude Code) — .clinerules lives at the root, not inside
|
|
8920
|
+
// a .cline/ subdirectory. Folded from `isCline` into
|
|
8921
|
+
// hostBehaviors.localTargetIsProjectRoot.
|
|
8219
8922
|
// #791: antigravity local installs write to .agents/ (canonical). The legacy .agent/
|
|
8220
8923
|
// directory is recognized by RUNTIME_DIRS (update-context) and _LEGACY_SCAN_SUBDIR_NAMES
|
|
8221
8924
|
// but NOT auto-removed here; legacy .agent/ gsd artifacts are recognized but not
|
|
8222
8925
|
// auto-removed on reinstall (dual-read fallback per issue #791 spec).
|
|
8223
8926
|
const targetDir = isGlobal
|
|
8224
8927
|
? getGlobalConfigDir(runtime, explicitConfigDir)
|
|
8225
|
-
:
|
|
8928
|
+
: _hostBehaviors(runtime).localTargetIsProjectRoot
|
|
8226
8929
|
? process.cwd()
|
|
8227
8930
|
: path.join(process.cwd(), dirName);
|
|
8228
8931
|
|
|
@@ -8301,7 +9004,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8301
9004
|
const isWindowsHost = process.platform === 'win32';
|
|
8302
9005
|
const pathPrefix = computePathPrefix({
|
|
8303
9006
|
isGlobal,
|
|
8304
|
-
isOpencode,
|
|
9007
|
+
isOpencode: _hostBehaviors(runtime).skipHomePrefixSubstitution === true,
|
|
8305
9008
|
isWindowsHost,
|
|
8306
9009
|
resolvedTarget,
|
|
8307
9010
|
homeDir,
|
|
@@ -8367,8 +9070,8 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8367
9070
|
// Map<filename, Buffer> — content snapshot of each pre-existing gsd-* agent file.
|
|
8368
9071
|
const codexPreInstallAgentContents = new Map();
|
|
8369
9072
|
let codexPreInstallVersionBytes = null;
|
|
8370
|
-
if (
|
|
8371
|
-
const _preSkillsDir =
|
|
9073
|
+
if (_hostBehaviors(runtime).tomlConfigInstall && !isMinimalMode(_effectiveInstallMode)) {
|
|
9074
|
+
const _preSkillsDir = _resolveSkillsRootDir(runtime, targetDir, isGlobal ? 'global' : 'local');
|
|
8372
9075
|
if (fs.existsSync(_preSkillsDir)) {
|
|
8373
9076
|
for (const entry of fs.readdirSync(_preSkillsDir, { withFileTypes: true })) {
|
|
8374
9077
|
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
|
@@ -8421,10 +9124,10 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8421
9124
|
// atomic-write temp files. It is safe to call before any writes have happened.
|
|
8422
9125
|
// The full restoreCodexSnapshot() (defined inside the config block) additionally
|
|
8423
9126
|
// handles config.toml, which is not yet touched at this point in the pipeline.
|
|
8424
|
-
const _codexPreConfigRollback = !
|
|
9127
|
+
const _codexPreConfigRollback = !_hostBehaviors(runtime).tomlConfigInstall || isMinimalMode(_effectiveInstallMode) ? null : () => {
|
|
8425
9128
|
rollbackInstallerMigrations();
|
|
8426
9129
|
// skills/gsd-* — pass 1: restore snapshot entries (may be absent if deleted mid-install).
|
|
8427
|
-
const _earlySkillsDir =
|
|
9130
|
+
const _earlySkillsDir = _resolveSkillsRootDir(runtime, targetDir, isGlobal ? 'global' : 'local');
|
|
8428
9131
|
for (const skillName of codexPreInstallSkillNames) {
|
|
8429
9132
|
const skillDirPath = path.join(_earlySkillsDir, skillName);
|
|
8430
9133
|
const fileMap = codexPreInstallSkillContents.get(skillName);
|
|
@@ -8587,7 +9290,6 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8587
9290
|
// Hermes: writeHermesCategoryDescription (not a layout kind)
|
|
8588
9291
|
// Cline global: skills emitted via layout; .clinerules still written below (#782)
|
|
8589
9292
|
// Cline local: no skills (only .clinerules) — falls through to cline-rules surface
|
|
8590
|
-
// OpenCode/Kilo: copyFlattenedCommands (frontmatter conversion not in commandsKind)
|
|
8591
9293
|
// Claude local: copyWithPathReplacement + stale-skills cleanup
|
|
8592
9294
|
|
|
8593
9295
|
// Layout-driven path for all skills-based runtimes (full and minimal modes).
|
|
@@ -8599,13 +9301,15 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8599
9301
|
// (it declares any skills/commands/agents/kimi-agents kind for this scope).
|
|
8600
9302
|
// This replaces the prior hardcoded `isCodex || isCopilot || ...` roster so a
|
|
8601
9303
|
// newly-added runtime with an artifact layout installs without a per-runtime
|
|
8602
|
-
// branch — the add-a-host tax ADR-1239 Phase B retires.
|
|
8603
|
-
//
|
|
8604
|
-
//
|
|
9304
|
+
// branch — the add-a-host tax ADR-1239 Phase B retires. OpenCode/Kilo now
|
|
9305
|
+
// route through this SAME path too: their hostBehaviors.combinedFamilyInstall
|
|
9306
|
+
// flag makes installRuntimeArtifacts (in src/install-engine.cts) delegate to
|
|
9307
|
+
// installOpencodeFamilyArtifacts for the combined commands+skills+native-plugin
|
|
9308
|
+
// install (ADR-1239 / #2087), replacing the bespoke inline block this comment
|
|
9309
|
+
// used to describe. Claude-local remains the one special-cased path
|
|
8605
9310
|
// (copyWithPathReplacement + stale-skills cleanup).
|
|
8606
9311
|
const _isSkillsRuntime = (() => {
|
|
8607
|
-
if (
|
|
8608
|
-
if (runtime === 'claude' && !isGlobal) return false; // claude-local legacy path
|
|
9312
|
+
if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && !isGlobal) return false; // legacy flat local path (descriptor-driven; #2086)
|
|
8609
9313
|
const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime];
|
|
8610
9314
|
const layout = cap && cap.runtime && cap.runtime.artifactLayout;
|
|
8611
9315
|
if (!layout) return false;
|
|
@@ -8616,7 +9320,28 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8616
9320
|
if (_isSkillsRuntime) {
|
|
8617
9321
|
// Layout-driven install for skills-based runtimes (full and minimal modes)
|
|
8618
9322
|
const scope = isGlobal ? 'global' : 'local';
|
|
8619
|
-
|
|
9323
|
+
// ADR-1239 upgrade 3 / #2088: a kind may declare an alternate install `home`
|
|
9324
|
+
// (e.g. Codex skills -> $HOME/.agents/skills) instead of the runtime's normal
|
|
9325
|
+
// configDir. Resolve the ACTUAL on-disk skills root here, descriptor-driven
|
|
9326
|
+
// (no isCodex check), so downstream sidecar-cleanup and post-install
|
|
9327
|
+
// verification look in the right place regardless of which runtime declares
|
|
9328
|
+
// an alternate home for its skills kind.
|
|
9329
|
+
const _skillsRootDir = _resolveSkillsRootDir(runtime, targetDir, scope);
|
|
9330
|
+
// ADR-1239 / #2086: drive install through the public Host-Integration Interface
|
|
9331
|
+
// (imperative adapter). The adapter delegates to the SAME installRuntimeArtifacts
|
|
9332
|
+
// engine call -> byte-identical output (gated by golden-install-parity). Fail-open
|
|
9333
|
+
// to the engine directly if the composed-registry adapter can't load.
|
|
9334
|
+
const _adapter = _runtimeAdapter(runtime);
|
|
9335
|
+
if (_adapter) {
|
|
9336
|
+
_adapter.install({
|
|
9337
|
+
configDir: targetDir,
|
|
9338
|
+
scope,
|
|
9339
|
+
resolvedProfile: _resolvedProfile,
|
|
9340
|
+
resolveAttribution: getCommitAttribution,
|
|
9341
|
+
});
|
|
9342
|
+
} else {
|
|
9343
|
+
installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile, getCommitAttribution);
|
|
9344
|
+
}
|
|
8620
9345
|
|
|
8621
9346
|
// #1326 — Codex only: remove stale agents/openai.yaml sidecars from managed
|
|
8622
9347
|
// gsd-* skill dirs. Prior installs wrote these files so Codex would show a
|
|
@@ -8624,28 +9349,46 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8624
9349
|
// index BOTH SKILL.md and the sidecar, causing each GSD skill to appear twice
|
|
8625
9350
|
// in autocomplete. Cleaning them up fixes the duplication; SKILL.md alone is
|
|
8626
9351
|
// sufficient for Codex discovery. User-owned dirs are never touched.
|
|
8627
|
-
if (
|
|
8628
|
-
cleanupCodexSkillMetadataSidecars(
|
|
9352
|
+
if (_hostBehaviors(runtime).cleanupSkillSidecars) {
|
|
9353
|
+
cleanupCodexSkillMetadataSidecars(_skillsRootDir);
|
|
9354
|
+
}
|
|
9355
|
+
|
|
9356
|
+
// ADR-1239 split-home migration: when a runtime's skills kind moved to an
|
|
9357
|
+
// alternate `home` (e.g. Codex → ~/.agents/skills), pre-move installs left
|
|
9358
|
+
// managed gsd-* skill dirs at the old configDir-rooted location
|
|
9359
|
+
// (~/.codex/skills). Reinstalling here writes the new location but would
|
|
9360
|
+
// otherwise orphan the old one — clean up the stale gsd-* dirs.
|
|
9361
|
+
{
|
|
9362
|
+
const _movedOldSkillsDir = _resolveMovedSkillsOldDir(runtime, targetDir, scope);
|
|
9363
|
+
if (_movedOldSkillsDir) {
|
|
9364
|
+
const migrated = cleanupMovedSkillsOldLocation(_movedOldSkillsDir, 'gsd-');
|
|
9365
|
+
if (migrated > 0) {
|
|
9366
|
+
console.log(` ${green}✓${reset} Migrated ${migrated} skill dir(s) off the legacy ${_movedOldSkillsDir} location`);
|
|
9367
|
+
}
|
|
9368
|
+
}
|
|
8629
9369
|
}
|
|
8630
9370
|
|
|
8631
9371
|
// #1629 Finding B: Windsurf local only — remove legacy .devin/skills/gsd-*
|
|
8632
9372
|
// dirs from pre-#1615 installs. #1615 moved Windsurf to .windsurf/workflows/
|
|
8633
9373
|
// but never cleaned up the old .devin/skills/ layout (#1085). User-owned
|
|
8634
9374
|
// content is preserved (non-gsd- dirs, gsd-dev-preferences, symlinks).
|
|
8635
|
-
|
|
9375
|
+
// Descriptor-driven (ADR-1239 / #2100): folded from `isWindsurf` into
|
|
9376
|
+
// hostBehaviors.legacyDevinSkillsCleanup (windsurf is the only runtime that
|
|
9377
|
+
// declares it, so this is byte-parity).
|
|
9378
|
+
if (_hostBehaviors(runtime).legacyDevinSkillsCleanup && !isGlobal) {
|
|
8636
9379
|
const removedCount = cleanupWindsurfLegacyDevinSkills(process.cwd());
|
|
8637
9380
|
if (removedCount > 0) {
|
|
8638
9381
|
console.log(` ${green}✓${reset} Removed ${removedCount} legacy .devin/skills/gsd-* dir(s) (pre-#1615 Windsurf layout)`);
|
|
8639
9382
|
}
|
|
8640
9383
|
}
|
|
8641
9384
|
|
|
8642
|
-
//
|
|
8643
|
-
if (
|
|
9385
|
+
// Descriptor-driven (#2090): write DESCRIPTION.md for the gsd/ category after layout install
|
|
9386
|
+
if (_hostBehaviors(runtime).writeCategoryDescription) {
|
|
8644
9387
|
writeHermesCategoryDescription(path.join(targetDir, 'skills', 'gsd'));
|
|
8645
9388
|
}
|
|
8646
9389
|
|
|
8647
9390
|
// Verify installed artifacts and report
|
|
8648
|
-
if (
|
|
9391
|
+
if (_hostBehaviors(runtime).reportSkillsCount) {
|
|
8649
9392
|
const hermesSkillsDir = path.join(targetDir, 'skills', 'gsd');
|
|
8650
9393
|
if (fs.existsSync(hermesSkillsDir)) {
|
|
8651
9394
|
// Hermes layout uses prefix: 'gsd-' (#947) — skill dirs have gsd-<stem> names
|
|
@@ -8659,7 +9402,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8659
9402
|
} else {
|
|
8660
9403
|
failures.push('skills/gsd/*');
|
|
8661
9404
|
}
|
|
8662
|
-
} else if (
|
|
9405
|
+
} else if (_hostBehaviors(runtime).verificationStyle === 'kimi') {
|
|
8663
9406
|
const skillsDir = path.join(targetDir, 'skills');
|
|
8664
9407
|
const rootAgentPath = path.join(targetDir, 'agents', 'gsd.yaml');
|
|
8665
9408
|
if (fs.existsSync(skillsDir)) {
|
|
@@ -8679,7 +9422,11 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8679
9422
|
} else {
|
|
8680
9423
|
failures.push('agents/gsd.yaml');
|
|
8681
9424
|
}
|
|
8682
|
-
|
|
9425
|
+
// Descriptor-driven (ADR-1239 / #2100): folded from `isWindsurf` into
|
|
9426
|
+
// hostBehaviors.verificationStyle === 'windsurf-workflows' (extends the
|
|
9427
|
+
// same mechanism the 'kimi' verificationStyle branch above uses; windsurf
|
|
9428
|
+
// is the only runtime that declares this value, so this is byte-parity).
|
|
9429
|
+
} else if (_hostBehaviors(runtime).verificationStyle === 'windsurf-workflows') {
|
|
8683
9430
|
if (isGlobal) {
|
|
8684
9431
|
console.log(` ${green}✓${reset} Windsurf global install skipped workflow artifacts (workspace-only)`);
|
|
8685
9432
|
} else {
|
|
@@ -8697,7 +9444,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8697
9444
|
}
|
|
8698
9445
|
}
|
|
8699
9446
|
} else {
|
|
8700
|
-
const skillsDir =
|
|
9447
|
+
const skillsDir = _skillsRootDir;
|
|
8701
9448
|
if (fs.existsSync(skillsDir)) {
|
|
8702
9449
|
const count = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
8703
9450
|
.filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length;
|
|
@@ -8725,24 +9472,9 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8725
9472
|
}
|
|
8726
9473
|
}
|
|
8727
9474
|
|
|
8728
|
-
//
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
if (fs.existsSync(commandsDir)) {
|
|
8732
|
-
const cmdCount = fs.readdirSync(commandsDir)
|
|
8733
|
-
.filter(f => f.startsWith('gsd-') && f.endsWith('.md')).length;
|
|
8734
|
-
if (cmdCount > 0) {
|
|
8735
|
-
console.log(` ${green}✓${reset} Installed ${cmdCount} slash commands to commands/`);
|
|
8736
|
-
} else {
|
|
8737
|
-
failures.push('commands/gsd-*');
|
|
8738
|
-
}
|
|
8739
|
-
} else {
|
|
8740
|
-
failures.push('commands/gsd-*');
|
|
8741
|
-
}
|
|
8742
|
-
}
|
|
8743
|
-
|
|
8744
|
-
// CodeBuddy only: also report the commands/ output (#789 — slash commands)
|
|
8745
|
-
if (isCodebuddy) {
|
|
9475
|
+
// Descriptor-driven commands/ output report (#785 — Cursor 1.6 slash commands).
|
|
9476
|
+
// Gated by hostBehaviors.reportCommandsDir, not a hardcoded `isCursor` branch (#2089).
|
|
9477
|
+
if (_hostBehaviors(runtime).reportCommandsDir) {
|
|
8746
9478
|
const commandsDir = path.join(targetDir, 'commands');
|
|
8747
9479
|
if (fs.existsSync(commandsDir)) {
|
|
8748
9480
|
const cmdCount = fs.readdirSync(commandsDir)
|
|
@@ -8757,69 +9489,23 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8757
9489
|
}
|
|
8758
9490
|
}
|
|
8759
9491
|
}
|
|
8760
|
-
} else if (
|
|
8761
|
-
// OpenCode/Kilo: flat structure in command/ directory
|
|
8762
|
-
const commandDir = path.join(targetDir, 'command');
|
|
8763
|
-
fs.mkdirSync(commandDir, { recursive: true });
|
|
8764
|
-
|
|
8765
|
-
// Copy commands/gsd/*.md as command/gsd-*.md (flatten structure)
|
|
8766
|
-
const gsdSrc = _stageSkills(_commandsDir);
|
|
8767
|
-
copyFlattenedCommands(gsdSrc, commandDir, 'gsd', pathPrefix, runtime);
|
|
8768
|
-
if (verifyInstalled(commandDir, 'command/gsd-*')) {
|
|
8769
|
-
const count = fs.readdirSync(commandDir).filter(f => f.startsWith('gsd-')).length;
|
|
8770
|
-
console.log(` ${green}✓${reset} Installed ${count} commands to command/`);
|
|
8771
|
-
} else {
|
|
8772
|
-
failures.push('command/gsd-*');
|
|
8773
|
-
}
|
|
8774
|
-
|
|
8775
|
-
// Also emit OpenCode-family skills (skills/<name>/SKILL.md). OpenCode and
|
|
8776
|
-
// Kilo support native, on-demand skills in addition to flat commands — see
|
|
8777
|
-
// resolveRuntimeArtifactLayout's opencode/kilo entries. Derive skills from
|
|
8778
|
-
// the SAME staged command set (gsdSrc) so both surfaces match exactly. (#784)
|
|
8779
|
-
const _skillCount = installOpencodeFamilySkills(runtime, targetDir, gsdSrc, pathPrefix, getCommitAttribution);
|
|
8780
|
-
if (_skillCount > 0) {
|
|
8781
|
-
console.log(` ${green}✓${reset} Installed ${_skillCount} skills to skills/`);
|
|
8782
|
-
} else {
|
|
8783
|
-
failures.push('skills/gsd-*');
|
|
8784
|
-
}
|
|
8785
|
-
|
|
8786
|
-
// OpenCode-only: install the native plugin adapter (#1914). OpenCode
|
|
8787
|
-
// declares hooksSurface: 'none', so GSD's lifecycle hooks are never
|
|
8788
|
-
// registered as settings.json hooks the way Claude Code does — the hook
|
|
8789
|
-
// *scripts* ship to <configDir>/hooks/ but nothing invokes them. This
|
|
8790
|
-
// plugin bridges OpenCode's event bus onto those existing hook scripts
|
|
8791
|
-
// (prompt guard, read guard, injection scanner, context monitor, ...),
|
|
8792
|
-
// spawning them as subprocesses. OpenCode auto-discovers plugin files under
|
|
8793
|
-
// <configDir>/plugins/ at startup — no opencode.json registration needed
|
|
8794
|
-
// (its `plugin` array is for npm packages, not local file paths).
|
|
8795
|
-
//
|
|
8796
|
-
// The file MUST land as `.js`: OpenCode's loader globs
|
|
8797
|
-
// `{plugin,plugins}/*.{ts,js}` (verified against its source) — a `.cjs`
|
|
8798
|
-
// extension would never be discovered. The config dir carries a
|
|
8799
|
-
// `{"type":"commonjs"}` package.json (written above), so the `.js` file is
|
|
8800
|
-
// interpreted as CommonJS, matching the adapter's module.exports/require.
|
|
8801
|
-
// Kilo has no plugin surface, so this is gated to OpenCode only.
|
|
8802
|
-
if (isOpencode) {
|
|
8803
|
-
const pluginSrc = path.join(src, '.opencode', 'plugins', 'gsd-core.js');
|
|
8804
|
-
const pluginDestDir = path.join(targetDir, 'plugins');
|
|
8805
|
-
const pluginDest = path.join(pluginDestDir, 'gsd-core.js');
|
|
8806
|
-
if (fs.existsSync(pluginSrc)) {
|
|
8807
|
-
fs.mkdirSync(pluginDestDir, { recursive: true });
|
|
8808
|
-
fs.copyFileSync(pluginSrc, pluginDest);
|
|
8809
|
-
if (fs.existsSync(pluginDest)) {
|
|
8810
|
-
console.log(` ${green}✓${reset} Installed OpenCode plugin (bridges GSD hooks)`);
|
|
8811
|
-
} else {
|
|
8812
|
-
failures.push('plugins/gsd-core.js');
|
|
8813
|
-
}
|
|
8814
|
-
} else {
|
|
8815
|
-
failures.push('plugins/gsd-core.js');
|
|
8816
|
-
}
|
|
8817
|
-
}
|
|
8818
|
-
} else if (isCline) {
|
|
9492
|
+
} else if (_hostBehaviors(runtime).localCommandsViaRules) {
|
|
8819
9493
|
// Cline local install: rules-based only — commands are embedded in .clinerules (generated below).
|
|
8820
9494
|
// No skills/commands directory needed for local installs.
|
|
8821
9495
|
// Global installs are handled above by _isSkillsRuntime (#782).
|
|
9496
|
+
// Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
|
|
9497
|
+
// hostBehaviors.localCommandsViaRules.
|
|
8822
9498
|
console.log(` ${green}✓${reset} Cline: commands will be available via .clinerules`);
|
|
9499
|
+
} else if (_hostBehaviors(runtime).pluginOnlyInstall) {
|
|
9500
|
+
// pi (ADR-1239 / #2102 Stage 1): plugin-only install — pi's /gsd command is
|
|
9501
|
+
// registered programmatically by the native extension (pi/gsd.cjs →
|
|
9502
|
+
// extensions/gsd.cjs, staged separately below) and dispatches in-process
|
|
9503
|
+
// through the embedded gsd-core command-routing hub. pi has no host-read
|
|
9504
|
+
// markdown surface (unlike Claude/OpenCode/etc., which scan commands/ or
|
|
9505
|
+
// command/ directories), so writing flat gsd-<cmd>.md files here would be
|
|
9506
|
+
// dead weight the extension never reads. Skip the flat-commands fallback
|
|
9507
|
+
// entirely for pluginOnlyInstall runtimes.
|
|
9508
|
+
console.log(` ${green}✓${reset} pi: /gsd registered via native extension (no declarative command files)`);
|
|
8823
9509
|
} else {
|
|
8824
9510
|
// Claude Code local: flat gsd-<cmd>.md layout — Claude Code registers
|
|
8825
9511
|
// commands from .claude/commands/ using the filename stem as the command
|
|
@@ -8890,6 +9576,18 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8890
9576
|
}
|
|
8891
9577
|
}
|
|
8892
9578
|
|
|
9579
|
+
// Native-extension/plugin staging for runtimes OUTSIDE the layout-driven
|
|
9580
|
+
// _isSkillsRuntime branch above (ADR-1239 / #2102 Stage 1: pi). OpenCode/Kilo
|
|
9581
|
+
// already get their nativePlugin file from installOpencodeFamilyArtifacts
|
|
9582
|
+
// (called inside the _isSkillsRuntime branch, since both declare a non-empty
|
|
9583
|
+
// artifactLayout) — guard on `!_isSkillsRuntime` so this standalone call never
|
|
9584
|
+
// double-stages their plugin file. A runtime like pi, whose artifactLayout is
|
|
9585
|
+
// intentionally empty for both scopes (`_isSkillsRuntime` is false), still
|
|
9586
|
+
// needs its declared hostBehaviors.nativePlugin file copied into targetDir.
|
|
9587
|
+
if (!_isSkillsRuntime && _hostBehaviors(runtime).nativePlugin) {
|
|
9588
|
+
_installNativePluginIfDeclared(runtime, targetDir, _hostBehaviors(runtime), src);
|
|
9589
|
+
}
|
|
9590
|
+
|
|
8893
9591
|
// Copy gsd-core skill with path replacement
|
|
8894
9592
|
// Preserve user-generated files before the wipe-and-copy so they survive re-install
|
|
8895
9593
|
const skillSrc = path.join(src, 'gsd-core');
|
|
@@ -8916,11 +9614,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8916
9614
|
// other runtime/scope deploys commands/gsd, so its walk-up already resolves
|
|
8917
9615
|
// and needs no marker. Guarded on source presence so a half-published
|
|
8918
9616
|
// package never writes a dangling marker.
|
|
8919
|
-
if (runtime
|
|
9617
|
+
if (_hostBehaviors(runtime).sourceMarkerFile && isGlobal) {
|
|
8920
9618
|
const gsdSourceCommands = path.join(src, 'commands', 'gsd');
|
|
8921
9619
|
if (fs.existsSync(gsdSourceCommands)) {
|
|
8922
9620
|
try {
|
|
8923
|
-
|
|
9621
|
+
// ADR-1239 Phase B write-confinement: the descriptor-sourced marker filename
|
|
9622
|
+
// must resolve under targetDir (parity with the other descriptor-driven writes).
|
|
9623
|
+
const _markerPath = assertDestWithinConfigHome(targetDir, _hostBehaviors(runtime).sourceMarkerFile);
|
|
9624
|
+
fs.writeFileSync(_markerPath, gsdSourceCommands + '\n', 'utf8');
|
|
8924
9625
|
} catch (err) {
|
|
8925
9626
|
// Non-fatal: install proceeds. But on the Claude-global layout walk-up
|
|
8926
9627
|
// also fails (no commands/gsd source tree), so a silent write failure
|
|
@@ -8938,7 +9639,11 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8938
9639
|
// this copy, every /gsd-* workflow in Cascade references a missing file and the LLM
|
|
8939
9640
|
// cannot execute the command body. Surfaced by the #1629 regression test after the
|
|
8940
9641
|
// original adversarial review of #1622 missed it.
|
|
8941
|
-
|
|
9642
|
+
// Descriptor-driven (ADR-1239 / #2100): folded from `isWindsurf` into
|
|
9643
|
+
// hostBehaviors.installsCommandBodiesForWorkflowDelegation (windsurf is the
|
|
9644
|
+
// only runtime that declares it, so this is byte-parity — the #1629 fix
|
|
9645
|
+
// itself is unchanged).
|
|
9646
|
+
if (_hostBehaviors(runtime).installsCommandBodiesForWorkflowDelegation && !isGlobal) {
|
|
8942
9647
|
const commandsSrc = path.join(src, 'commands', 'gsd');
|
|
8943
9648
|
const commandsDest = path.join(skillDest, 'commands', 'gsd');
|
|
8944
9649
|
if (fs.existsSync(commandsSrc)) {
|
|
@@ -8994,28 +9699,39 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
8994
9699
|
// Trivial group (cursor/windsurf/augment/trae/codebuddy) cut over together.
|
|
8995
9700
|
// #1575: copilot and antigravity cut over — copilot gets .agent.md filename
|
|
8996
9701
|
// rename via _copyStaged(runtime); antigravity uses scope-aware converter.
|
|
9702
|
+
// #2092 Phase B Upgrade 1: qwen cut over — native .qwen/agents/*.md subagent
|
|
9703
|
+
// projection via convertClaudeAgentToQwenAgent. Without this exclusion the
|
|
9704
|
+
// legacy inline loop below deletes+re-copies qwen's agents RAW (bypassing the
|
|
9705
|
+
// new converter entirely, since qwen has no dedicated branch in the inline
|
|
9706
|
+
// loop's if/else-if chain — it would silently fall through to the generic
|
|
9707
|
+
// brandingRewrites-only branch).
|
|
8997
9708
|
// cline remains excluded: rules-only local branch + local/global complication
|
|
8998
9709
|
// that the descriptor-driven path does not handle correctly.
|
|
8999
|
-
const _DESCRIPTOR_AGENTS_RUNTIMES = new Set(['cursor', 'windsurf', 'augment', 'trae', 'codebuddy', 'copilot', 'antigravity']);
|
|
9710
|
+
const _DESCRIPTOR_AGENTS_RUNTIMES = new Set(['cursor', 'windsurf', 'augment', 'trae', 'codebuddy', 'copilot', 'antigravity', 'qwen', 'kimi']);
|
|
9000
9711
|
|
|
9001
9712
|
// Always remove stale gsd-* agents first so re-installing with
|
|
9002
9713
|
// `--minimal` actually shrinks a previously-full install.
|
|
9003
9714
|
// For Codex this also covers per-agent `.toml` files alongside the `.md`
|
|
9004
9715
|
// sources so a full → minimal switch doesn't leave stale registrations.
|
|
9005
|
-
// Skipped for descriptor-agent runtimes (installRuntimeArtifacts prunes)
|
|
9006
|
-
|
|
9716
|
+
// Skipped for descriptor-agent runtimes (installRuntimeArtifacts prunes) and
|
|
9717
|
+
// for pluginOnlyInstall runtimes (pi, ADR-1239 / #2102 Stage 1 — no agents/
|
|
9718
|
+
// dir is ever written for them, see the leading branch below).
|
|
9719
|
+
if (!_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime) && !_hostBehaviors(runtime).pluginOnlyInstall && fs.existsSync(agentsDest)) {
|
|
9007
9720
|
for (const file of fs.readdirSync(agentsDest)) {
|
|
9008
9721
|
if (
|
|
9009
9722
|
file.startsWith('gsd-') &&
|
|
9010
|
-
(file.endsWith('.md') || (
|
|
9723
|
+
(file.endsWith('.md') || (_hostBehaviors(runtime).agentTomlFiles && file.endsWith('.toml')))
|
|
9011
9724
|
) {
|
|
9012
9725
|
fs.unlinkSync(path.join(agentsDest, file));
|
|
9013
9726
|
}
|
|
9014
9727
|
}
|
|
9015
9728
|
}
|
|
9016
9729
|
|
|
9017
|
-
if (
|
|
9018
|
-
|
|
9730
|
+
if (_hostBehaviors(runtime).pluginOnlyInstall) {
|
|
9731
|
+
// pi (ADR-1239 / #2102 Stage 1): programmatic dispatch has no named-dispatch
|
|
9732
|
+
// subagent toolkit (dispatch.subagentToolkit: "undocumented", no Agent-tool
|
|
9733
|
+
// equivalent) and no host-read markdown surface — skip writing agents/ entirely.
|
|
9734
|
+
console.log(` ${green}✓${reset} pi: no subagent files (programmatic dispatch, no named-dispatch toolkit)`);
|
|
9019
9735
|
} else if (_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime)) {
|
|
9020
9736
|
// installRuntimeArtifacts already wrote agents + handles stale-file cleanup
|
|
9021
9737
|
// via its own prune pass. No further action needed.
|
|
@@ -9025,7 +9741,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9025
9741
|
// Without stripping them here, a full → minimal reinstall would leave the
|
|
9026
9742
|
// runtime advertising the old full agent surface even though the agent
|
|
9027
9743
|
// files are gone. Reuse the same helper that powers `--uninstall`.
|
|
9028
|
-
if (
|
|
9744
|
+
if (_hostBehaviors(runtime).tomlConfigInstall) {
|
|
9029
9745
|
const codexConfigPath = path.join(targetDir, 'config.toml');
|
|
9030
9746
|
if (fs.existsSync(codexConfigPath)) {
|
|
9031
9747
|
const existing = fs.readFileSync(codexConfigPath, 'utf8');
|
|
@@ -9052,15 +9768,21 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9052
9768
|
const bareDirRegex = /~\/\.claude\b/g;
|
|
9053
9769
|
const bareHomeDirRegex = /\$HOME\/\.claude\b/g;
|
|
9054
9770
|
const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9771
|
+
// #2096: `&& !isAntigravity` dropped — antigravity is in
|
|
9772
|
+
// _DESCRIPTOR_AGENTS_RUNTIMES above, so this whole branch is already
|
|
9773
|
+
// unreachable for it; the path-rewrite skip for antigravity now lives
|
|
9774
|
+
// in the descriptor-driven `applyAgentPathRewrites` (hostBehaviors.noPathRewrite).
|
|
9775
|
+
// #2099: `if (!isCopilot)` guard dropped — copilot is ALSO in
|
|
9776
|
+
// _DESCRIPTOR_AGENTS_RUNTIMES (line ~9564 above), so this whole
|
|
9777
|
+
// `else if (fs.existsSync(agentsSrc))` branch is unreachable for it;
|
|
9778
|
+
// isCopilot was therefore always false here, making the guard a no-op.
|
|
9779
|
+
content = content.replace(dirRegex, pathPrefix);
|
|
9780
|
+
content = content.replace(homeDirRegex, pathPrefix);
|
|
9781
|
+
content = content.replace(bareDirRegex, normalizedPathPrefix);
|
|
9782
|
+
content = content.replace(bareHomeDirRegex, normalizedPathPrefix);
|
|
9061
9783
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
9062
9784
|
// Convert frontmatter for runtime compatibility (agents need different handling)
|
|
9063
|
-
if (
|
|
9785
|
+
if (_hostBehaviors(runtime).frontmatterDialect === 'opencode') {
|
|
9064
9786
|
// Resolve per-agent model for OpenCode agents.
|
|
9065
9787
|
// Precedence: model_overrides[agent] > model_profile_overrides.opencode.<tier> > omit.
|
|
9066
9788
|
// model_overrides (#2256): explicit per-agent override, highest precedence.
|
|
@@ -9079,34 +9801,53 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9079
9801
|
}
|
|
9080
9802
|
}
|
|
9081
9803
|
content = convertClaudeToOpencodeFrontmatter(content, { isAgent: true, modelOverride: _ocModelOverride });
|
|
9082
|
-
} else if (
|
|
9083
|
-
|
|
9084
|
-
|
|
9804
|
+
} else if (_hostBehaviors(runtime).frontmatterDialect === 'kilo') {
|
|
9805
|
+
// Resolve per-agent model for Kilo agents (#2093 UPGRADE 2; Kilo is an
|
|
9806
|
+
// OpenCode fork with the same static-frontmatter model constraint).
|
|
9807
|
+
// Precedence: model_overrides[agent] > model_profile_overrides.kilo.<tier> > omit.
|
|
9808
|
+
// model_overrides (#2256): explicit per-agent override, highest precedence.
|
|
9809
|
+
// model_profile_overrides (#2794): tier-based runtime resolver, same parity as OpenCode.
|
|
9810
|
+
const _kiloAgentName = entry.name.replace(/\.md$/, '');
|
|
9811
|
+
const _kiloModelOverrides = readGsdEffectiveModelOverrides(targetDir);
|
|
9812
|
+
let _kiloModelOverride = _kiloModelOverrides?.[_kiloAgentName] || null;
|
|
9813
|
+
if (!_kiloModelOverride) {
|
|
9814
|
+
// Fall back to tier-based resolution via model_profile_overrides.kilo.<tier>.
|
|
9815
|
+
const _kiloRuntimeResolver = readGsdRuntimeProfileResolver(targetDir);
|
|
9816
|
+
if (_kiloRuntimeResolver) {
|
|
9817
|
+
const _kiloEntry = _kiloRuntimeResolver.resolve(_kiloAgentName);
|
|
9818
|
+
if (_kiloEntry?.model) {
|
|
9819
|
+
_kiloModelOverride = _kiloEntry.model;
|
|
9820
|
+
}
|
|
9821
|
+
}
|
|
9822
|
+
}
|
|
9823
|
+
content = convertClaudeToKiloFrontmatter(content, { isAgent: true, modelOverride: _kiloModelOverride });
|
|
9824
|
+
} else if (_hostBehaviors(runtime).frontmatterDialect === 'codex') {
|
|
9085
9825
|
content = convertClaudeAgentToCodexAgent(content);
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
} else if (
|
|
9099
|
-
|
|
9100
|
-
|
|
9826
|
+
// #2099: `else if (isCopilot)` arm dropped — copilot is unreachable
|
|
9827
|
+
// here (see the isCopilot-guard-drop comment above); its content
|
|
9828
|
+
// conversion is applied pre-staging via the descriptor's
|
|
9829
|
+
// artifactLayout.converter (runtime-artifact-layout.cts), independent
|
|
9830
|
+
// of this legacy loop.
|
|
9831
|
+
// #2100: `else if (isWindsurf)` arm dropped — windsurf is ALSO in
|
|
9832
|
+
// _DESCRIPTOR_AGENTS_RUNTIMES (line ~9575 above), so this whole
|
|
9833
|
+
// `else if (fs.existsSync(agentsSrc))` branch is unreachable for it;
|
|
9834
|
+
// isWindsurf was therefore always false here, making the arm dead.
|
|
9835
|
+
// Its content conversion is applied pre-staging via the descriptor's
|
|
9836
|
+
// artifactLayout.converter (convertClaudeAgentToWindsurfAgent),
|
|
9837
|
+
// independent of this legacy loop.
|
|
9838
|
+
} else if (_hostBehaviors(runtime).frontmatterDialect === 'cline') {
|
|
9839
|
+
// Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
|
|
9840
|
+
// hostBehaviors.frontmatterDialect === 'cline'.
|
|
9101
9841
|
content = convertClaudeAgentToClineAgent(content);
|
|
9102
|
-
} else if (
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
content = content.replace(
|
|
9109
|
-
content = content.replace(
|
|
9842
|
+
} else if (_hostBehaviors(runtime).brandingRewrites) {
|
|
9843
|
+
// Descriptor-driven (ADR-1239 / #2092): folded from separate
|
|
9844
|
+
// `isQwen` / hermes-hardcoded branches into a single read of
|
|
9845
|
+
// runtime.hostBehaviors.brandingRewrites (qwen -> QWEN.md/Qwen
|
|
9846
|
+
// Code/.qwen/, hermes -> HERMES.md/Hermes Agent/.hermes/).
|
|
9847
|
+
const _b = _hostBehaviors(runtime).brandingRewrites;
|
|
9848
|
+
content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
|
|
9849
|
+
content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
|
|
9850
|
+
content = content.replace(/\.claude\//g, _b['.claude/']);
|
|
9110
9851
|
}
|
|
9111
9852
|
// #443 — Inject `effort:` into the Claude .md frontmatter ONLY.
|
|
9112
9853
|
// OpenCode/Qwen/Hermes also produce .md files but break on
|
|
@@ -9115,11 +9856,11 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9115
9856
|
// Claude Code reads per-subagent `effort:` frontmatter (anthropics/claude-code #31536).
|
|
9116
9857
|
// Injection is per-runtime at install time because the canonical source
|
|
9117
9858
|
// agents/*.md must stay runtime-safe (no effort: key in source).
|
|
9118
|
-
if (runtime
|
|
9859
|
+
if ((_hostBehaviors(runtime).agentFrontmatterExtensions || []).includes('effort')) {
|
|
9119
9860
|
const _effortCfg = readGsdEffectiveEffortConfig(targetDir);
|
|
9120
9861
|
const _agentName = entry.name.replace(/\.md$/, '');
|
|
9121
9862
|
const _universalEffort = resolveInstallTimeEffort(_effortCfg, _agentName);
|
|
9122
|
-
const _renderedEffort = _getGsdEffortCatalog().renderEffortForRuntime(
|
|
9863
|
+
const _renderedEffort = _getGsdEffortCatalog().renderEffortForRuntime(runtime, _universalEffort).value;
|
|
9123
9864
|
content = injectEffortFrontmatter(content, _renderedEffort);
|
|
9124
9865
|
const _disallowedTools = READONLY_AGENT_DISALLOWED_TOOLS[_agentName];
|
|
9125
9866
|
if (_disallowedTools) content = injectDisallowedToolsFrontmatter(content, _disallowedTools);
|
|
@@ -9131,7 +9872,12 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9131
9872
|
// shouldNormalizeHyphenNamespaceInAgentBody above. Mirrors the
|
|
9132
9873
|
// SKILL.md-body fix shipped via #3629.
|
|
9133
9874
|
content = normalizeAgentBodyForRuntime(content, runtime, readGsdCommandNames());
|
|
9134
|
-
|
|
9875
|
+
// #2099: `isCopilot ? ... : entry.name` ternary dropped — copilot is
|
|
9876
|
+
// unreachable here (see the isCopilot-guard-drop comment above), so
|
|
9877
|
+
// the ternary always evaluated to entry.name in practice; its
|
|
9878
|
+
// .agent.md suffix is applied by the descriptor-driven fold in
|
|
9879
|
+
// src/install-engine.cts (hostBehaviors.agentFileExtension).
|
|
9880
|
+
const destName = entry.name;
|
|
9135
9881
|
fs.writeFileSync(path.join(agentsDest, destName), content);
|
|
9136
9882
|
}
|
|
9137
9883
|
}
|
|
@@ -9163,19 +9909,41 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9163
9909
|
failures.push('VERSION');
|
|
9164
9910
|
}
|
|
9165
9911
|
|
|
9166
|
-
|
|
9912
|
+
// Reusable: copy hooks/dist/ + hooks/lib/ into destRootDir, writing the
|
|
9913
|
+
// CommonJS package.json marker alongside them. Used below for the generic
|
|
9914
|
+
// configDir install path (guarded by hostBehaviors.skipSharedHooksInstall),
|
|
9915
|
+
// and — since #2095 — for Kimi's OWN native hook-install root (~/.kimi,
|
|
9916
|
+
// resolved by resolveKimiHooksTomlDir), a directory entirely separate from
|
|
9917
|
+
// Kimi's configDir/agents-root. Kimi's contract forbids hooks/ or
|
|
9918
|
+
// package.json under its generic Agent-Skills root (see
|
|
9919
|
+
// capabilities/kimi/capability.json hostBehaviors.skipSharedHooksInstall
|
|
9920
|
+
// and the kimi-hooks-toml branch further below), so its shared-hooks bundle
|
|
9921
|
+
// is installed into its own root via this same helper instead.
|
|
9922
|
+
// Returns false when hooks/dist/ exists but failed to verify post-copy (a
|
|
9923
|
+
// genuine failure the caller should surface); true otherwise (including
|
|
9924
|
+
// when hooks/dist/ is absent from the package — nothing to verify).
|
|
9925
|
+
function installSharedHooksBundle(destRootDir) {
|
|
9926
|
+
// destRootDir already exists for the generic call site (targetDir — created
|
|
9927
|
+
// earlier in install() by the skills/agents writes above). It does NOT yet
|
|
9928
|
+
// exist for kimi's call site (~/.kimi, resolved by resolveKimiHooksTomlDir):
|
|
9929
|
+
// a fresh install has never created that dir before. mkdirSync recursive is
|
|
9930
|
+
// a safe no-op when the dir is already present.
|
|
9931
|
+
fs.mkdirSync(destRootDir, { recursive: true });
|
|
9932
|
+
|
|
9167
9933
|
// Write package.json to force CommonJS mode for GSD scripts
|
|
9168
9934
|
// Prevents "require is not defined" errors when project has "type": "module"
|
|
9169
9935
|
// Node.js walks up looking for package.json - this stops inheritance from project
|
|
9170
|
-
const pkgJsonDest = path.join(
|
|
9936
|
+
const pkgJsonDest = path.join(destRootDir, 'package.json');
|
|
9171
9937
|
fs.writeFileSync(pkgJsonDest, '{"type":"commonjs"}\n');
|
|
9172
9938
|
console.log(` ${green}✓${reset} Wrote package.json (CommonJS mode)`);
|
|
9173
9939
|
|
|
9940
|
+
let hooksOk = true;
|
|
9941
|
+
|
|
9174
9942
|
// Copy hooks from dist/ (bundled with dependencies)
|
|
9175
9943
|
// Template paths for the target runtime (replaces '.claude' with correct config dir)
|
|
9176
9944
|
const hooksSrc = path.join(src, 'hooks', 'dist');
|
|
9177
9945
|
if (fs.existsSync(hooksSrc)) {
|
|
9178
|
-
const hooksDest = path.join(
|
|
9946
|
+
const hooksDest = path.join(destRootDir, 'hooks');
|
|
9179
9947
|
fs.mkdirSync(hooksDest, { recursive: true });
|
|
9180
9948
|
const hookEntries = fs.readdirSync(hooksSrc);
|
|
9181
9949
|
const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
|
|
@@ -9188,13 +9956,15 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9188
9956
|
content = content.replace(/'\.claude'/g, configDirReplacement);
|
|
9189
9957
|
content = content.replace(/\/\.claude\//g, `/${getDirName(runtime)}/`);
|
|
9190
9958
|
content = content.replace(/\.claude\//g, `${getDirName(runtime)}/`);
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9959
|
+
// Descriptor-driven (ADR-1239 / #2092): folded from separate
|
|
9960
|
+
// `isQwen` / hermes-hardcoded branches into a single read of
|
|
9961
|
+
// runtime.hostBehaviors.brandingRewrites. This site only
|
|
9962
|
+
// rewrites the two brand-name keys (no `.claude/` here — the
|
|
9963
|
+
// config-dir replace above already handled path fragments).
|
|
9964
|
+
const _b2 = _hostBehaviors(runtime).brandingRewrites;
|
|
9965
|
+
if (_b2) {
|
|
9966
|
+
content = content.replace(/CLAUDE\.md/g, _b2['CLAUDE.md']);
|
|
9967
|
+
content = content.replace(/\bClaude Code\b/g, _b2['Claude Code']);
|
|
9198
9968
|
}
|
|
9199
9969
|
// #376: rewrite gsd: → gsd- for hyphen-namespace runtimes
|
|
9200
9970
|
if (shouldNormalizeHyphenNamespaceInAgentBody(runtime)) {
|
|
@@ -9247,25 +10017,65 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9247
10017
|
}
|
|
9248
10018
|
}
|
|
9249
10019
|
} else {
|
|
9250
|
-
|
|
9251
|
-
}
|
|
10020
|
+
hooksOk = false;
|
|
10021
|
+
}
|
|
10022
|
+
}
|
|
10023
|
+
|
|
10024
|
+
// Gate hooks/lib/ install on the same set of runtimes that receive hooks/.
|
|
10025
|
+
// Codex/Copilot/Cursor/Windsurf/Trae/Cline/Kilo do not use the shared
|
|
10026
|
+
// hooks/lib/ helpers (Cursor uses standalone .js hook scripts registered
|
|
10027
|
+
// via hooks.json — gated descriptor-driven via
|
|
10028
|
+
// hostBehaviors.skipSharedHooksInstall, #2089; Cline likewise #2090; Kilo
|
|
10029
|
+
// likewise #2093; Trae likewise #2094; Codex uses hooks.json directly;
|
|
10030
|
+
// the others skip hooks entirely); Kilo and ZCode also skip hooks entirely
|
|
10031
|
+
// (hooksSurface:'none' with no plugin surface — #1821). None of the
|
|
10032
|
+
// excluded runtimes must receive the hooks/lib/ helpers — otherwise the
|
|
10033
|
+
// Codex comment downstream ("we deliberately do *not* copy hooks/lib/ for
|
|
10034
|
+
// Codex") is contradicted in practice. (Gating lives at the call sites
|
|
10035
|
+
// below; this helper itself only checks source presence.)
|
|
10036
|
+
const hooksLibSrc = path.join(src, 'hooks', 'lib');
|
|
10037
|
+
if (fs.existsSync(hooksLibSrc)) {
|
|
10038
|
+
const hooksLibDest = path.join(destRootDir, 'hooks', 'lib');
|
|
10039
|
+
fs.mkdirSync(hooksLibDest, { recursive: true });
|
|
10040
|
+
copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES);
|
|
10041
|
+
console.log(` ${green}✓${reset} Installed hooks/lib/ helpers (git-cmd, graphify-rebuild, ...)`);
|
|
10042
|
+
}
|
|
10043
|
+
|
|
10044
|
+
return hooksOk;
|
|
10045
|
+
}
|
|
10046
|
+
|
|
10047
|
+
// #1821: Kilo and ZCode declare hooksSurface:'none' AND have no plugin surface,
|
|
10048
|
+
// so the staged hook scripts are dead weight for them — exclude both here.
|
|
10049
|
+
// OpenCode also declares hooksSurface:'none' but is deliberately NOT excluded:
|
|
10050
|
+
// its native plugin adapter (#1914, installed above under plugins/gsd-core.js)
|
|
10051
|
+
// spawns the staged hooks/*.js scripts via OpenCode's event bus and needs both
|
|
10052
|
+
// them and the CommonJS package.json marker written below.
|
|
10053
|
+
// #2089: Cursor's exclusion is now descriptor-driven via
|
|
10054
|
+
// hostBehaviors.skipSharedHooksInstall (was hardcoded !isCursor).
|
|
10055
|
+
// #2090: Cline's exclusion is likewise descriptor-driven (cline declares
|
|
10056
|
+
// skipSharedHooksInstall:true) — the redundant `&& !isCline` was removed.
|
|
10057
|
+
// #2093: Kilo's exclusion is likewise descriptor-driven (kilo declares
|
|
10058
|
+
// skipSharedHooksInstall:true) — the redundant `&& !isKilo` was removed.
|
|
10059
|
+
// #2094: Trae's exclusion is likewise descriptor-driven (trae declares
|
|
10060
|
+
// skipSharedHooksInstall:true) — the redundant `&& !isTrae` was removed.
|
|
10061
|
+
// #2101: ZCode's exclusion is likewise descriptor-driven (zcode declares
|
|
10062
|
+
// skipSharedHooksInstall:true) — the redundant `&& !isZcode` was removed.
|
|
10063
|
+
// #2095: Kimi's exclusion is likewise descriptor-driven (kimi declares
|
|
10064
|
+
// skipSharedHooksInstall:true) — kimi's shared hooks/ + package.json marker
|
|
10065
|
+
// are instead installed into its OWN native hook root (~/.kimi, resolved by
|
|
10066
|
+
// resolveKimiHooksTomlDir) via installSharedHooksBundle, at the
|
|
10067
|
+
// kimi-hooks-toml branch further below — never under the generic
|
|
10068
|
+
// Agent-Skills configDir GSD installs skills/agents into for kimi.
|
|
10069
|
+
// #2099: Copilot's exclusion is likewise descriptor-driven (copilot declares
|
|
10070
|
+
// skipSharedHooksInstall:true) — the redundant `&& !isCopilot` was removed.
|
|
10071
|
+
// #2100: Windsurf's exclusion is likewise descriptor-driven (windsurf declares
|
|
10072
|
+
// skipSharedHooksInstall:true) — the redundant `&& !isWindsurf` was removed.
|
|
10073
|
+
if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true) {
|
|
10074
|
+
if (!installSharedHooksBundle(targetDir)) {
|
|
10075
|
+
failures.push('hooks');
|
|
9252
10076
|
}
|
|
9253
10077
|
}
|
|
9254
10078
|
|
|
9255
|
-
// Gate hooks/lib/ install on the same runtimes that receive hooks (see line ~8702).
|
|
9256
|
-
// Codex/Copilot/Cursor/Windsurf/Trae/Cline do not use the shared hooks/lib/ helpers
|
|
9257
|
-
// (Cursor uses standalone .js hook scripts registered via hooks.json; Codex uses
|
|
9258
|
-
// hooks.json directly; the others skip hooks entirely), so they must not receive
|
|
9259
|
-
// the hooks/lib/ helpers — otherwise the Codex comment downstream
|
|
9260
|
-
// ("we deliberately do *not* copy hooks/lib/ for Codex") is contradicted in practice.
|
|
9261
|
-
const hooksLibSrc = path.join(src, 'hooks', 'lib');
|
|
9262
|
-
if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi && fs.existsSync(hooksLibSrc)) {
|
|
9263
|
-
const hooksLibDest = path.join(targetDir, 'hooks', 'lib');
|
|
9264
|
-
fs.mkdirSync(hooksLibDest, { recursive: true });
|
|
9265
|
-
copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES);
|
|
9266
|
-
console.log(` ${green}✓${reset} Installed hooks/lib/ helpers (git-cmd, graphify-rebuild, ...)`);
|
|
9267
|
-
}
|
|
9268
|
-
|
|
9269
10079
|
// Install scripts/changeset/ and scripts/lib/ into <configDir>/scripts/
|
|
9270
10080
|
// so that `node "$GSD_DIR/scripts/changeset/cli.cjs"` resolves at runtime.
|
|
9271
10081
|
//
|
|
@@ -9386,14 +10196,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9386
10196
|
}
|
|
9387
10197
|
|
|
9388
10198
|
// Write file manifest for future modification detection
|
|
9389
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode });
|
|
10199
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' });
|
|
9390
10200
|
console.log(` ${green}✓${reset} Wrote file manifest (${MANIFEST_NAME})`);
|
|
9391
10201
|
|
|
9392
10202
|
// Report any backed-up local patches
|
|
9393
10203
|
reportLocalPatches(targetDir, runtime);
|
|
9394
10204
|
|
|
9395
10205
|
// Verify no leaked .claude paths in non-Claude runtimes (manifest-scoped)
|
|
9396
|
-
if (runtime
|
|
10206
|
+
if (!_hostBehaviors(runtime).ownsClaudePaths) {
|
|
9397
10207
|
const leakedPaths = [];
|
|
9398
10208
|
// Only scan files that were written by this install (manifest-tracked).
|
|
9399
10209
|
// Scanning the entire targetDir can match user-authored content that
|
|
@@ -9529,7 +10339,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9529
10339
|
// (copyCommandsAsCodexSkills removes pre-existing gsd-* dirs before re-writing)
|
|
9530
10340
|
// are restored even when they are absent from disk at rollback time (#3245 CR).
|
|
9531
10341
|
// • Dirs that did not pre-exist: remove entirely.
|
|
9532
|
-
const _rollbackSkillsDir =
|
|
10342
|
+
const _rollbackSkillsDir = _resolveSkillsRootDir(runtime, targetDir, isGlobal ? 'global' : 'local');
|
|
9533
10343
|
// Pass 1 — restore snapshot entries (may be absent from disk if deleted mid-install).
|
|
9534
10344
|
for (const skillName of codexPreInstallSkillNames) {
|
|
9535
10345
|
const skillDirPath = path.join(_rollbackSkillsDir, skillName);
|
|
@@ -9640,7 +10450,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9640
10450
|
// Re-write the manifest now that .toml agent files exist on disk.
|
|
9641
10451
|
// The initial writeManifest call (before Codex config generation) could
|
|
9642
10452
|
// not include agents/gsd-*.toml because those files did not yet exist.
|
|
9643
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode });
|
|
10453
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' });
|
|
9644
10454
|
} else {
|
|
9645
10455
|
console.log(` ${dim}↳${reset} Skipping Codex agent config generation (minimal install)`);
|
|
9646
10456
|
}
|
|
@@ -9780,24 +10590,23 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9780
10590
|
}
|
|
9781
10591
|
}
|
|
9782
10592
|
|
|
9783
|
-
// ── Codex extended hook events (#772)
|
|
9784
|
-
// Codex CLI stabilised a full hook-event set in rust-v0.137.0.
|
|
9785
|
-
//
|
|
9786
|
-
//
|
|
9787
|
-
//
|
|
9788
|
-
//
|
|
9789
|
-
//
|
|
9790
|
-
//
|
|
9791
|
-
//
|
|
9792
|
-
//
|
|
9793
|
-
//
|
|
9794
|
-
// to a follow-on issue.
|
|
10593
|
+
// ── Codex extended hook events (#772, #2088) ─────────────────────────
|
|
10594
|
+
// Codex CLI stabilised a full hook-event set in rust-v0.137.0. GSD
|
|
10595
|
+
// registers CODEX_EXTENDED_HOOK_EVENTS (#2088 adds the 6 documented
|
|
10596
|
+
// events beyond the original #772 three) — all routed through
|
|
10597
|
+
// gsd-context-monitor.js so context-headroom warnings surface at each
|
|
10598
|
+
// lifecycle point: SubagentStart/SubagentStop (subagent open/close),
|
|
10599
|
+
// Stop (final-response), PreToolUse/PostToolUse (tool boundaries),
|
|
10600
|
+
// PermissionRequest (approval prompts), Pre/PostCompact (context
|
|
10601
|
+
// compaction), and UserPromptSubmit (per-turn context injection). The
|
|
10602
|
+
// context-monitor script decides per-payload what to do; unregistered
|
|
10603
|
+
// events simply never fire.
|
|
9795
10604
|
//
|
|
9796
10605
|
// Guard: only register when the context-monitor file exists and the node
|
|
9797
10606
|
// runner is available — same guards as the SessionStart path above.
|
|
9798
10607
|
const contextMonitorFile = path.join(targetDir, 'hooks', 'gsd-context-monitor.js');
|
|
9799
10608
|
if (codexNodeRunner && fs.existsSync(contextMonitorFile)) {
|
|
9800
|
-
for (const codexEvent of
|
|
10609
|
+
for (const codexEvent of CODEX_EXTENDED_HOOK_EVENTS) {
|
|
9801
10610
|
const eventWrite = ensureCodexHooksJsonEvent(targetDir, codexEvent, {
|
|
9802
10611
|
absoluteRunner: codexNodeRunner,
|
|
9803
10612
|
platform: process.platform,
|
|
@@ -9809,7 +10618,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9809
10618
|
}
|
|
9810
10619
|
}
|
|
9811
10620
|
} else if (!codexNodeRunner) {
|
|
9812
|
-
console.warn(` ${yellow}⚠${reset} Skipped Codex
|
|
10621
|
+
console.warn(` ${yellow}⚠${reset} Skipped Codex extended hook-event registration — Node runner unavailable.`);
|
|
9813
10622
|
}
|
|
9814
10623
|
// ── end Codex extended hook events ────────────────────────────────────
|
|
9815
10624
|
}
|
|
@@ -9874,22 +10683,98 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9874
10683
|
}
|
|
9875
10684
|
|
|
9876
10685
|
if (plan.installSurface === 'cursor-hooks-json') {
|
|
9877
|
-
// #
|
|
9878
|
-
//
|
|
9879
|
-
|
|
10686
|
+
// ADR-1239 / #2089: Cursor hooks.json driven by the descriptor-managed hook-bus
|
|
10687
|
+
// adapter. Registers all 6 managed events (sessionStart, postToolUse, preToolUse,
|
|
10688
|
+
// stop, subagentStart, subagentStop) via runtime-hooks-surface.cts, which reads
|
|
10689
|
+
// the event list from the descriptor-driven adapter module.
|
|
10690
|
+
const cursorHookResult = writeCursorHooksJson(targetDir, src, {
|
|
10691
|
+
managedHookEvents: _hostBehaviors(runtime).managedHookEvents,
|
|
10692
|
+
});
|
|
9880
10693
|
if (cursorHookResult.changed) {
|
|
9881
|
-
console.log(` ${green}✓${reset} Configured Cursor lifecycle hooks (sessionStart, postToolUse)`);
|
|
10694
|
+
console.log(` ${green}✓${reset} Configured Cursor lifecycle hooks (sessionStart, postToolUse, preToolUse, stop, subagentStart, subagentStop)`);
|
|
9882
10695
|
} else {
|
|
9883
10696
|
console.log(` ${green}✓${reset} Cursor lifecycle hooks already up to date`);
|
|
9884
10697
|
}
|
|
9885
|
-
// Re-run the manifest pass
|
|
9886
|
-
|
|
10698
|
+
// Re-run the manifest pass to capture any files the hooks-json write path
|
|
10699
|
+
// produced. NOTE: hooks.json and the gsd-cursor-*.js scripts are NOT
|
|
10700
|
+
// manifest-tracked (verified) — uninstall removes them explicitly via
|
|
10701
|
+
// removeCursorHooksJson + its script list, and reconcile is idempotent.
|
|
10702
|
+
// The re-run is retained for parity with the settings.json install path.
|
|
10703
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' });
|
|
9887
10704
|
persistActiveProfileMarker();
|
|
9888
10705
|
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
9889
10706
|
}
|
|
9890
10707
|
|
|
9891
10708
|
if (plan.installSurface === 'profile-marker-only') {
|
|
9892
|
-
// Windsurf/Trae
|
|
10709
|
+
// Windsurf/Trae use artifact-only surfaces — no config.toml or settings.json
|
|
10710
|
+
// hooks needed. Kimi is also artifact-only for its INSTALL surface (skills +
|
|
10711
|
+
// kimi-agents, no settings.json) but #2095 Upgrade 1 gives it its own
|
|
10712
|
+
// independent hooksSurface: kimi's native config.toml [[hooks]] array, which
|
|
10713
|
+
// lives outside targetDir entirely (resolveKimiHooksTomlDir resolves ~/.kimi,
|
|
10714
|
+
// a sibling of targetDir's ~/.config/agents) — hence writing it here, inside
|
|
10715
|
+
// this early-return, rather than requiring installSurface to change.
|
|
10716
|
+
//
|
|
10717
|
+
// GATED TO GLOBAL ONLY (belt-and-suspenders): kimi local installs already
|
|
10718
|
+
// return early at the top of install() via hostBehaviors.localInstallDeferred,
|
|
10719
|
+
// long before this point is ever reached — so `isGlobal` is always true here
|
|
10720
|
+
// in practice. The explicit check documents that invariant and fails closed
|
|
10721
|
+
// if that early-return is ever refactored away.
|
|
10722
|
+
//
|
|
10723
|
+
// Kimi's contract forbids hooks/ or package.json under its generic
|
|
10724
|
+
// Agent-Skills configDir (targetDir) — capabilities/kimi/capability.json
|
|
10725
|
+
// declares hostBehaviors.skipSharedHooksInstall:true, which excludes it from
|
|
10726
|
+
// the shared installSharedHooksBundle(targetDir) call above. Kimi still needs
|
|
10727
|
+
// those SAME hook scripts + the CommonJS package.json marker, but SELF-
|
|
10728
|
+
// CONTAINED under its own native hook root instead — so install them there,
|
|
10729
|
+
// and point buildHookCommand (via writeKimiHooksToml's second arg) at that
|
|
10730
|
+
// same root so the generated [[hooks]] command paths reference
|
|
10731
|
+
// ~/.kimi/hooks/<script> rather than a script that doesn't exist under
|
|
10732
|
+
// targetDir/hooks (which kimi no longer receives).
|
|
10733
|
+
if (plan.hooksSurface === 'kimi-hooks-toml' && isGlobal) {
|
|
10734
|
+
const kimiHooksRoot = resolveKimiHooksTomlDir();
|
|
10735
|
+
// Note: the `failures` array's hard-fail gate (`if (failures.length > 0)
|
|
10736
|
+
// process.exit(1)`) runs earlier in this function, before this
|
|
10737
|
+
// profile-marker-only branch is ever reached — pushing to it here would
|
|
10738
|
+
// be silently ineffective. Warn instead; a failed hooks copy still
|
|
10739
|
+
// leaves kimi's skills/agents artifacts installed correctly.
|
|
10740
|
+
if (!installSharedHooksBundle(kimiHooksRoot)) {
|
|
10741
|
+
console.warn(` ${yellow}⚠${reset} Kimi hook bundle did not verify at ${path.join(kimiHooksRoot, 'hooks')} — GSD lifecycle hooks may be incomplete`);
|
|
10742
|
+
}
|
|
10743
|
+
const kimiHookOpts = { portableHooks: hasPortableHooks, runtime };
|
|
10744
|
+
const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
|
|
10745
|
+
const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts });
|
|
10746
|
+
if (kimiHooksResult.changed) {
|
|
10747
|
+
console.log(` ${green}✓${reset} Configured ${kimiHooksResult.entryCount} GSD hook(s) in ${kimiHooksTomlPath}`);
|
|
10748
|
+
}
|
|
10749
|
+
}
|
|
10750
|
+
|
|
10751
|
+
// ADR-1239 / #2100 Stage 2: Windsurf's own independent hooksSurface —
|
|
10752
|
+
// Cascade's native hooks.json blocking hook bus (pre_write_code,
|
|
10753
|
+
// pre_run_command), wired via runtime-hooks-surface.cts exactly like
|
|
10754
|
+
// Cursor's writeCursorHooksJson but with Cascade's exit-code-2 blocking
|
|
10755
|
+
// protocol instead of Cursor's stdout-JSON form. Unlike kimi's branch
|
|
10756
|
+
// above, this is NOT gated to `isGlobal` — Windsurf has no
|
|
10757
|
+
// hostBehaviors.localInstallDeferred early-return, so both local
|
|
10758
|
+
// (.windsurf/hooks.json) and global (~/.codeium/windsurf/hooks.json)
|
|
10759
|
+
// installs reach this branch and must get the hook bus wired.
|
|
10760
|
+
if (plan.hooksSurface === 'windsurf-hooks-json') {
|
|
10761
|
+
const windsurfHookResult = writeWindsurfHooksJson(targetDir, src, {
|
|
10762
|
+
platform: process.platform,
|
|
10763
|
+
});
|
|
10764
|
+
if (windsurfHookResult.changed) {
|
|
10765
|
+
console.log(` ${green}✓${reset} Configured Windsurf lifecycle hooks (pre_write_code, pre_run_command)`);
|
|
10766
|
+
} else {
|
|
10767
|
+
console.log(` ${green}✓${reset} Windsurf lifecycle hooks already up to date`);
|
|
10768
|
+
}
|
|
10769
|
+
// Re-run the manifest pass, mirroring the cursor writer's pattern above
|
|
10770
|
+
// for parity. This does NOT hash-track hooks.json or the
|
|
10771
|
+
// gsd-windsurf-*.js scripts (same as cursor): uninstall removes them
|
|
10772
|
+
// explicitly via removeWindsurfHooksJson, and reconcileWindsurfHooksJson
|
|
10773
|
+
// is idempotent on repeated installs, so manifest tracking isn't needed
|
|
10774
|
+
// for correctness here.
|
|
10775
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' });
|
|
10776
|
+
}
|
|
10777
|
+
|
|
9893
10778
|
persistActiveProfileMarker();
|
|
9894
10779
|
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
9895
10780
|
}
|
|
@@ -9901,7 +10786,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9901
10786
|
writeClineArtifacts(targetDir, isGlobal);
|
|
9902
10787
|
// Re-run the manifest pass: these artifacts are written *after* the earlier
|
|
9903
10788
|
// writeManifest() call, so a second pass is needed to hash-track them.
|
|
9904
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode });
|
|
10789
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' });
|
|
9905
10790
|
persistActiveProfileMarker();
|
|
9906
10791
|
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
9907
10792
|
}
|
|
@@ -9916,9 +10801,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9916
10801
|
// #338: local Claude installs write to settings.local.json (Claude Code's per-user/gitignored slot)
|
|
9917
10802
|
// so engineer-specific absolute paths (Node binary, home dir) never land in the repo-shared
|
|
9918
10803
|
// settings.json. Global installs and all other runtimes continue to use settings.json.
|
|
9919
|
-
const
|
|
9920
|
-
const
|
|
9921
|
-
const
|
|
10804
|
+
const _scopedSettings = _hostBehaviors(runtime).settingsFileByScope || null;
|
|
10805
|
+
const isLocalClaude = (!isGlobal && !!(_scopedSettings && _scopedSettings.local));
|
|
10806
|
+
const settingsFileName = isLocalClaude
|
|
10807
|
+
? _scopedSettings.local
|
|
10808
|
+
: ((_scopedSettings && _scopedSettings.global) || 'settings.json');
|
|
10809
|
+
// ADR-1239 Phase B write-confinement: the descriptor-sourced settings filename
|
|
10810
|
+
// must resolve under targetDir (this path also drives a recursive mkdirSync).
|
|
10811
|
+
const settingsPath = assertDestWithinConfigHome(targetDir, settingsFileName);
|
|
9922
10812
|
|
|
9923
10813
|
// #338 migration: if a prior local Claude install wrote GSD-shaped entries to settings.json,
|
|
9924
10814
|
// relocate them to settings.local.json and clear them from the shared file in the same run.
|
|
@@ -10012,7 +10902,10 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
10012
10902
|
// Claude Code sets $CLAUDE_PROJECT_DIR; Antigravity does not — and on
|
|
10013
10903
|
// Windows its own substitution logic doubles the path (#2557). It runs
|
|
10014
10904
|
// project hooks with the project dir as cwd, so bare relative paths work.
|
|
10015
|
-
|
|
10905
|
+
// Descriptor-driven (ADR-1239 / #2096): hookPathStyle comes from the
|
|
10906
|
+
// runtime's hostBehaviors instead of a hardcoded `runtime === 'antigravity'`
|
|
10907
|
+
// check inside projectLocalHookPrefix.
|
|
10908
|
+
const localPrefix = projectLocalHookPrefix({ runtime, dirName, hookPathStyle: _hostBehaviors(runtime).hookPathStyle });
|
|
10016
10909
|
const hookOpts = { portableHooks: hasPortableHooks, runtime };
|
|
10017
10910
|
// #2979: local-install hook commands also use the absolute node path so
|
|
10018
10911
|
// GUI/minimal-PATH runtimes can resolve them. Bare `node` fails when the
|
|
@@ -10101,7 +10994,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
10101
10994
|
// installAllRuntimes can register it at finalize time when the user opts
|
|
10102
10995
|
// in (#2795). Computed here (not in finishInstall) so the same buildHookCommand
|
|
10103
10996
|
// / localCmd resolution logic is shared with the other JS hooks.
|
|
10104
|
-
const updateBannerCommand =
|
|
10997
|
+
const updateBannerCommand = _hostBehaviors(runtime).skipUpdateBannerCommand
|
|
10105
10998
|
? null
|
|
10106
10999
|
: (isGlobal
|
|
10107
11000
|
? buildHookCommand(targetDir, 'gsd-update-banner.js', hookOpts)
|
|
@@ -10184,11 +11077,20 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
10184
11077
|
/**
|
|
10185
11078
|
* Apply statusline config, then print completion message
|
|
10186
11079
|
*/
|
|
10187
|
-
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime =
|
|
10188
|
-
|
|
11080
|
+
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = DEFAULT_RUNTIME, isGlobal = true, configDir = null, bannerOpts = {}) {
|
|
11081
|
+
// #2093: isKilo dropped — the Kilo permissions-writer call below is gated
|
|
11082
|
+
// on plan.finishPermissionWriter === 'kilo' (descriptor-driven), not this flag.
|
|
11083
|
+
// #2094: isTrae dropped — unused in this function.
|
|
11084
|
+
// #2095: isKimi dropped — the Kimi "Done!" banner below reads
|
|
11085
|
+
// _hostBehaviors(runtime).doneBannerStyle === 'kimi-agent-file' (descriptor-driven), not this flag.
|
|
11086
|
+
// #2096: isAntigravity dropped — unused in this function.
|
|
11087
|
+
// #2098: isCodebuddy dropped — unused in this function.
|
|
11088
|
+
// #2099: isCopilot dropped — unused in this function.
|
|
11089
|
+
// #2100: isWindsurf dropped — unused in this function.
|
|
11090
|
+
const { isOpencode, isCodex, isCursor, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
|
10189
11091
|
const plan = resolveInstallPlan(runtime);
|
|
10190
11092
|
|
|
10191
|
-
if (shouldInstallStatusline && plan.writesSharedSettings && !
|
|
11093
|
+
if (shouldInstallStatusline && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {
|
|
10192
11094
|
if (!isGlobal && !forceStatusline) {
|
|
10193
11095
|
// Local installs skip statusLine by default: repo settings.json takes precedence over
|
|
10194
11096
|
// profile-level settings.json in Claude Code, so writing here would silently clobber
|
|
@@ -10214,7 +11116,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
10214
11116
|
// settings.json hooks block — opencode/kilo/codex/cursor/windsurf/trae/
|
|
10215
11117
|
// cline either lack the surface or use a different config schema.
|
|
10216
11118
|
const { shouldInstallBanner, bannerCommand } = bannerOpts;
|
|
10217
|
-
if (shouldInstallBanner && settings && plan.writesSharedSettings && !
|
|
11119
|
+
if (shouldInstallBanner && settings && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {
|
|
10218
11120
|
if (!bannerCommand) {
|
|
10219
11121
|
console.warn(` ${yellow}⚠${reset} Skipped update banner registration — Node executable path unavailable. See #2979 / #3002.`);
|
|
10220
11122
|
} else {
|
|
@@ -10243,10 +11145,16 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
10243
11145
|
// Merges GSD-owned entries non-destructively (preserves existing user permissions).
|
|
10244
11146
|
// Scoped to Claude only: antigravity/qwen/hermes/codebuddy also write
|
|
10245
11147
|
// settings.json but use different runtimes and do not use these permission strings.
|
|
10246
|
-
if (runtime === 'claude') {
|
|
11148
|
+
if (_hostBehaviors(runtime).permissionsSchema === 'claude') {
|
|
10247
11149
|
mergeClaudePermissions(settings);
|
|
10248
11150
|
}
|
|
10249
11151
|
|
|
11152
|
+
// #2097 UPGRADE 3 (transport:mcp): companion MCP server for runtimes that host
|
|
11153
|
+
// MCP in settings.json (Augment). settings.json is golden-excluded, so no golden change.
|
|
11154
|
+
if (_hostBehaviors(runtime).mcpCompanion === 'settings-json' && settings && plan.writesSharedSettings) {
|
|
11155
|
+
mergeGsdMcpServerIntoSettings(settings);
|
|
11156
|
+
}
|
|
11157
|
+
|
|
10250
11158
|
// Write settings when runtime supports settings.json.
|
|
10251
11159
|
// #3002 CR: defense-in-depth — re-run validateHookFields right before
|
|
10252
11160
|
// serialization. The push-site guards above already skip null-command
|
|
@@ -10268,6 +11176,15 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
10268
11176
|
configureKiloPermissions(isGlobal, configDir);
|
|
10269
11177
|
}
|
|
10270
11178
|
|
|
11179
|
+
// Configure Antigravity permissions + MCP companion server (#2096 Phase B
|
|
11180
|
+
// Upgrades 1+2). Not GSD_TEST_MODE-gated — mirrors Kilo's dispatch exactly;
|
|
11181
|
+
// both writers target files (settings.json, mcp_config.json) scoped under
|
|
11182
|
+
// this runtime's own configDir, so they are safe to run unconditionally.
|
|
11183
|
+
if (plan.finishPermissionWriter === 'antigravity') {
|
|
11184
|
+
configureAntigravityPermissions(isGlobal, configDir);
|
|
11185
|
+
configureAntigravityMcpConfig(isGlobal, configDir);
|
|
11186
|
+
}
|
|
11187
|
+
|
|
10271
11188
|
// For non-Claude runtimes, DEFAULT resolve_model_ids to "omit" in ~/.gsd/defaults.json
|
|
10272
11189
|
// when it is absent or falsy, so resolveModelInternal() returns '' instead of Claude
|
|
10273
11190
|
// aliases (opus/sonnet/haiku) the runtime can't resolve. An explicit `true` opt-in
|
|
@@ -10276,7 +11193,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
10276
11193
|
// chat model instead of pinning the resolved model. See #1156 (default-to-omit
|
|
10277
11194
|
// intent) and #1569 (preserve explicit true). Guard matches the #130-class pattern
|
|
10278
11195
|
// on configureOpencodePermissions above.
|
|
10279
|
-
if (runtime
|
|
11196
|
+
if (!_hostBehaviors(runtime).nativeModelAliases && !process.env.GSD_TEST_MODE) {
|
|
10280
11197
|
const gsdDir = path.join(os.homedir(), '.gsd');
|
|
10281
11198
|
const defaultsPath = path.join(gsdDir, 'defaults.json');
|
|
10282
11199
|
try {
|
|
@@ -10318,7 +11235,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
10318
11235
|
// Restart is required for CC to pick up newly-installed skills, and the
|
|
10319
11236
|
// slash-menu surface depends on CC version — so the instruction needs to
|
|
10320
11237
|
// cover both invocation paths to avoid #2957-style "no commands appear".
|
|
10321
|
-
if (runtime
|
|
11238
|
+
if (_hostBehaviors(runtime).skillsGlobalOnboarding && isGlobal) {
|
|
10322
11239
|
console.log(`
|
|
10323
11240
|
${green}Done!${reset} Restart ${program}, then in any directory either type ${cyan}${command}${reset} or ask Claude to run the ${cyan}gsd-new-project${reset} skill.
|
|
10324
11241
|
|
|
@@ -10327,7 +11244,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
10327
11244
|
return;
|
|
10328
11245
|
}
|
|
10329
11246
|
|
|
10330
|
-
if (runtime === 'kimi') {
|
|
11247
|
+
if (_hostBehaviors(runtime).doneBannerStyle === 'kimi-agent-file') {
|
|
10331
11248
|
const agentPath = configDir ? path.join(configDir, 'agents', 'gsd.yaml') : 'agents/gsd.yaml';
|
|
10332
11249
|
console.log(`
|
|
10333
11250
|
${green}Done!${reset} Start ${program} with ${cyan}kimi --agent-file ${agentPath}${reset}, then run ${cyan}${command}${reset}.
|
|
@@ -10415,13 +11332,14 @@ const runtimeMap = {
|
|
|
10415
11332
|
'10': 'kimi',
|
|
10416
11333
|
'11': 'kilo',
|
|
10417
11334
|
'12': 'opencode',
|
|
10418
|
-
'13': '
|
|
10419
|
-
'14': '
|
|
10420
|
-
'15': '
|
|
10421
|
-
'16': '
|
|
11335
|
+
'13': 'pi',
|
|
11336
|
+
'14': 'qwen',
|
|
11337
|
+
'15': 'trae',
|
|
11338
|
+
'16': 'windsurf',
|
|
11339
|
+
'17': 'zcode'
|
|
10422
11340
|
};
|
|
10423
|
-
const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'hermes', 'kimi', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf', 'zcode'];
|
|
10424
|
-
const ALL_RUNTIMES_OPTION = '
|
|
11341
|
+
const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'hermes', 'kimi', 'kilo', 'opencode', 'pi', 'qwen', 'trae', 'windsurf', 'zcode'];
|
|
11342
|
+
const ALL_RUNTIMES_OPTION = '18';
|
|
10425
11343
|
|
|
10426
11344
|
/**
|
|
10427
11345
|
* Build the runtime-selection prompt text shown by the interactive installer.
|
|
@@ -10441,11 +11359,12 @@ function buildRuntimePromptText() {
|
|
|
10441
11359
|
${cyan}10${reset}) Kimi ${dim}(~/.config/agents, then ~/.agents if existing)${reset}
|
|
10442
11360
|
${cyan}11${reset}) Kilo ${dim}(~/.config/kilo)${reset}
|
|
10443
11361
|
${cyan}12${reset}) OpenCode ${dim}(~/.config/opencode)${reset}
|
|
10444
|
-
${cyan}13${reset})
|
|
10445
|
-
${cyan}14${reset})
|
|
10446
|
-
${cyan}15${reset})
|
|
10447
|
-
${cyan}16${reset})
|
|
10448
|
-
${cyan}17${reset})
|
|
11362
|
+
${cyan}13${reset}) pi ${dim}(~/.pi/agent)${reset}
|
|
11363
|
+
${cyan}14${reset}) Qwen Code ${dim}(~/.qwen)${reset}
|
|
11364
|
+
${cyan}15${reset}) Trae ${dim}(~/.trae)${reset}
|
|
11365
|
+
${cyan}16${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset}
|
|
11366
|
+
${cyan}17${reset}) ZCode ${dim}(~/.zcode)${reset}
|
|
11367
|
+
${cyan}18${reset}) All
|
|
10449
11368
|
|
|
10450
11369
|
${dim}Select multiple: 1,2,6 or 1 2 6${reset}
|
|
10451
11370
|
`;
|
|
@@ -10477,7 +11396,7 @@ function parseRuntimeInput(answer) {
|
|
|
10477
11396
|
}
|
|
10478
11397
|
}
|
|
10479
11398
|
|
|
10480
|
-
return selected.length > 0 ? selected : [
|
|
11399
|
+
return selected.length > 0 ? selected : [DEFAULT_RUNTIME];
|
|
10481
11400
|
}
|
|
10482
11401
|
|
|
10483
11402
|
function promptRuntime(callback) {
|
|
@@ -10992,8 +11911,8 @@ const _LEGACY_SCAN_SUBDIR_NAMES = [
|
|
|
10992
11911
|
'.agents', // antigravity local form (canonical, #791)
|
|
10993
11912
|
'.agent', // antigravity local form (legacy, backward-compat)
|
|
10994
11913
|
'.cursor',
|
|
10995
|
-
'.devin', // windsurf local form (
|
|
10996
|
-
'.windsurf', // windsurf local form (
|
|
11914
|
+
'.devin', // windsurf local form (legacy, pre-#1615; Devin Desktop preferred dir, #1085)
|
|
11915
|
+
'.windsurf', // windsurf local form (canonical since #1615; capability.json localConfigDir)
|
|
10997
11916
|
'.codeium/windsurf',
|
|
10998
11917
|
'.augment',
|
|
10999
11918
|
'.trae',
|
|
@@ -11096,7 +12015,7 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
11096
12015
|
throw error;
|
|
11097
12016
|
}
|
|
11098
12017
|
|
|
11099
|
-
const statuslineRuntimes = [
|
|
12018
|
+
const statuslineRuntimes = [DEFAULT_RUNTIME];
|
|
11100
12019
|
const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime));
|
|
11101
12020
|
|
|
11102
12021
|
const finalize = (shouldInstallStatusline, shouldInstallBanner) => {
|
|
@@ -11193,6 +12112,13 @@ module.exports = {
|
|
|
11193
12112
|
generateCodexAgentToml,
|
|
11194
12113
|
cleanupCodexSkillMetadataSidecars,
|
|
11195
12114
|
cleanupWindsurfLegacyDevinSkills,
|
|
12115
|
+
cleanupMovedSkillsOldLocation,
|
|
12116
|
+
_resolveMovedSkillsOldDir,
|
|
12117
|
+
_resolveSkillsRootDir,
|
|
12118
|
+
codexBareAgentsHasOnlyKnownScalars,
|
|
12119
|
+
extractCodexUserAgentsScalars,
|
|
12120
|
+
spliceCodexAgentsScalars,
|
|
12121
|
+
CODEX_EXTENDED_HOOK_EVENTS,
|
|
11196
12122
|
generateCodexConfigBlock,
|
|
11197
12123
|
stripGsdFromCodexConfig,
|
|
11198
12124
|
migrateCodexHooksMapFormat,
|
|
@@ -11212,6 +12138,9 @@ module.exports = {
|
|
|
11212
12138
|
install,
|
|
11213
12139
|
installAllRuntimes,
|
|
11214
12140
|
uninstall,
|
|
12141
|
+
// #2086 — host-behavior resolution + the #338 privacy fail-safe floor (exported for tests)
|
|
12142
|
+
_resolveHostBehaviors,
|
|
12143
|
+
FALLBACK_HOST_BEHAVIORS,
|
|
11215
12144
|
convertSlashCommandsToCodexSkillMentions,
|
|
11216
12145
|
convertClaudeCommandToCodexSkill,
|
|
11217
12146
|
convertClaudeCommandToKimiSkill,
|
|
@@ -11235,6 +12164,13 @@ module.exports = {
|
|
|
11235
12164
|
getConfigDirFromHome,
|
|
11236
12165
|
resolveKiloConfigPath,
|
|
11237
12166
|
configureKiloPermissions,
|
|
12167
|
+
// #2096 Phase B Upgrades 1+2 — Antigravity permission-writer + MCP companion
|
|
12168
|
+
toTildePosixPath,
|
|
12169
|
+
buildAntigravityAllowRules,
|
|
12170
|
+
configureAntigravityPermissions,
|
|
12171
|
+
configureAntigravityMcpConfig,
|
|
12172
|
+
// #2097 UPGRADE 3 — Augment MCP companion (settings.json-hosted)
|
|
12173
|
+
mergeGsdMcpServerIntoSettings,
|
|
11238
12174
|
claudeToCopilotTools,
|
|
11239
12175
|
convertCopilotToolName,
|
|
11240
12176
|
convertClaudeToCopilotContent,
|
|
@@ -11276,12 +12212,22 @@ module.exports = {
|
|
|
11276
12212
|
mergeGsdAgentsMd,
|
|
11277
12213
|
GSD_CURSOR_SESSION_HOOK_SCRIPT,
|
|
11278
12214
|
GSD_CURSOR_POST_TOOL_HOOK_SCRIPT,
|
|
12215
|
+
GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT,
|
|
12216
|
+
GSD_CURSOR_STOP_HOOK_SCRIPT,
|
|
12217
|
+
GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT,
|
|
12218
|
+
GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT,
|
|
12219
|
+
GSD_CURSOR_HOOK_SCRIPTS,
|
|
11279
12220
|
GSD_CURSOR_HOOK_MARKER,
|
|
11280
12221
|
buildCursorHookEntry,
|
|
11281
12222
|
isManagedCursorHookEntry,
|
|
11282
12223
|
reconcileCursorHooksJson,
|
|
11283
12224
|
writeCursorHooksJson,
|
|
11284
12225
|
removeCursorHooksJson,
|
|
12226
|
+
GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT,
|
|
12227
|
+
GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT,
|
|
12228
|
+
GSD_WINDSURF_HOOK_SCRIPTS,
|
|
12229
|
+
writeWindsurfHooksJson,
|
|
12230
|
+
removeWindsurfHooksJson,
|
|
11285
12231
|
stripGsdFromAgentsMd,
|
|
11286
12232
|
GSD_AGENTS_MD_MARKER,
|
|
11287
12233
|
GSD_AGENTS_MD_CLOSE_MARKER,
|
|
@@ -11380,7 +12326,7 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
|
|
|
11380
12326
|
console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
|
|
11381
12327
|
process.exit(1);
|
|
11382
12328
|
}
|
|
11383
|
-
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : [
|
|
12329
|
+
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : [DEFAULT_RUNTIME];
|
|
11384
12330
|
for (const runtime of runtimes) {
|
|
11385
12331
|
uninstall(hasGlobal, runtime);
|
|
11386
12332
|
}
|
|
@@ -11392,12 +12338,12 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
|
|
|
11392
12338
|
}
|
|
11393
12339
|
} else if (hasGlobal || hasLocal) {
|
|
11394
12340
|
// Default to Claude if no runtime specified but location is
|
|
11395
|
-
installAllRuntimes([
|
|
12341
|
+
installAllRuntimes([DEFAULT_RUNTIME], hasGlobal, false);
|
|
11396
12342
|
} else {
|
|
11397
12343
|
// Interactive
|
|
11398
12344
|
if (!process.stdin.isTTY) {
|
|
11399
12345
|
console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
|
|
11400
|
-
installAllRuntimes([
|
|
12346
|
+
installAllRuntimes([DEFAULT_RUNTIME], true, false);
|
|
11401
12347
|
} else {
|
|
11402
12348
|
promptRuntime((runtimes) => {
|
|
11403
12349
|
promptLocation(runtimes);
|