@opengsd/gsd-core 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +131 -34
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-executor.md +7 -5
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-plan-checker.md +9 -0
- package/agents/gsd-planner.md +5 -8
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-verifier.md +14 -70
- package/bin/install.js +503 -341
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +607 -63
- package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
- package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
- package/gsd-core/bin/lib/api-coverage.cjs +120 -0
- package/gsd-core/bin/lib/audit.cjs +89 -1
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-registry.cjs +96 -110
- package/gsd-core/bin/lib/capability-validator.cjs +12 -2
- package/gsd-core/bin/lib/check-command-router.cjs +43 -1
- package/gsd-core/bin/lib/command-aliases.cjs +72 -0
- package/gsd-core/bin/lib/commands.cjs +26 -25
- package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +12 -1
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +91 -12
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/frontmatter.cjs +84 -12
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1021 -57
- package/gsd-core/bin/lib/install-engine.cjs +64 -10
- package/gsd-core/bin/lib/install-profiles.cjs +27 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
- package/gsd-core/bin/lib/io.cjs +28 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone.cjs +106 -51
- package/gsd-core/bin/lib/phase-id.cjs +63 -0
- package/gsd-core/bin/lib/phase-locator.cjs +138 -45
- package/gsd-core/bin/lib/phase.cjs +260 -25
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
- package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
- package/gsd-core/bin/lib/roadmap.cjs +37 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
- package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
- package/gsd-core/bin/lib/smart-entry.cjs +12 -0
- package/gsd-core/bin/lib/state-transition.cjs +73 -8
- package/gsd-core/bin/lib/state.cjs +151 -62
- package/gsd-core/bin/lib/surface.cjs +12 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
- package/gsd-core/bin/lib/uat.cjs +320 -21
- package/gsd-core/bin/lib/unusable-input.cjs +9 -0
- package/gsd-core/bin/lib/verification.cjs +29 -12
- package/gsd-core/bin/lib/verify.cjs +29 -5
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
- package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
- package/gsd-core/bin/lib/workstream.cjs +6 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/specless-probe-fallback.md +7 -6
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +32 -69
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
- package/gsd-core/workflows/code-review.md +42 -145
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +23 -81
- package/gsd-core/workflows/debug.md +9 -12
- package/gsd-core/workflows/diagnose-issues.md +22 -0
- package/gsd-core/workflows/discovery-phase.md +4 -4
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +65 -137
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/ingest-docs.md +2 -1
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +21 -38
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +13 -226
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +49 -193
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +11 -153
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +20 -390
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +15 -8
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +17 -14
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +49 -11
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +8 -21
- package/gsd-core/workflows/ui-phase.md +8 -7
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +18 -7
- package/gsd-core/workflows/verify-phase.md +4 -7
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +8 -58
- package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
- package/hooks/dist/gsd-check-update-worker.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
- package/hooks/dist/gsd-read-injection-scanner.js +7 -0
- package/hooks/dist/gsd-statusline.js +72 -6
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/isolation-sentinel.js +268 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +428 -0
- package/hooks/gsd-check-update-worker.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +532 -26
- package/hooks/gsd-read-injection-scanner.js +7 -0
- package/hooks/gsd-statusline.js +72 -6
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/isolation-sentinel.js +268 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +14 -5
- package/pi/gsd.cjs +57 -12
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +9 -2
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-context-index.cjs +448 -0
- package/scripts/gen-inventory-manifest.cjs +101 -1
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-test-file-count.allowlist.json +27 -1
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/prompt-injection-scan.sh +27 -6
- package/scripts/registry-schema.cjs +323 -94
- package/scripts/run-tests.cjs +3 -2
- package/scripts/validate-registry.cjs +10 -6
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +2 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-emitted-baseline.cjs +0 -145
package/bin/install.js
CHANGED
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
projectPathActionProjection,
|
|
15
15
|
projectPortableHookBaseDir,
|
|
16
16
|
projectPersistentPathExportActions,
|
|
17
|
+
PATH_ACTION_REASON,
|
|
17
18
|
projectShellCommandText,
|
|
18
19
|
projectCodexHookTomlCommand,
|
|
19
20
|
shellHookOmitsBashRunner,
|
|
@@ -33,6 +34,7 @@ const {
|
|
|
33
34
|
getGlobalConfigDir,
|
|
34
35
|
getGlobalSkillsBase,
|
|
35
36
|
resolveKimiHooksTomlDir,
|
|
37
|
+
isRegisteredRuntimeId,
|
|
36
38
|
} = require('../gsd-core/bin/lib/runtime-homes.cjs');
|
|
37
39
|
// getDirName (runtime -> local config dir name) is relocated out of this
|
|
38
40
|
// installer to the runtime-name-policy leaf (ADR-1508 / #1510 Phase 1) so the
|
|
@@ -45,7 +47,18 @@ const {
|
|
|
45
47
|
} = require('../gsd-core/bin/lib/worktree-base-ref.cjs');
|
|
46
48
|
const { resolveInstallPlan } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs');
|
|
47
49
|
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
|
|
50
|
+
// #2930 (epic #1671 Phase 3): strips `<!-- gsd:section -->` markers from
|
|
51
|
+
// workflow .md content at emit time, before any per-runtime rewrite runs.
|
|
52
|
+
const { composeWorkflow } = require('../gsd-core/bin/lib/workflow-fragments.cjs');
|
|
53
|
+
// #3072: THE shared composition-scope predicate (also consumed by the served
|
|
54
|
+
// MCP catalog, src/mcp-catalog.cts) — see the comment at its call site below.
|
|
55
|
+
const { shouldCompose } = require('../gsd-core/bin/lib/mcp-catalog.cjs');
|
|
48
56
|
const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
|
|
57
|
+
// #2544: the CommonJS marker's single source of truth. classifyMarker() backs
|
|
58
|
+
// BOTH ensureCommonJsMarker() (install) and removeCommonJsMarker() (uninstall),
|
|
59
|
+
// so the write side can no longer clobber a package.json the remove side would
|
|
60
|
+
// correctly refuse to delete.
|
|
61
|
+
const { ensureCommonJsMarker, removeCommonJsMarker } = require('../gsd-core/bin/lib/commonjs-marker.cjs');
|
|
49
62
|
// Canonical set of hook files shipped to users. Imported here so writeManifest()
|
|
50
63
|
// records exactly the same set that build-hooks.js copies to hooks/dist/, making
|
|
51
64
|
// the manifest and the installed hooks/ dir structurally identical. Avoids the
|
|
@@ -333,6 +346,68 @@ const GSD_WINDSURF_HOOK_SCRIPTS = [
|
|
|
333
346
|
// that does receive hooks/lib.
|
|
334
347
|
const GSD_HOOK_LIB_FILES = ['git-cmd.js', 'gsd-graphify-rebuild.sh', 'cursor-workspace.js'];
|
|
335
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Directory name GSD stages its shared hook bundle under, inside a runtime's
|
|
351
|
+
* install root. Defaults to 'hooks' — the name every runtime used before #3023.
|
|
352
|
+
*
|
|
353
|
+
* pi (pi.dev) reserves `hooks/` as its own now-deprecated extension location and
|
|
354
|
+
* prints a migration warning on every startup when one exists, so pi overrides
|
|
355
|
+
* this via hostBehaviors.sharedHooksDirName. Following pi's advised remediation
|
|
356
|
+
* (move it into extensions/) would break the adapter's path resolution AND expose
|
|
357
|
+
* GSD's .js helpers to pi's extension auto-discovery, so the bundle is renamed in
|
|
358
|
+
* place instead — same depth, so every `__dirname/..`-relative resolution inside
|
|
359
|
+
* the bundle (e.g. hooks/gsd-context-monitor.js reaching ../gsd-core/bin/) keeps
|
|
360
|
+
* working.
|
|
361
|
+
*/
|
|
362
|
+
const SHARED_HOOKS_DIR_DEFAULT = 'hooks';
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Resolve a runtime's shared-hooks directory name from its descriptor.
|
|
366
|
+
*
|
|
367
|
+
* The value is a single path SEGMENT. This string is joined onto a user's config
|
|
368
|
+
* root and then written to and recursively read, so anything that is not a plain,
|
|
369
|
+
* non-empty, separator-free, non-dot segment is rejected back to the default —
|
|
370
|
+
* a descriptor typo must never let the installer write outside the install root.
|
|
371
|
+
*
|
|
372
|
+
* The "non-dot" part of that contract is enforced beyond the literal '.' / '..'
|
|
373
|
+
* segments: an all-dot (or dot-and-whitespace-only) segment is rejected as a
|
|
374
|
+
* meaningless name, a segment with a trailing dot or space is rejected because
|
|
375
|
+
* Windows silently strips it at directory-creation time (which would split the
|
|
376
|
+
* name the installer creates from the name callers probe for), and a Windows
|
|
377
|
+
* reserved device name (CON, PRN, AUX, NUL, COM1-9, LPT1-9, with or without an
|
|
378
|
+
* extension) is rejected because it cannot exist as a directory on Windows at
|
|
379
|
+
* all. These checks are unconditional on every platform: the descriptor is
|
|
380
|
+
* authored once and shipped everywhere, so a value invalid on Windows must be
|
|
381
|
+
* rejected identically on Linux/macOS, or the install and its fixtures disagree
|
|
382
|
+
* cross-platform.
|
|
383
|
+
*
|
|
384
|
+
* @param {string} runtime
|
|
385
|
+
* @returns {string}
|
|
386
|
+
*/
|
|
387
|
+
function resolveSharedHooksDirName(runtime) {
|
|
388
|
+
const raw = _hostBehaviors(runtime).sharedHooksDirName;
|
|
389
|
+
if (typeof raw !== 'string') return SHARED_HOOKS_DIR_DEFAULT;
|
|
390
|
+
const name = raw.trim();
|
|
391
|
+
if (name === '') return SHARED_HOOKS_DIR_DEFAULT;
|
|
392
|
+
if (name === '.' || name === '..') return SHARED_HOOKS_DIR_DEFAULT;
|
|
393
|
+
// All-dot or dot+whitespace segments ('...', '. .') are not meaningful
|
|
394
|
+
// directory names and are almost certainly a descriptor typo.
|
|
395
|
+
if (name.replace(/[.\s]/g, '') === '') return SHARED_HOOKS_DIR_DEFAULT;
|
|
396
|
+
// Windows silently strips a trailing dot or space at creation time, so the
|
|
397
|
+
// directory the installer creates would not match the name the adapter
|
|
398
|
+
// probes for — a split-brain that only reproduces off-Linux.
|
|
399
|
+
if (/[. ]$/.test(name)) return SHARED_HOOKS_DIR_DEFAULT;
|
|
400
|
+
// Windows reserved device names cannot exist as directories.
|
|
401
|
+
if (/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\..*)?$/i.test(name)) return SHARED_HOOKS_DIR_DEFAULT;
|
|
402
|
+
if (name.includes('/') || name.includes('\\')) return SHARED_HOOKS_DIR_DEFAULT;
|
|
403
|
+
// Belt-and-braces: reject anything path.basename() would reduce, and any
|
|
404
|
+
// Windows drive/UNC-flavoured value.
|
|
405
|
+
if (path.basename(name) !== name) return SHARED_HOOKS_DIR_DEFAULT;
|
|
406
|
+
if (path.isAbsolute(name)) return SHARED_HOOKS_DIR_DEFAULT;
|
|
407
|
+
if (name.includes('\0')) return SHARED_HOOKS_DIR_DEFAULT;
|
|
408
|
+
return name;
|
|
409
|
+
}
|
|
410
|
+
|
|
336
411
|
const CODEX_AGENT_SANDBOX = {
|
|
337
412
|
'gsd-executor': 'workspace-write',
|
|
338
413
|
'gsd-planner': 'workspace-write',
|
|
@@ -902,7 +977,7 @@ if (hasUninstall) {
|
|
|
902
977
|
|
|
903
978
|
// Show help if requested
|
|
904
979
|
if (hasHelp) {
|
|
905
|
-
console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--zcode${reset} Install for ZCode only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=<name>${reset} Install a named skill profile. Profiles:\n core — ${PROFILES.core.length} main-loop skills incl. phase (~130 desc tokens)\n standard — ${PROFILES.standard.length} skills incl. phase, review, config (~700)\n full — all skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi CLI under ~/.kimi-code${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi-code\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline globally${reset}\n npx ${pkg.name} --cline --global\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / COPILOT_HOME / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to the first existing generic skills root: ${cyan}~/.config/agents/skills${reset}, then ${cyan}~/.agents/skills${reset}; if neither exists, GSD creates ${cyan}~/.config/agents${reset}.\n Use ${cyan}--config-dir ~/.kimi-code${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi-code${reset} for brand-specific Kimi installs.\n`);
|
|
980
|
+
console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--zcode${reset} Install for ZCode only\n ${cyan}--pi${reset} Install for Pi only\n ${cyan}--gemini${reset} Install for Gemini CLI only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=<name>${reset} Install a named skill profile. Profiles:\n core — ${PROFILES.core.length} main-loop skills incl. phase (~130 desc tokens)\n standard — ${PROFILES.standard.length} skills incl. phase, review, config (~700)\n full — all skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi CLI under ~/.kimi-code${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi-code\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline globally${reset}\n npx ${pkg.name} --cline --global\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / COPILOT_HOME / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to the first existing generic skills root: ${cyan}~/.config/agents/skills${reset}, then ${cyan}~/.agents/skills${reset}; if neither exists, GSD creates ${cyan}~/.config/agents${reset}.\n Use ${cyan}--config-dir ~/.kimi-code${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi-code${reset} for brand-specific Kimi installs.\n`);
|
|
906
981
|
process.exit(0);
|
|
907
982
|
}
|
|
908
983
|
|
|
@@ -952,6 +1027,31 @@ const applyRuntimeContentRewritesForCommandsInPlace = runtimeArtifactConversion.
|
|
|
952
1027
|
const convertClaudeToAugmentMarkdown = runtimeArtifactConversion.convertClaudeToAugmentMarkdown;
|
|
953
1028
|
const convertClaudeCommandToAugmentSkill = runtimeArtifactConversion.convertClaudeCommandToAugmentSkill;
|
|
954
1029
|
const convertClaudeAgentToAugmentAgent = runtimeArtifactConversion.convertClaudeAgentToAugmentAgent;
|
|
1030
|
+
// #2931 (ADR-1508): the windsurf converter family is single-sourced in the
|
|
1031
|
+
// conversion module, same pattern as the #1675 Augment dedup above. install.js
|
|
1032
|
+
// re-binds (does not re-define) these so there is exactly one body — the
|
|
1033
|
+
// generative-drift hazard the dedup removes. The two private helpers
|
|
1034
|
+
// (getWindsurfSkillAdapterHeader, convertSlashCommandsToWindsurfSkillMentions)
|
|
1035
|
+
// live only in the conversion module now; they are no longer duplicated here.
|
|
1036
|
+
// The reference-identity parity guard lives in
|
|
1037
|
+
// tests/install-runtime-artifacts.test.cjs (single-owner reference-identity
|
|
1038
|
+
// guard describe block), not tests/enh-1511-rewrite-engine-relocation.test.cjs
|
|
1039
|
+
// as the Augment comment above stated — that reference was stale.
|
|
1040
|
+
// (All call sites are below this line → no TDZ hazard.)
|
|
1041
|
+
const convertClaudeToWindsurfMarkdown = runtimeArtifactConversion.convertClaudeToWindsurfMarkdown;
|
|
1042
|
+
const convertClaudeCommandToWindsurfSkill = runtimeArtifactConversion.convertClaudeCommandToWindsurfSkill;
|
|
1043
|
+
const convertClaudeCommandToWindsurfWorkflow = runtimeArtifactConversion.convertClaudeCommandToWindsurfWorkflow;
|
|
1044
|
+
const convertClaudeAgentToWindsurfAgent = runtimeArtifactConversion.convertClaudeAgentToWindsurfAgent;
|
|
1045
|
+
// #2931 (ADR-1508): single-sourced in the conversion module — was a second,
|
|
1046
|
+
// unlinked verbatim copy here (used by the local Cursor/Trae/CodeBuddy/Cline
|
|
1047
|
+
// converters below), the exact drift class this PR exists to reduce. Verified
|
|
1048
|
+
// behaviorally identical (no block / one block / adjacent blocks / whole-
|
|
1049
|
+
// content block / unclosed opening tag / nested-looking tags / repeated
|
|
1050
|
+
// sequential calls for global-regex lastIndex leakage) before merging.
|
|
1051
|
+
// install.js re-binds (does not re-define) — RUNTIME_COMPATIBILITY_BLOCK_RE
|
|
1052
|
+
// is no longer duplicated here either. (All call sites are below this line
|
|
1053
|
+
// → no TDZ hazard.)
|
|
1054
|
+
const applyClaudeCodeBrandSwap = runtimeArtifactConversion.applyClaudeCodeBrandSwap;
|
|
955
1055
|
|
|
956
1056
|
function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
|
|
957
1057
|
return hooksSurface.rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts);
|
|
@@ -1947,7 +2047,9 @@ function skillFrontmatterName(skillDirName) {
|
|
|
1947
2047
|
}
|
|
1948
2048
|
|
|
1949
2049
|
function normalizeClaudeSkillEffort(effort) {
|
|
1950
|
-
|
|
2050
|
+
// #3039: `max` is rejected by Anthropic models when extended thinking is disabled.
|
|
2051
|
+
if (effort === 'xhigh' || effort === 'max') return 'high';
|
|
2052
|
+
return effort;
|
|
1951
2053
|
}
|
|
1952
2054
|
|
|
1953
2055
|
/**
|
|
@@ -2501,56 +2603,6 @@ function extractFrontmatterField(frontmatter, fieldName) {
|
|
|
2501
2603
|
return match[1].trim().replace(/^['"]|['"]$/g, '');
|
|
2502
2604
|
}
|
|
2503
2605
|
|
|
2504
|
-
// #2284 finding (b): the `<runtime_compatibility>` block appearing in
|
|
2505
|
-
// gsd-core/workflows/{plan-phase,execute-phase}.md is a runtime-COMPARISON
|
|
2506
|
-
// table ("**Claude Code:** Uses `Agent(...)`" / "a backgrounded Claude Code
|
|
2507
|
-
// agent" / "top-level Claude Code") — every "Claude Code" mention inside it
|
|
2508
|
-
// is a COMPARED-RUNTIME LABEL, not a host self-reference. The brand swap
|
|
2509
|
-
// below (`Claude Code` → the installing runtime's own display name) is
|
|
2510
|
-
// meant only for host self-references; applying it inside this block
|
|
2511
|
-
// mislabels the comparison (e.g. Windsurf installs would read "**Windsurf:**
|
|
2512
|
-
// Uses `Agent(...)`" describing what is actually Claude Code's behavior).
|
|
2513
|
-
// This is cross-cutting across every runtime that brand-swaps workflow
|
|
2514
|
-
// content (cursor/windsurf/trae/cline/codebuddy hardcoded; qwen/hermes
|
|
2515
|
-
// descriptor-driven via hostBehaviors.brandingRewrites) — confirmed to
|
|
2516
|
-
// reproduce on unmodified Windsurf, not Hermes-specific.
|
|
2517
|
-
const RUNTIME_COMPATIBILITY_BLOCK_RE = /<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/g;
|
|
2518
|
-
|
|
2519
|
-
/**
|
|
2520
|
-
* Rewrite bare "Claude Code" self-references in workflow content to
|
|
2521
|
-
* `brandName`, EXCEPT inside `<runtime_compatibility>...</runtime_compatibility>`
|
|
2522
|
-
* blocks, which are left byte-for-byte verbatim. Every other content
|
|
2523
|
-
* transform in a runtime's `.md` converter (tool-name renames, path
|
|
2524
|
-
* rewrites, etc.) is unaffected — only this literal brand-name swap is
|
|
2525
|
-
* protected-region-aware, since only it risks mislabeling a
|
|
2526
|
-
* runtime-comparison table.
|
|
2527
|
-
*
|
|
2528
|
-
* Implementation: SPLIT `content` on the protected-block regex, brand-swap
|
|
2529
|
-
* only the GAP text between (and around) matches, then rejoin gap+block
|
|
2530
|
-
* alternately. No placeholder/sentinel token of any kind is substituted in
|
|
2531
|
-
* — a prior version used a sentinel-token mask/restore, which is exactly the
|
|
2532
|
-
* kind of invisible landmine this rewrite eliminates (a sentinel string, no
|
|
2533
|
-
* matter how obscure, is a theoretical collision risk with real content and
|
|
2534
|
-
* is easy to silently reintroduce in a future edit without it showing in a
|
|
2535
|
-
* diff). Behavior-identical to the removed sentinel-token version — verified
|
|
2536
|
-
* via `npm run gen:golden` producing zero further diff.
|
|
2537
|
-
*/
|
|
2538
|
-
function applyClaudeCodeBrandSwap(content, brandName) {
|
|
2539
|
-
if (!brandName) return content;
|
|
2540
|
-
let result = '';
|
|
2541
|
-
let lastIndex = 0;
|
|
2542
|
-
RUNTIME_COMPATIBILITY_BLOCK_RE.lastIndex = 0; // reset shared global-regex state before each use
|
|
2543
|
-
let m;
|
|
2544
|
-
while ((m = RUNTIME_COMPATIBILITY_BLOCK_RE.exec(content))) {
|
|
2545
|
-
const gap = content.slice(lastIndex, m.index);
|
|
2546
|
-
result += gap.replace(/\bClaude Code\b/g, brandName);
|
|
2547
|
-
result += m[0]; // protected block, verbatim — never brand-swapped
|
|
2548
|
-
lastIndex = m.index + m[0].length;
|
|
2549
|
-
}
|
|
2550
|
-
result += content.slice(lastIndex).replace(/\bClaude Code\b/g, brandName);
|
|
2551
|
-
return result;
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
2606
|
// Tool name mapping from Claude Code to Cursor CLI
|
|
2555
2607
|
const claudeToCursorTools = {
|
|
2556
2608
|
Bash: 'Shell',
|
|
@@ -2629,36 +2681,11 @@ function convertClaudeCommandToCursorSkill(content, skillName) {
|
|
|
2629
2681
|
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
|
2630
2682
|
const adapter = getCursorSkillAdapterHeader(skillName);
|
|
2631
2683
|
|
|
2632
|
-
//
|
|
2633
|
-
//
|
|
2634
|
-
//
|
|
2635
|
-
//
|
|
2636
|
-
|
|
2637
|
-
// '/' while keeping model invocation — distinct from disable-model-invocation.)
|
|
2638
|
-
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
2639
|
-
}
|
|
2640
|
-
|
|
2641
|
-
/**
|
|
2642
|
-
* Convert a Claude Code command to a Cursor 1.6 slash command (#785).
|
|
2643
|
-
*
|
|
2644
|
-
* Cursor slash commands live in `.cursor/commands/<name>.md` and are
|
|
2645
|
-
* plain markdown — no YAML frontmatter, no adapter header. The filename
|
|
2646
|
-
* becomes the command name (e.g. `gsd-help.md` → `/gsd-help`).
|
|
2647
|
-
*
|
|
2648
|
-
* Applies the same `convertClaudeToCursorMarkdown` transforms as the skill
|
|
2649
|
-
* converter (tool renames, brand substitution, slash-command normalisation),
|
|
2650
|
-
* then strips the YAML frontmatter block so only the prose body remains.
|
|
2651
|
-
*
|
|
2652
|
-
* @param {string} content raw Claude Code command markdown (may have frontmatter)
|
|
2653
|
-
* @param {string} _commandName the target command name (unused; present for
|
|
2654
|
-
* API symmetry with other converters so the runtime-artifact-layout stage
|
|
2655
|
-
* function can call it uniformly)
|
|
2656
|
-
* @returns {string} plain markdown body, no frontmatter
|
|
2657
|
-
*/
|
|
2658
|
-
function convertClaudeCommandToCursorCommand(content, _commandName) {
|
|
2659
|
-
const converted = convertClaudeToCursorMarkdown(content);
|
|
2660
|
-
const { body } = extractFrontmatterAndBody(converted);
|
|
2661
|
-
return body.trimStart();
|
|
2684
|
+
// Cursor skills are both slash-invocable and model-invocable. Do not emit the
|
|
2685
|
+
// unsupported `user-invocable` field: it is ignored by Cursor and previously
|
|
2686
|
+
// hid the real cause of duplicate entries, the parallel commands/ surface
|
|
2687
|
+
// retired in #2644.
|
|
2688
|
+
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
2662
2689
|
}
|
|
2663
2690
|
|
|
2664
2691
|
/**
|
|
@@ -2681,142 +2708,16 @@ function convertClaudeAgentToCursorAgent(content) {
|
|
|
2681
2708
|
}
|
|
2682
2709
|
|
|
2683
2710
|
// --- Windsurf converters ---
|
|
2684
|
-
//
|
|
2685
|
-
//
|
|
2686
|
-
|
|
2687
|
-
//
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
function convertSlashCommandsToWindsurfSkillMentions(content) {
|
|
2696
|
-
// Keep leading "/" for slash commands; only normalize gsd: -> gsd-.
|
|
2697
|
-
return content.replace(/gsd:/gi, 'gsd-');
|
|
2698
|
-
}
|
|
2699
|
-
|
|
2700
|
-
function convertClaudeToWindsurfMarkdown(content) {
|
|
2701
|
-
let converted = convertSlashCommandsToWindsurfSkillMentions(content);
|
|
2702
|
-
// Replace tool name references in body text
|
|
2703
|
-
converted = converted.replace(/\bBash\(/g, 'Shell(');
|
|
2704
|
-
converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
|
|
2705
|
-
converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting');
|
|
2706
|
-
// Replace subagent_type from Claude to Windsurf format
|
|
2707
|
-
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
|
|
2708
|
-
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
|
2709
|
-
// Replace project-level Claude conventions with Windsurf equivalents.
|
|
2710
|
-
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`');
|
|
2711
|
-
converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules');
|
|
2712
|
-
converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`');
|
|
2713
|
-
converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules');
|
|
2714
|
-
converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/');
|
|
2715
|
-
converted = converted.replace(/\.\/\.claude\//g, './.windsurf/');
|
|
2716
|
-
converted = converted.replace(/\.claude\//g, '.windsurf/');
|
|
2717
|
-
// Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
|
|
2718
|
-
// Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
|
|
2719
|
-
converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.windsurf');
|
|
2720
|
-
converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.windsurf');
|
|
2721
|
-
// Environment variable name rewrite
|
|
2722
|
-
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR');
|
|
2723
|
-
// Remove Claude Code-specific bug workarounds before brand replacement
|
|
2724
|
-
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
2725
|
-
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
2726
|
-
// Replace "Claude Code" brand references with "Windsurf" — #2284(b): skips
|
|
2727
|
-
// <runtime_compatibility> comparison-table content (protected region).
|
|
2728
|
-
converted = applyClaudeCodeBrandSwap(converted, 'Windsurf');
|
|
2729
|
-
return converted;
|
|
2730
|
-
}
|
|
2731
|
-
|
|
2732
|
-
function getWindsurfSkillAdapterHeader(skillName) {
|
|
2733
|
-
return `<windsurf_skill_adapter>
|
|
2734
|
-
## A. Skill Invocation
|
|
2735
|
-
- This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill.
|
|
2736
|
-
- Treat all user text after the skill mention as \`{{GSD_ARGS}}\`.
|
|
2737
|
-
- If no arguments are present, treat \`{{GSD_ARGS}}\` as empty.
|
|
2738
|
-
|
|
2739
|
-
## B. User Prompting
|
|
2740
|
-
When the workflow needs user input, prompt the user conversationally:
|
|
2741
|
-
- Present options as a numbered list in your response text
|
|
2742
|
-
- Ask the user to reply with their choice
|
|
2743
|
-
- For multi-select, ask for comma-separated numbers
|
|
2744
|
-
|
|
2745
|
-
## C. Tool Usage
|
|
2746
|
-
Use these Windsurf tools when executing GSD workflows:
|
|
2747
|
-
- \`Shell\` for running commands (terminal operations)
|
|
2748
|
-
- \`StrReplace\` for editing existing files
|
|
2749
|
-
- \`Read\`, \`Write\`, \`Glob\`, \`Grep\`, \`Task\`, \`WebSearch\`, \`WebFetch\`, \`TodoWrite\` as needed
|
|
2750
|
-
|
|
2751
|
-
## D. Subagent Spawning
|
|
2752
|
-
When the workflow needs to spawn a subagent:
|
|
2753
|
-
- Use \`Task(subagent_type="generalPurpose", ...)\`
|
|
2754
|
-
- The \`model\` parameter maps to Windsurf's model options (e.g., "fast")
|
|
2755
|
-
</windsurf_skill_adapter>`;
|
|
2756
|
-
}
|
|
2757
|
-
|
|
2758
|
-
function convertClaudeCommandToWindsurfSkill(content, skillName) {
|
|
2759
|
-
const converted = convertClaudeToWindsurfMarkdown(content);
|
|
2760
|
-
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
2761
|
-
let description = `Run GSD workflow ${skillName}.`;
|
|
2762
|
-
if (frontmatter) {
|
|
2763
|
-
const maybeDescription = extractFrontmatterField(frontmatter, 'description');
|
|
2764
|
-
if (maybeDescription) {
|
|
2765
|
-
description = maybeDescription;
|
|
2766
|
-
}
|
|
2767
|
-
}
|
|
2768
|
-
description = toSingleLine(description);
|
|
2769
|
-
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
|
2770
|
-
const adapter = getWindsurfSkillAdapterHeader(skillName);
|
|
2771
|
-
|
|
2772
|
-
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
2773
|
-
}
|
|
2774
|
-
|
|
2775
|
-
function convertClaudeCommandToWindsurfWorkflow(content, commandName) {
|
|
2776
|
-
// #1615 security: commandName flows unsanitized into a markdown body that
|
|
2777
|
-
// Windsurf loads as an LLM-readable workflow. Validate at entry to prevent
|
|
2778
|
-
// (a) prompt injection via newlines / markdown structure in the filename,
|
|
2779
|
-
// (b) path-component injection via .., /, \ in stem → @-reference target.
|
|
2780
|
-
// Pattern: optional gsd- prefix + lowercase alphanumeric + dashes; rejects
|
|
2781
|
-
// everything else. See DEFECT.PROMPT-INJECTION-SCAN-COLLISION and the
|
|
2782
|
-
// PR #1622 security review.
|
|
2783
|
-
if (typeof commandName !== 'string' || !/^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(commandName)) {
|
|
2784
|
-
const preview = typeof commandName === 'string' ? JSON.stringify(commandName.slice(0, 60)) : String(commandName);
|
|
2785
|
-
throw new Error(
|
|
2786
|
-
`convertClaudeCommandToWindsurfWorkflow: rejected commandName ${preview}; ` +
|
|
2787
|
-
'must match /^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ (no slashes, backslashes, spaces, dots, trailing dash, or control chars — prevents prompt injection and path-component injection into the workflow body)'
|
|
2788
|
-
);
|
|
2789
|
-
}
|
|
2790
|
-
const converted = convertClaudeToWindsurfMarkdown(content);
|
|
2791
|
-
const { frontmatter } = extractFrontmatterAndBody(converted);
|
|
2792
|
-
const description = frontmatter ? extractFrontmatterField(frontmatter, 'description') : '';
|
|
2793
|
-
const stem = commandName.startsWith('gsd-') ? commandName.slice(4) : commandName;
|
|
2794
|
-
const workflow = `# ${commandName}\n\n${toSingleLine(description || `Run ${commandName}.`)}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`;
|
|
2795
|
-
const byteLength = Buffer.byteLength(workflow, 'utf8');
|
|
2796
|
-
if (byteLength > 12000) {
|
|
2797
|
-
throw new Error(`Windsurf workflow ${commandName} exceeds 12000 bytes (${byteLength}); extract references before installing`);
|
|
2798
|
-
}
|
|
2799
|
-
return workflow;
|
|
2800
|
-
}
|
|
2801
|
-
|
|
2802
|
-
/**
|
|
2803
|
-
* Convert Claude Code agent markdown to Windsurf agent format.
|
|
2804
|
-
* Strips frontmatter fields Windsurf doesn't support (color, skills),
|
|
2805
|
-
* converts tool references, and adds a role context header.
|
|
2806
|
-
*/
|
|
2807
|
-
function convertClaudeAgentToWindsurfAgent(content) {
|
|
2808
|
-
let converted = convertClaudeToWindsurfMarkdown(content);
|
|
2809
|
-
|
|
2810
|
-
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
2811
|
-
if (!frontmatter) return converted;
|
|
2812
|
-
|
|
2813
|
-
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
|
2814
|
-
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
2815
|
-
|
|
2816
|
-
const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
|
|
2817
|
-
|
|
2818
|
-
return `${cleanFrontmatter}\n${body}`;
|
|
2819
|
-
}
|
|
2711
|
+
// #2931 (ADR-1508): single-sourced in runtimeArtifactConversion, bound near
|
|
2712
|
+
// the top of this file alongside the #1675 Augment family. This block
|
|
2713
|
+
// previously carried byte-identical local duplicates of
|
|
2714
|
+
// convertSlashCommandsToWindsurfSkillMentions, convertClaudeToWindsurfMarkdown,
|
|
2715
|
+
// getWindsurfSkillAdapterHeader, convertClaudeCommandToWindsurfSkill,
|
|
2716
|
+
// convertClaudeCommandToWindsurfWorkflow, and convertClaudeAgentToWindsurfAgent,
|
|
2717
|
+
// plus an unused claudeToWindsurfTools table. Deleted here; the two
|
|
2718
|
+
// unexported helpers (getWindsurfSkillAdapterHeader,
|
|
2719
|
+
// convertSlashCommandsToWindsurfSkillMentions) now live only in the
|
|
2720
|
+
// conversion module, with no other caller in this file.
|
|
2820
2721
|
|
|
2821
2722
|
// --- Augment converters ---
|
|
2822
2723
|
// Augment uses a tool set similar to Cursor/Windsurf.
|
|
@@ -2844,10 +2745,55 @@ function convertClaudeToTraeMarkdown(content) {
|
|
|
2844
2745
|
// Replace general-purpose subagent type with Trae's equivalent "general_purpose_task"
|
|
2845
2746
|
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="general_purpose_task"');
|
|
2846
2747
|
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2748
|
+
// #2658: full-path forms (with a leading dot-claude-slash prefix) MUST be
|
|
2749
|
+
// replaced before the bare Claude-instruction-file pattern and before the
|
|
2750
|
+
// generic dot-claude-slash rewrite below — otherwise the bare pattern
|
|
2751
|
+
// consumes only the instruction-filename tail, leaving that prefix stale
|
|
2752
|
+
// in place, and the generic rewrite then mutates the stale leftover too,
|
|
2753
|
+
// producing a doubled trae-prefix segment ahead of the rules path instead
|
|
2754
|
+
// of a single clean one. (Deliberately never spelling the instruction
|
|
2755
|
+
// filename as one contiguous "CLAUDE" + dot + "md" token, and never
|
|
2756
|
+
// spelling either malformed shape out as a literal contiguous string, in
|
|
2757
|
+
// ANY comment in this function: this file ships verbatim into local
|
|
2758
|
+
// `--trae` installs, where it is itself run through this same class of
|
|
2759
|
+
// find/replace — a literal instruction-filename token sitting in a
|
|
2760
|
+
// comment gets "fixed" right along with real code, and the emitted-content
|
|
2761
|
+
// regression test added alongside this fix asserts neither malformed
|
|
2762
|
+
// shape appears anywhere in the installed tree, comments included; this
|
|
2763
|
+
// bit the fix itself twice during development.) All forms converge on the
|
|
2764
|
+
// same concrete file (never a bare directory) so this stays in parity
|
|
2765
|
+
// with the `trae.js` RUNTIME_CONTENT_DISPATCH entry.
|
|
2766
|
+
converted = converted.replace(/`\.\/\.claude\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
|
|
2767
|
+
converted = converted.replace(/\.\/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
|
|
2768
|
+
converted = converted.replace(/`\.claude\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
|
|
2769
|
+
converted = converted.replace(/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
|
|
2770
|
+
// #2658 (found via the end-to-end install regression test, not the static
|
|
2771
|
+
// trace above): `copyWithPathReplacement` runs a GENERIC dot-claude-slash
|
|
2772
|
+
// -> runtime-config-dir rewrite on every .md file before calling this
|
|
2773
|
+
// converter — for `~/.claude/`, `$HOME/.claude/`, AND `./.claude/` alike —
|
|
2774
|
+
// substituting a runtime-appropriate `pathPrefix` this function is never
|
|
2775
|
+
// given and cannot itself compute (it differs per install invocation: a
|
|
2776
|
+
// relative `./.trae/` for a project-local install, an arbitrary absolute
|
|
2777
|
+
// path for a local install rooted elsewhere, `~/.trae/` for a global one).
|
|
2778
|
+
// So for source using any of those prefixed forms, the patterns above
|
|
2779
|
+
// never fire here — this converter only ever sees the ALREADY-rewritten
|
|
2780
|
+
// "<runtime-config-dir>/" + instruction-filename shape, with whatever
|
|
2781
|
+
// prefix the install actually used. The generic pattern below preserves
|
|
2782
|
+
// that prefix verbatim (via the capture group) and only fixes the
|
|
2783
|
+
// filename suffix, rather than assuming a fixed `./.trae/` shape — a
|
|
2784
|
+
// narrower fixed-prefix version of this pattern shipped first and still
|
|
2785
|
+
// left the doubled-prefix defect live for the `$HOME/.claude/` and
|
|
2786
|
+
// `~/.claude/` forms specifically (found the same way, one regression-test
|
|
2787
|
+
// run later). Scoped to a `.trae/` tail so it cannot also swallow the
|
|
2788
|
+
// unprefixed `./CLAUDE.md` form the very next pattern handles differently
|
|
2789
|
+
// (discarding the prefix entirely, not preserving it). Must run before
|
|
2790
|
+
// the bare pattern for the same consume-the-full-match-first reason.
|
|
2791
|
+
converted = converted.replace(/`([^\s`]*\.trae\/)CLAUDE\.md`/g, '`$1rules/rules.md`');
|
|
2792
|
+
converted = converted.replace(/([^\s`]*\.trae\/)CLAUDE\.md/g, '$1rules/rules.md');
|
|
2793
|
+
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
|
|
2794
|
+
converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/rules.md');
|
|
2795
|
+
converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/rules.md`');
|
|
2796
|
+
converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/rules.md');
|
|
2851
2797
|
converted = converted.replace(/\.claude\/skills\//g, '.trae/skills/');
|
|
2852
2798
|
converted = converted.replace(/\.\/\.claude\//g, './.trae/');
|
|
2853
2799
|
converted = converted.replace(/\.claude\//g, '.trae/');
|
|
@@ -4019,6 +3965,8 @@ Typed mapping (agent_type-capable schema only):
|
|
|
4019
3965
|
inherited, or unsupported values; do not invent one-off effort literals in
|
|
4020
3966
|
workflow prose.
|
|
4021
3967
|
- \`fork_context: false\` by default — GSD agents load their own context via \`<files_to_read>\` blocks
|
|
3968
|
+
- \`task_name\` — required by the collaboration schema; provide a descriptive name for each spawned task
|
|
3969
|
+
- \`fork_turns\` — optional parameter controlling turn-forking depth; coexists with \`fork_context\` (not a replacement)
|
|
4022
3970
|
- \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping,
|
|
4023
3971
|
but Codex declares \`dispatch.isolation: orchestrator-worktree\` (#2584). Codex
|
|
4024
3972
|
\`spawn_agent\` still does not create or bind a git worktree; instead GSD itself
|
|
@@ -4055,11 +4003,13 @@ Spawn restriction:
|
|
|
4055
4003
|
defaulting to inline execution.
|
|
4056
4004
|
|
|
4057
4005
|
Parallel fan-out:
|
|
4058
|
-
- Spawn multiple agents → collect agent IDs → \`
|
|
4006
|
+
- Spawn multiple agents → collect agent IDs → \`collaboration.wait_agent(timeout_ms=...)\` for each to complete
|
|
4007
|
+
- Do NOT use \`functions.wait(cell_id=...)\` — that is an unrelated exec-cell tool, not the collaboration wait
|
|
4059
4008
|
|
|
4060
4009
|
Result parsing:
|
|
4061
4010
|
- Look for structured markers in agent output: \`CHECKPOINT\`, \`PLAN COMPLETE\`, \`SUMMARY\`, etc.
|
|
4062
|
-
- \`close_agent(id)\` after collecting results
|
|
4011
|
+
- \`close_agent(id)\` after collecting results — but only if \`close_agent\` is visible in the current
|
|
4012
|
+
tool schema (check via \`tool_search\` first, same schema-detection gate as \`spawn_agent\` above)
|
|
4063
4013
|
</codex_skill_adapter>`;
|
|
4064
4014
|
}
|
|
4065
4015
|
|
|
@@ -6445,17 +6395,40 @@ function mergeCodexConfig(configPath, gsdBlock) {
|
|
|
6445
6395
|
const normalizedGsdBlock = mergedGsdBlock.replace(/\r?\n/g, eol);
|
|
6446
6396
|
const markerIndex = existing.indexOf(GSD_CODEX_MARKER);
|
|
6447
6397
|
|
|
6448
|
-
// Case 2: Has GSD marker —
|
|
6398
|
+
// Case 2: Has GSD marker — preserve user content on BOTH sides, regenerate the GSD block.
|
|
6399
|
+
//
|
|
6400
|
+
// #2940: the marker delimits where GSD's OWN block begins, NOT where every post-marker byte
|
|
6401
|
+
// is GSD-owned. A fresh install writes the GSD block as the file's entire content, so any
|
|
6402
|
+
// settings the user or Codex CLI later adds ([model], [mcp_servers.*], [profiles.*]) land
|
|
6403
|
+
// AFTER the block. The previous truncate-to-marker logic discarded that trailing region on
|
|
6404
|
+
// every update, destroying user config. The fix routes the trailing region through the
|
|
6405
|
+
// existing AST-based `stripLeakedGsdCodexSections`, which removes GSD's own managed/leaked
|
|
6406
|
+
// sections (the bare [agents] table GSD regenerates, legacy [agents.gsd-*], [[agents]])
|
|
6407
|
+
// while preserving genuine user TOML — so #2406's de-dup still holds AND user content survives.
|
|
6449
6408
|
if (markerIndex !== -1) {
|
|
6450
6409
|
let before = existing.substring(0, markerIndex).trimEnd();
|
|
6451
6410
|
if (before) {
|
|
6452
6411
|
// Strip any GSD-managed sections that leaked above the marker from previous installs
|
|
6453
6412
|
before = stripLeakedGsdCodexSections(before).trimEnd();
|
|
6454
|
-
|
|
6455
|
-
atomicWriteFileSync(configPath, before + eol + eol + normalizedGsdBlock + eol);
|
|
6456
|
-
} else {
|
|
6457
|
-
atomicWriteFileSync(configPath, normalizedGsdBlock + eol);
|
|
6458
6413
|
}
|
|
6414
|
+
// Capture and preserve genuine user content AFTER the GSD-managed region. The whole
|
|
6415
|
+
// post-marker region is passed through stripLeakedGsdCodexSections: GSD's own previously-
|
|
6416
|
+
// emitted [agents] table (regenerated above as normalizedGsdBlock) and any leaked sections
|
|
6417
|
+
// are removed, while user tables ([model], [mcp_servers.*], [profiles.*]) are kept. The
|
|
6418
|
+
// marker comment line itself (and the optional codex_hooks ownership line right under it)
|
|
6419
|
+
// is GSD-owned and is stripped from the trailing region so it is not duplicated alongside
|
|
6420
|
+
// the freshly regenerated block.
|
|
6421
|
+
const rawAfter = existing.substring(markerIndex);
|
|
6422
|
+
const markerStripped = rawAfter
|
|
6423
|
+
.replace(GSD_CODEX_MARKER, '')
|
|
6424
|
+
.replace(/^\r?\n# GSD codex_hooks ownership: (?:section|root_dotted)\r?\n/, '');
|
|
6425
|
+
const afterUser = stripLeakedGsdCodexSections(markerStripped).trim();
|
|
6426
|
+
|
|
6427
|
+
const parts = [];
|
|
6428
|
+
if (before) parts.push(before);
|
|
6429
|
+
parts.push(normalizedGsdBlock);
|
|
6430
|
+
if (afterUser) parts.push(afterUser);
|
|
6431
|
+
atomicWriteFileSync(configPath, parts.join(eol + eol) + eol);
|
|
6459
6432
|
return;
|
|
6460
6433
|
}
|
|
6461
6434
|
|
|
@@ -6983,6 +6956,47 @@ function writeCopilotHookConfig(targetDir) {
|
|
|
6983
6956
|
* Generate config.toml and per-agent .toml files for Codex.
|
|
6984
6957
|
* Reads agent .md files from source, extracts metadata, writes .toml configs.
|
|
6985
6958
|
*/
|
|
6959
|
+
|
|
6960
|
+
/**
|
|
6961
|
+
* #2834: Write ~/.gsd/defaults.json for non-Claude runtimes — sets
|
|
6962
|
+
* resolve_model_ids="omit" (so resolveModelInternal() returns '' instead of
|
|
6963
|
+
* Claude aliases the runtime can't resolve) and runtime=<runtime> (so
|
|
6964
|
+
* resolveRuntime() resolves correctly out of the box). MUST be called BEFORE
|
|
6965
|
+
* installCodexConfig (or any other step that reads defaults.json at generation
|
|
6966
|
+
* time), so a clean first install produces correctly-model-routed agent TOMLs.
|
|
6967
|
+
* No-op for Claude runtimes (Claude is the resolveRuntime fallback + has native
|
|
6968
|
+
* model aliases). Preserves an explicit `true` opt-in and existing values.
|
|
6969
|
+
*/
|
|
6970
|
+
function writeNonClaudeDefaults(runtime) {
|
|
6971
|
+
if (_hostBehaviors(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return;
|
|
6972
|
+
const gsdDir = path.join(os.homedir(), '.gsd');
|
|
6973
|
+
const defaultsPath = path.join(gsdDir, 'defaults.json');
|
|
6974
|
+
try {
|
|
6975
|
+
fs.mkdirSync(gsdDir, { recursive: true });
|
|
6976
|
+
let defaults = {};
|
|
6977
|
+
try { defaults = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')); } catch { /* new file */ }
|
|
6978
|
+
if (defaults === null || typeof defaults !== 'object' || Array.isArray(defaults)) {
|
|
6979
|
+
defaults = {};
|
|
6980
|
+
}
|
|
6981
|
+
// Three-valued domain: false/absent → aliases; true → full IDs; "omit" → ''.
|
|
6982
|
+
const existing = defaults.resolve_model_ids;
|
|
6983
|
+
const shouldDefaultToOmit = existing !== true && existing !== 'omit';
|
|
6984
|
+
if (shouldDefaultToOmit) {
|
|
6985
|
+
defaults.resolve_model_ids = 'omit';
|
|
6986
|
+
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
|
|
6987
|
+
console.log(` ${green}✓${reset} Set resolve_model_ids: "omit" in ~/.gsd/defaults.json`);
|
|
6988
|
+
}
|
|
6989
|
+
// #2395: persist runtime for non-Claude runtimes.
|
|
6990
|
+
if (defaults.runtime === undefined || defaults.runtime === null || defaults.runtime === '') {
|
|
6991
|
+
defaults.runtime = runtime;
|
|
6992
|
+
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
|
|
6993
|
+
console.log(` ${green}✓${reset} Set runtime: "${runtime}" in ~/.gsd/defaults.json`);
|
|
6994
|
+
}
|
|
6995
|
+
} catch (e) {
|
|
6996
|
+
console.log(` ${yellow}⚠${reset} Could not write ~/.gsd/defaults.json: ${e.message}`);
|
|
6997
|
+
}
|
|
6998
|
+
}
|
|
6999
|
+
|
|
6986
7000
|
function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-sandbox') {
|
|
6987
7001
|
// ADR-1239 Phase B write-confinement: every Codex config write stays under targetDir.
|
|
6988
7002
|
const configPath = assertDestWithinConfigHome(targetDir, 'config.toml');
|
|
@@ -7010,7 +7024,16 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
|
|
|
7010
7024
|
const codexGsdPath = `${path.resolve(targetDir, 'gsd-core').replace(/\\/g, '/')}/`;
|
|
7011
7025
|
|
|
7012
7026
|
for (const file of agentEntries) {
|
|
7013
|
-
|
|
7027
|
+
const agentTomlSourcePath = path.join(agentsSrc, file);
|
|
7028
|
+
let content = fs.readFileSync(agentTomlSourcePath, 'utf8');
|
|
7029
|
+
// #2995 (epic #1671 Phase 6.4): Codex embeds each agent's prompt into a
|
|
7030
|
+
// per-agent `.toml`, reading the source .md independently of the inline
|
|
7031
|
+
// agent loop — a separate emission path that must strip gsd:section
|
|
7032
|
+
// markers too, or a marked agent ships its markers inside the TOML.
|
|
7033
|
+
// Found by the exhaustive per-runtime emission sweep in
|
|
7034
|
+
// tests/agent-fragments-emission.install.test.cjs, not by call-graph
|
|
7035
|
+
// analysis, which is why that guard is behavioral rather than structural.
|
|
7036
|
+
content = composeWorkflow(content, { sourcePath: agentTomlSourcePath });
|
|
7014
7037
|
// Replace full .claude/gsd-core prefix so path resolves to the Codex
|
|
7015
7038
|
// GSD install before generic .claude → .codex conversion rewrites it.
|
|
7016
7039
|
content = content.replace(/~\/\.claude\/gsd-core\//g, codexGsdPath);
|
|
@@ -7627,7 +7650,18 @@ const RUNTIME_CONTENT_DISPATCH = {
|
|
|
7627
7650
|
return `/gsd-${commandName}`;
|
|
7628
7651
|
});
|
|
7629
7652
|
content = content.replace(/\.claude\/skills\//g, '.trae/skills/');
|
|
7630
|
-
|
|
7653
|
+
// #2658: the full dot-claude-slash-prefixed instruction-file path must
|
|
7654
|
+
// be replaced before the bare instruction-filename fallback, or the
|
|
7655
|
+
// bare regex only rewrites that filename and leaves the prefix stale
|
|
7656
|
+
// in place, producing a malformed doubled-prefix path (see the longer
|
|
7657
|
+
// note in convertClaudeToTraeMarkdown above — the instruction filename
|
|
7658
|
+
// and either malformed shape are deliberately never spelled out
|
|
7659
|
+
// contiguously here either, for the same reason: this file ships
|
|
7660
|
+
// verbatim). Both forms target the same concrete file (never a bare
|
|
7661
|
+
// directory), matching the `.md` converter (convertClaudeToTraeMarkdown)
|
|
7662
|
+
// so js/cjs and md content agree on one canonical path.
|
|
7663
|
+
content = content.replace(/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
|
|
7664
|
+
content = content.replace(/CLAUDE\.md/g, '.trae/rules/rules.md');
|
|
7631
7665
|
content = content.replace(/\bClaude Code\b/g, 'Trae');
|
|
7632
7666
|
return content;
|
|
7633
7667
|
},
|
|
@@ -7745,6 +7779,42 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7745
7779
|
// Replace ~/.claude/ and $HOME/.claude/ and ./.claude/ with runtime-appropriate paths
|
|
7746
7780
|
// Skip generic replacement for Copilot/Antigravity — their converters handle all paths
|
|
7747
7781
|
let content = fs.readFileSync(srcPath, 'utf8');
|
|
7782
|
+
|
|
7783
|
+
// #2930 (epic #1671 Phase 3): strip `<!-- gsd:section -->` markers
|
|
7784
|
+
// BEFORE any per-runtime rewrite so a `.claude/` -> `.windsurf/` regex
|
|
7785
|
+
// (or any other converter below) never reaches inside a marker
|
|
7786
|
+
// attribute and corrupts it. composeWorkflow is a no-op (byte-identical
|
|
7787
|
+
// return) for the 88+ workflows and every non-workflow .md that carries
|
|
7788
|
+
// no markers, and for a malformed marker it throws loudly naming
|
|
7789
|
+
// srcPath — never emit a half-composed workflow.
|
|
7790
|
+
//
|
|
7791
|
+
// Scoped to gsd-core/workflows/ ONLY (two independent reviewers,
|
|
7792
|
+
// chore/2930): copyWithPathReplacement is the emit path for every .md
|
|
7793
|
+
// under gsd-core/, skills/, and commands/ (see the three call sites),
|
|
7794
|
+
// not just workflows. A doc that merely DOCUMENTS the marker syntax
|
|
7795
|
+
// with an unfenced example (docs/reference/workflow-fragments.md is
|
|
7796
|
+
// the live instance of this class, though not under the install tree
|
|
7797
|
+
// today) would otherwise get silently mis-parsed as a real marker and
|
|
7798
|
+
// that line lossily dropped — a file class issue #2930 never scoped
|
|
7799
|
+
// to. Path is normalized UNCONDITIONALLY (backslash paths arrive on
|
|
7800
|
+
// Linux too — CONTEXT.md path-separator rule) and checked as a
|
|
7801
|
+
// path-segment match so the recursive descent (srcPath may be several
|
|
7802
|
+
// directory levels below gsd-core/workflows/) is still caught.
|
|
7803
|
+
//
|
|
7804
|
+
// #3072: the scoping predicate itself now lives in ONE place —
|
|
7805
|
+
// shouldCompose (src/mcp-catalog.cts, imported above) — rather than
|
|
7806
|
+
// being re-declared inline here. The MCP served catalog calls the
|
|
7807
|
+
// SAME function to decide what it composes vs serves verbatim, so this
|
|
7808
|
+
// install path and the catalog can never independently drift on what
|
|
7809
|
+
// gets composed (ADR-1671:309, DEFECT.GENERATIVE-FIX; the parity gate
|
|
7810
|
+
// is tests/mcp-catalog-parity.test.cjs). shouldCompose normalizes with
|
|
7811
|
+
// the identical unconditional `.replace(/\\/g, '/')` internally, so
|
|
7812
|
+
// this call is behavior-preserving byte-for-behavior with the regex it
|
|
7813
|
+
// replaces.
|
|
7814
|
+
if (shouldCompose(srcPath)) {
|
|
7815
|
+
content = composeWorkflow(content, { sourcePath: srcPath });
|
|
7816
|
+
}
|
|
7817
|
+
|
|
7748
7818
|
if (!dispatch.mdSkipGenericRewrite) {
|
|
7749
7819
|
const globalClaudeRegex = /~\/\.claude\//g;
|
|
7750
7820
|
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
@@ -8127,11 +8197,12 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8127
8197
|
|
|
8128
8198
|
// 1a-kimi. Non-layout Kimi side-effect (#2095 EoS/kimi Upgrade 1): kimi's
|
|
8129
8199
|
// native config.toml lives outside targetDir entirely (resolveKimiHooksTomlDir
|
|
8130
|
-
// resolves ~/.kimi
|
|
8200
|
+
// resolves ~/.kimi for kimi and ~/.kimi-code for kimi-code (#2755), a sibling
|
|
8201
|
+
// of targetDir's ~/.config/agents), so its
|
|
8131
8202
|
// cleanup can't be driven by anything under targetDir the way every other
|
|
8132
8203
|
// hook surface above is.
|
|
8133
8204
|
if (resolveInstallPlan(runtime).hooksSurface === 'kimi-hooks-toml') {
|
|
8134
|
-
const kimiHooksRoot = resolveKimiHooksTomlDir();
|
|
8205
|
+
const kimiHooksRoot = resolveKimiHooksTomlDir({ runtime });
|
|
8135
8206
|
const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
|
|
8136
8207
|
const kimiHooksCleanup = removeKimiHooksToml(kimiHooksTomlPath);
|
|
8137
8208
|
if (kimiHooksCleanup.changed) {
|
|
@@ -8178,23 +8249,24 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8178
8249
|
}
|
|
8179
8250
|
}
|
|
8180
8251
|
|
|
8252
|
+
// #2544: the marker now lives inside kimi's hooks/ dir — remove it
|
|
8253
|
+
// before the emptiness check below, or the dir would never prune.
|
|
8254
|
+
if (removeCommonJsMarker(kimiHooksDir)) {
|
|
8255
|
+
removedCount++;
|
|
8256
|
+
console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksDir}`);
|
|
8257
|
+
}
|
|
8258
|
+
|
|
8181
8259
|
try {
|
|
8182
8260
|
if (fs.readdirSync(kimiHooksDir).length === 0) fs.rmdirSync(kimiHooksDir);
|
|
8183
8261
|
} catch (_) { /* not empty — leave it */ }
|
|
8184
8262
|
}
|
|
8185
8263
|
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
removedCount++;
|
|
8193
|
-
console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksRoot}`);
|
|
8194
|
-
}
|
|
8195
|
-
} catch (e) {
|
|
8196
|
-
// Ignore read errors
|
|
8197
|
-
}
|
|
8264
|
+
// Retire the pre-#2544 marker at kimi's root (~/.kimi), where the bundle
|
|
8265
|
+
// used to write it. Exact content match — a user's own package.json in
|
|
8266
|
+
// kimi's native config home is never touched.
|
|
8267
|
+
if (removeCommonJsMarker(kimiHooksRoot)) {
|
|
8268
|
+
removedCount++;
|
|
8269
|
+
console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksRoot} (pre-#2544 marker)`);
|
|
8198
8270
|
}
|
|
8199
8271
|
}
|
|
8200
8272
|
|
|
@@ -8460,7 +8532,8 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8460
8532
|
}
|
|
8461
8533
|
|
|
8462
8534
|
// 4. Remove GSD hooks
|
|
8463
|
-
|
|
8535
|
+
// #3023: mirror the install site's descriptor-driven bundle dir name.
|
|
8536
|
+
const hooksDir = path.join(targetDir, resolveSharedHooksDirName(runtime));
|
|
8464
8537
|
if (fs.existsSync(hooksDir)) {
|
|
8465
8538
|
let hookCount = 0;
|
|
8466
8539
|
for (const hook of GSD_UNINSTALL_HOOKS) {
|
|
@@ -8502,13 +8575,18 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8502
8575
|
}
|
|
8503
8576
|
}
|
|
8504
8577
|
|
|
8505
|
-
//
|
|
8506
|
-
//
|
|
8507
|
-
// it
|
|
8508
|
-
//
|
|
8509
|
-
//
|
|
8578
|
+
// Retire the CommonJS marker staged into hooks/. hooks/ is shared space and
|
|
8579
|
+
// is deliberately never rmdir'd here, so the marker must be removed
|
|
8580
|
+
// explicitly or it would be left behind. Removed ONLY when it still carries
|
|
8581
|
+
// GSD's exact content — a user-authored package.json is never deleted.
|
|
8582
|
+
//
|
|
8583
|
+
// #2717 reaches the runtimes that stage .js hooks via dedicated paths
|
|
8584
|
+
// (cursor/windsurf/codex); #2544 reaches the shared-bundle runtimes, whose
|
|
8585
|
+
// marker this PR moves out of the config root and into hooks/. Both land in
|
|
8586
|
+
// the same directory, so one guarded call covers both.
|
|
8510
8587
|
try {
|
|
8511
8588
|
if (hooksSurface.removeCommonJsMarkerIfGsdOwned(hooksDir)) {
|
|
8589
|
+
removedCount++;
|
|
8512
8590
|
console.log(` ${green}✓${reset} Removed GSD hooks/package.json (CommonJS marker)`);
|
|
8513
8591
|
}
|
|
8514
8592
|
} catch { /* best-effort */ }
|
|
@@ -8523,12 +8601,38 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8523
8601
|
if (_np) {
|
|
8524
8602
|
const pluginsDir = path.join(targetDir, _np.dir);
|
|
8525
8603
|
const pluginPath = path.join(pluginsDir, _np.file);
|
|
8604
|
+
// Tracks whether GSD actually removed anything from pluginsDir. The rmdir
|
|
8605
|
+
// below is gated on it: pruning a directory GSD never wrote to is the same
|
|
8606
|
+
// "don't touch territory GSD didn't fill" violation this issue is about,
|
|
8607
|
+
// just inverted — a user-created but empty plugin/ or extensions/ dir is
|
|
8608
|
+
// theirs, and an uninstall that never removed anything has no business
|
|
8609
|
+
// deleting it.
|
|
8610
|
+
let removedFromPluginsDir = false;
|
|
8526
8611
|
if (fs.existsSync(pluginPath)) {
|
|
8527
8612
|
try {
|
|
8528
8613
|
fs.unlinkSync(pluginPath);
|
|
8529
8614
|
removedCount++;
|
|
8615
|
+
removedFromPluginsDir = true;
|
|
8530
8616
|
console.log(` ${green}✓${reset} Removed native plugin adapter (${runtime})`);
|
|
8531
8617
|
} catch (_) { /* best-effort */ }
|
|
8618
|
+
}
|
|
8619
|
+
// #2544: the adapter's CommonJS marker sits beside it. Cleaned up OUTSIDE
|
|
8620
|
+
// the adapter-exists guard above — a partial install (or a hand-deleted
|
|
8621
|
+
// adapter) would otherwise strand GSD's marker forever and keep the dir
|
|
8622
|
+
// from ever pruning. Conditioned on the adapter being GONE, though: if the
|
|
8623
|
+
// unlink above failed, pulling the marker out from under a still-present
|
|
8624
|
+
// CommonJS adapter would leave it unloadable. The exact content match
|
|
8625
|
+
// still leaves any user-authored package.json in place.
|
|
8626
|
+
if (!fs.existsSync(pluginPath) && removeCommonJsMarker(pluginsDir)) {
|
|
8627
|
+
removedCount++;
|
|
8628
|
+
removedFromPluginsDir = true;
|
|
8629
|
+
console.log(` ${green}✓${reset} Removed GSD package.json from ${_np.dir}/`);
|
|
8630
|
+
}
|
|
8631
|
+
// Only prune a dir GSD emptied. Pre-fix this rmdir sat inside the
|
|
8632
|
+
// adapter-exists guard, so it could never fire on a dir GSD had not
|
|
8633
|
+
// written to; hoisting it out to catch the marker-only case must not
|
|
8634
|
+
// silently widen it to "any empty plugin dir".
|
|
8635
|
+
if (removedFromPluginsDir) {
|
|
8532
8636
|
try { fs.rmdirSync(pluginsDir); } catch (_) { /* not empty — user plugins present */ }
|
|
8533
8637
|
}
|
|
8534
8638
|
}
|
|
@@ -8589,19 +8693,14 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8589
8693
|
}
|
|
8590
8694
|
|
|
8591
8695
|
// 5. Remove GSD package.json (CommonJS mode marker)
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
console.log(` ${green}✓${reset} Removed GSD package.json`);
|
|
8601
|
-
}
|
|
8602
|
-
} catch (e) {
|
|
8603
|
-
// Ignore read errors
|
|
8604
|
-
}
|
|
8696
|
+
// Since #2544 the marker is staged into hooks/ (and the nativePlugin dir,
|
|
8697
|
+
// handled at 4z above) rather than at targetDir. The targetDir removal is
|
|
8698
|
+
// retained to retire the marker written by pre-#2544 installs — same exact
|
|
8699
|
+
// content match as before, so a user-authored package.json is still never
|
|
8700
|
+
// touched.
|
|
8701
|
+
if (removeCommonJsMarker(targetDir)) {
|
|
8702
|
+
removedCount++;
|
|
8703
|
+
console.log(` ${green}✓${reset} Removed GSD package.json (pre-#2544 config-root marker)`);
|
|
8605
8704
|
}
|
|
8606
8705
|
|
|
8607
8706
|
// 6. Clean up settings.json (remove GSD hooks and statusline)
|
|
@@ -9533,7 +9632,10 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9533
9632
|
// #2100: Windsurf's exclusion is likewise descriptor-driven (windsurf declares
|
|
9534
9633
|
// skipSharedHooksInstall:true) — the redundant `&& !isWindsurf` was removed.
|
|
9535
9634
|
if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true) {
|
|
9536
|
-
|
|
9635
|
+
// #3023: manifest keys must track the bundle wherever the descriptor put it,
|
|
9636
|
+
// or uninstall/saveLocalPatches silently orphan the tree.
|
|
9637
|
+
const sharedHooksDirName = resolveSharedHooksDirName(runtime);
|
|
9638
|
+
const hooksDir = path.join(configDir, sharedHooksDirName);
|
|
9537
9639
|
if (fs.existsSync(hooksDir)) {
|
|
9538
9640
|
// Drive from INSTALLED_HOOK_FILES (the canonical HOOKS_TO_COPY set from
|
|
9539
9641
|
// scripts/build-hooks.js) rather than a prefix/extension regex, so the
|
|
@@ -9545,7 +9647,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9545
9647
|
for (const hook of INSTALLED_HOOK_FILES) {
|
|
9546
9648
|
const hookPath = path.join(hooksDir, hook);
|
|
9547
9649
|
if (fs.existsSync(hookPath)) {
|
|
9548
|
-
manifest.files['
|
|
9650
|
+
manifest.files[sharedHooksDirName + '/' + hook] = fileHash(hookPath);
|
|
9549
9651
|
}
|
|
9550
9652
|
}
|
|
9551
9653
|
// Track hooks/lib/ helpers so saveLocalPatches() can back up user edits
|
|
@@ -9554,7 +9656,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9554
9656
|
if (fs.existsSync(hooksLibDir)) {
|
|
9555
9657
|
for (const file of fs.readdirSync(hooksLibDir)) {
|
|
9556
9658
|
if (GSD_HOOK_LIB_FILES.includes(file)) {
|
|
9557
|
-
manifest.files['
|
|
9659
|
+
manifest.files[sharedHooksDirName + '/lib/' + file] = fileHash(path.join(hooksLibDir, file));
|
|
9558
9660
|
}
|
|
9559
9661
|
}
|
|
9560
9662
|
}
|
|
@@ -10614,8 +10716,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10614
10716
|
}
|
|
10615
10717
|
}
|
|
10616
10718
|
|
|
10617
|
-
// Descriptor-driven commands/ output report (
|
|
10618
|
-
//
|
|
10719
|
+
// Descriptor-driven commands/ output report (currently CodeBuddy).
|
|
10720
|
+
// Cursor retired this parallel surface in #2644 because its skills are
|
|
10721
|
+
// already slash-menu entries as well as model-invocable context.
|
|
10619
10722
|
if (_hostBehaviors(runtime).reportCommandsDir) {
|
|
10620
10723
|
const commandsDir = path.join(targetDir, 'commands');
|
|
10621
10724
|
if (fs.existsSync(commandsDir)) {
|
|
@@ -10880,7 +10983,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10880
10983
|
const agentEntries = fs.readdirSync(agentsSrc, { withFileTypes: true });
|
|
10881
10984
|
for (const entry of agentEntries) {
|
|
10882
10985
|
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
10883
|
-
|
|
10986
|
+
const agentSourcePath = path.join(agentsSrc, entry.name);
|
|
10987
|
+
let content = fs.readFileSync(agentSourcePath, 'utf8');
|
|
10988
|
+
// #2995 (epic #1671 Phase 6.4): strip `<!-- gsd:section -->` markers BEFORE
|
|
10989
|
+
// the path-rewrite regexes below, so a rewrite can never reach inside a
|
|
10990
|
+
// marker attribute. No-op (byte-identical) for an unmarked agent.
|
|
10991
|
+
content = composeWorkflow(content, { sourcePath: agentSourcePath });
|
|
10884
10992
|
// Replace ~/.claude/ and $HOME/.claude/ as they are the source of truth in the repo
|
|
10885
10993
|
const dirRegex = /~\/\.claude\//g;
|
|
10886
10994
|
const homeDirRegex = /\$HOME\/\.claude\//g;
|
|
@@ -11065,22 +11173,30 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11065
11173
|
// a safe no-op when the dir is already present.
|
|
11066
11174
|
fs.mkdirSync(destRootDir, { recursive: true });
|
|
11067
11175
|
|
|
11068
|
-
//
|
|
11069
|
-
//
|
|
11070
|
-
//
|
|
11071
|
-
const
|
|
11072
|
-
|
|
11073
|
-
|
|
11176
|
+
// #3023: the bundle's directory NAME is descriptor-driven — a host that
|
|
11177
|
+
// reserves `hooks/` (pi) must be able to opt out. Resolved once here so the
|
|
11178
|
+
// stage / lib / marker sites can never disagree about where the bundle is.
|
|
11179
|
+
const sharedHooksDirName = resolveSharedHooksDirName(runtime);
|
|
11180
|
+
|
|
11181
|
+
// #2544: the CommonJS marker is NOT written here (destRootDir is the
|
|
11182
|
+
// runtime's shared config root — user-writable territory on OpenCode and
|
|
11183
|
+
// Kilo, where it is the documented place to declare local-plugin npm
|
|
11184
|
+
// dependencies). It is written into hooks/ below, the directory GSD
|
|
11185
|
+
// creates and fills with its own .js scripts, once that directory exists.
|
|
11074
11186
|
|
|
11075
11187
|
let hooksOk = true;
|
|
11188
|
+
// #2544: true once GSD has actually written into destRootDir/hooks/, which
|
|
11189
|
+
// is what licenses the CommonJS marker below.
|
|
11190
|
+
let stagedHooks = false;
|
|
11076
11191
|
|
|
11077
11192
|
// Copy hooks from dist/ (bundled with dependencies)
|
|
11078
11193
|
// Template paths for the target runtime (replaces '.claude' with correct config dir)
|
|
11079
11194
|
const hooksSrc = path.join(src, 'hooks', 'dist');
|
|
11080
11195
|
if (fs.existsSync(hooksSrc)) {
|
|
11081
|
-
const hooksDest = path.join(destRootDir,
|
|
11196
|
+
const hooksDest = path.join(destRootDir, sharedHooksDirName);
|
|
11082
11197
|
fs.mkdirSync(hooksDest, { recursive: true });
|
|
11083
11198
|
const hookEntries = fs.readdirSync(hooksSrc);
|
|
11199
|
+
if (hookEntries.some((e) => fs.statSync(path.join(hooksSrc, e)).isFile())) stagedHooks = true;
|
|
11084
11200
|
const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
|
|
11085
11201
|
for (const entry of hookEntries) {
|
|
11086
11202
|
const srcFile = path.join(hooksSrc, entry);
|
|
@@ -11143,7 +11259,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11143
11259
|
}
|
|
11144
11260
|
}
|
|
11145
11261
|
if (verifyInstalled(hooksDest, 'hooks')) {
|
|
11146
|
-
console.log(` ${green}✓${reset} Installed
|
|
11262
|
+
console.log(` ${green}✓${reset} Installed ${sharedHooksDirName} (bundled)`);
|
|
11147
11263
|
// Warn if expected community .sh hooks are missing (non-fatal)
|
|
11148
11264
|
const expectedShHooks = ['gsd-session-state.sh', 'gsd-validate-commit.sh', 'gsd-phase-boundary.sh', 'gsd-graphify-update.sh'];
|
|
11149
11265
|
for (const sh of expectedShHooks) {
|
|
@@ -11172,10 +11288,48 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11172
11288
|
// below; this helper itself only checks source presence.)
|
|
11173
11289
|
const hooksLibSrc = path.join(src, 'hooks', 'lib');
|
|
11174
11290
|
if (fs.existsSync(hooksLibSrc)) {
|
|
11175
|
-
const hooksLibDest = path.join(destRootDir,
|
|
11291
|
+
const hooksLibDest = path.join(destRootDir, sharedHooksDirName, 'lib');
|
|
11176
11292
|
fs.mkdirSync(hooksLibDest, { recursive: true });
|
|
11177
11293
|
copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES);
|
|
11178
|
-
|
|
11294
|
+
if (GSD_HOOK_LIB_FILES.some((f) => fs.existsSync(path.join(hooksLibDest, f)))) stagedHooks = true;
|
|
11295
|
+
console.log(` ${green}✓${reset} Installed ${sharedHooksDirName}/lib/ helpers (git-cmd, graphify-rebuild, ...)`);
|
|
11296
|
+
}
|
|
11297
|
+
|
|
11298
|
+
// #2544: pin the staged hook scripts to CommonJS from inside hooks/ — the
|
|
11299
|
+
// directory GSD just created and filled — instead of from destRootDir.
|
|
11300
|
+
// Scoping the marker to GSD's own directory keeps `require` working in
|
|
11301
|
+
// hooks/*.js and hooks/lib/*.js under any ambient "type": "module", while
|
|
11302
|
+
// leaving the shared config root untouched.
|
|
11303
|
+
//
|
|
11304
|
+
// Gated on `stagedHooks`, NOT on the directory merely existing: hooks/ is
|
|
11305
|
+
// shared space, so an existence check would drop a GSD marker into a
|
|
11306
|
+
// hooks/ directory the user created and GSD never wrote to — the same
|
|
11307
|
+
// write-into-someone-else's-territory this issue is about. And never
|
|
11308
|
+
// written over a package.json GSD does not own.
|
|
11309
|
+
//
|
|
11310
|
+
// ALSO gated on `hooksOk`: `stagedHooks` is computed from the SOURCE
|
|
11311
|
+
// listing before the copy loop, so it stays true when the copies land but
|
|
11312
|
+
// `verifyInstalled` then fails. Marking a hooks/ GSD did not successfully
|
|
11313
|
+
// populate as CommonJS claims an ownership the install did not earn — the
|
|
11314
|
+
// two flags answer different questions ("did we intend to fill it" vs "is
|
|
11315
|
+
// it actually filled"), and the marker needs both.
|
|
11316
|
+
const hooksMarkerDir = path.join(destRootDir, sharedHooksDirName);
|
|
11317
|
+
if (stagedHooks && hooksOk) {
|
|
11318
|
+
switch (ensureCommonJsMarker(hooksMarkerDir)) {
|
|
11319
|
+
case 'written':
|
|
11320
|
+
console.log(` ${green}✓${reset} Wrote ${sharedHooksDirName}/package.json (CommonJS mode)`);
|
|
11321
|
+
break;
|
|
11322
|
+
case 'preserved-foreign':
|
|
11323
|
+
console.warn(` ${yellow}⚠${reset} Left existing ${sharedHooksDirName}/package.json untouched (not GSD's marker) — GSD hooks may not resolve as CommonJS`);
|
|
11324
|
+
break;
|
|
11325
|
+
case 'failed':
|
|
11326
|
+
// Best-effort: a read-only or full config dir must not abort the
|
|
11327
|
+
// install with a raw stack trace. The hooks themselves are staged.
|
|
11328
|
+
console.warn(` ${yellow}⚠${reset} Could not write ${sharedHooksDirName}/package.json (CommonJS mode) — install continued; GSD hooks may not resolve as CommonJS`);
|
|
11329
|
+
break;
|
|
11330
|
+
default:
|
|
11331
|
+
break;
|
|
11332
|
+
}
|
|
11179
11333
|
}
|
|
11180
11334
|
|
|
11181
11335
|
return hooksOk;
|
|
@@ -11586,6 +11740,10 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11586
11740
|
|
|
11587
11741
|
let agentCount = 0;
|
|
11588
11742
|
if (!isMinimalMode(_effectiveInstallMode)) {
|
|
11743
|
+
// #2834: write ~/.gsd/defaults.json (resolve_model_ids + runtime) BEFORE generating
|
|
11744
|
+
// agent TOMLs — installCodexConfig reads defaults.json at generation time, so on a
|
|
11745
|
+
// clean first install the runtime-aware model resolver must already know the runtime.
|
|
11746
|
+
writeNonClaudeDefaults(runtime);
|
|
11589
11747
|
try {
|
|
11590
11748
|
// Generate Codex config.toml and per-agent .toml files.
|
|
11591
11749
|
agentCount = installCodexConfig(targetDir, agentsSrc, plan.sandboxTier);
|
|
@@ -11622,6 +11780,10 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11622
11780
|
const codexHooksDest = path.join(targetDir, 'hooks');
|
|
11623
11781
|
fs.mkdirSync(codexHooksDest, { recursive: true });
|
|
11624
11782
|
const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
|
|
11783
|
+
// #2544: track whether anything was actually staged. hooks/dist existing
|
|
11784
|
+
// is not the same as an allowlisted file landing in it — see the marker
|
|
11785
|
+
// gate below.
|
|
11786
|
+
let codexStagedHooks = false;
|
|
11625
11787
|
for (const entry of fs.readdirSync(codexHooksSrc)) {
|
|
11626
11788
|
if (!CODEX_HOOKS_TO_COPY.includes(entry)) continue;
|
|
11627
11789
|
const srcFile = path.join(codexHooksSrc, entry);
|
|
@@ -11655,6 +11817,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11655
11817
|
fs.copyFileSync(srcFile, destFile);
|
|
11656
11818
|
try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows */ }
|
|
11657
11819
|
}
|
|
11820
|
+
codexStagedHooks = true;
|
|
11658
11821
|
}
|
|
11659
11822
|
console.log(` ${green}✓${reset} Installed hooks (Codex)`);
|
|
11660
11823
|
// #2717: write the CommonJS marker into hooks/ alongside the staged .js
|
|
@@ -11665,7 +11828,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11665
11828
|
// gsd-context-monitor.js as ESM and their require() calls fail silently.
|
|
11666
11829
|
// Reuses the same helper the Cursor/Windsurf writers call so the marker
|
|
11667
11830
|
// content + user-file-preservation contract is identical everywhere.
|
|
11668
|
-
|
|
11831
|
+
//
|
|
11832
|
+
// #2544: gated on codexStagedHooks, mirroring installSharedHooksBundle's
|
|
11833
|
+
// `stagedHooks`. The enclosing guard only proves hooks/dist EXISTS; if it
|
|
11834
|
+
// holds none of CODEX_HOOKS_TO_COPY, this block mkdirs hooks/ and stages
|
|
11835
|
+
// nothing, and an ungated marker would claim a directory GSD did not fill.
|
|
11836
|
+
if (codexStagedHooks && hooksSurface.ensureCommonJsMarker(codexHooksDest)) {
|
|
11669
11837
|
console.log(` ${green}✓${reset} Wrote hooks/package.json (CommonJS mode)`);
|
|
11670
11838
|
}
|
|
11671
11839
|
}
|
|
@@ -11886,7 +12054,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11886
12054
|
// hooks needed. Kimi is also artifact-only for its INSTALL surface (skills +
|
|
11887
12055
|
// kimi-agents, no settings.json) but #2095 Upgrade 1 gives it its own
|
|
11888
12056
|
// independent hooksSurface: kimi's native config.toml [[hooks]] array, which
|
|
11889
|
-
// lives outside targetDir entirely (resolveKimiHooksTomlDir resolves
|
|
12057
|
+
// lives outside targetDir entirely (resolveKimiHooksTomlDir resolves the
|
|
12058
|
+
// per-runtime root — ~/.kimi for kimi, ~/.kimi-code for kimi-code, #2755 —
|
|
11890
12059
|
// a sibling of targetDir's ~/.config/agents) — hence writing it here, inside
|
|
11891
12060
|
// this early-return, rather than requiring installSurface to change.
|
|
11892
12061
|
//
|
|
@@ -11907,7 +12076,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11907
12076
|
// ~/.kimi/hooks/<script> rather than a script that doesn't exist under
|
|
11908
12077
|
// targetDir/hooks (which kimi no longer receives).
|
|
11909
12078
|
if (plan.hooksSurface === 'kimi-hooks-toml' && isGlobal) {
|
|
11910
|
-
const kimiHooksRoot = resolveKimiHooksTomlDir();
|
|
12079
|
+
const kimiHooksRoot = resolveKimiHooksTomlDir({ runtime });
|
|
11911
12080
|
// Note: the `failures` array's hard-fail gate (`if (failures.length > 0)
|
|
11912
12081
|
// process.exit(1)`) runs earlier in this function, before this
|
|
11913
12082
|
// profile-marker-only branch is ever reached — pushing to it here would
|
|
@@ -11916,6 +12085,20 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11916
12085
|
if (!installSharedHooksBundle(kimiHooksRoot)) {
|
|
11917
12086
|
console.warn(` ${yellow}⚠${reset} Kimi hook bundle did not verify at ${path.join(kimiHooksRoot, 'hooks')} — GSD lifecycle hooks may be incomplete`);
|
|
11918
12087
|
}
|
|
12088
|
+
// #2544: retire the pre-fix marker at kimi's root. installSharedHooksBundle
|
|
12089
|
+
// used to write {"type":"commonjs"} at destRootDir itself; it now writes it
|
|
12090
|
+
// under destRootDir/hooks/, so on an upgrade the old root file is stale and
|
|
12091
|
+
// would keep ~/.kimi pinned to CommonJS.
|
|
12092
|
+
//
|
|
12093
|
+
// Done HERE rather than in installer-migration 007 (which retires the same
|
|
12094
|
+
// stale marker for every other runtime) because kimi's hook root is
|
|
12095
|
+
// the per-runtime Kimi root — resolved by resolveKimiHooksTomlDir, OUTSIDE the configDir.
|
|
12096
|
+
// Migration relPaths are structurally confined to configDir, so the
|
|
12097
|
+
// framework cannot address this path at all. Same exact-content predicate
|
|
12098
|
+
// either way, so a user-authored ~/.kimi/package.json is never touched.
|
|
12099
|
+
if (removeCommonJsMarker(kimiHooksRoot)) {
|
|
12100
|
+
console.log(` ${green}✓${reset} Removed stale package.json from ${kimiHooksRoot} (pre-#2544 marker)`);
|
|
12101
|
+
}
|
|
11919
12102
|
const kimiHookOpts = { portableHooks: hasPortableHooks, runtime };
|
|
11920
12103
|
const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
|
|
11921
12104
|
const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts });
|
|
@@ -12361,58 +12544,11 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
12361
12544
|
configureAntigravityMcpConfig(isGlobal, configDir);
|
|
12362
12545
|
}
|
|
12363
12546
|
|
|
12364
|
-
//
|
|
12365
|
-
//
|
|
12366
|
-
//
|
|
12367
|
-
//
|
|
12368
|
-
|
|
12369
|
-
// chat model instead of pinning the resolved model. See #1156 (default-to-omit
|
|
12370
|
-
// intent) and #1569 (preserve explicit true). Guard matches the #130-class pattern
|
|
12371
|
-
// on configureOpencodePermissions above.
|
|
12372
|
-
if (!_hostBehaviors(runtime).nativeModelAliases && !process.env.GSD_TEST_MODE) {
|
|
12373
|
-
const gsdDir = path.join(os.homedir(), '.gsd');
|
|
12374
|
-
const defaultsPath = path.join(gsdDir, 'defaults.json');
|
|
12375
|
-
try {
|
|
12376
|
-
fs.mkdirSync(gsdDir, { recursive: true });
|
|
12377
|
-
let defaults = {};
|
|
12378
|
-
try { defaults = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')); } catch { /* new file */ }
|
|
12379
|
-
// Recover a malformed (valid-JSON-but-non-object) defaults.json to a fresh object so
|
|
12380
|
-
// the write below succeeds and the file is no longer broken. Without this, `null` /
|
|
12381
|
-
// `[]` / a number / a string bypass the parse catch and either throw a TypeError on
|
|
12382
|
-
// property access (swallowed by the outer try/catch, leaving the file broken) or get
|
|
12383
|
-
// a property set that won't round-trip through JSON.stringify. (#1657)
|
|
12384
|
-
if (defaults === null || typeof defaults !== 'object' || Array.isArray(defaults)) {
|
|
12385
|
-
defaults = {};
|
|
12386
|
-
}
|
|
12387
|
-
// Three-valued domain: false/absent → aliases; true → full IDs; "omit" → ''.
|
|
12388
|
-
// Honor ONLY an explicit canonical `true` opt-in (full model IDs) and an existing
|
|
12389
|
-
// "omit"; default everything else — absent, falsy, OR any non-canonical value — to
|
|
12390
|
-
// "omit", the safe non-Claude default. Allowlist-based so malformed values
|
|
12391
|
-
// (0, "", "yes", {}, …) don't leak Claude aliases the runtime can't resolve (#1569).
|
|
12392
|
-
const existing = defaults.resolve_model_ids;
|
|
12393
|
-
const shouldDefaultToOmit = existing !== true && existing !== 'omit';
|
|
12394
|
-
if (shouldDefaultToOmit) {
|
|
12395
|
-
defaults.resolve_model_ids = 'omit';
|
|
12396
|
-
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
|
|
12397
|
-
console.log(` ${green}✓${reset} Set resolve_model_ids: "omit" in ~/.gsd/defaults.json`);
|
|
12398
|
-
}
|
|
12399
|
-
|
|
12400
|
-
// #2395: also persist `runtime: <runtime>` for non-Claude runtimes, so
|
|
12401
|
-
// resolveRuntime() (precedence: GSD_RUNTIME env > config.runtime > 'claude')
|
|
12402
|
-
// resolves to the install's actual runtime identity out of the box — without
|
|
12403
|
-
// this, agent_runtime and every runtime-branded slash hint falls through to
|
|
12404
|
-
// the hard-coded 'claude' default. Mirrors the resolve_model_ids write above:
|
|
12405
|
-
// honor an explicit pre-existing value (any string), only default-populating
|
|
12406
|
-
// when absent. Claude is the resolveRuntime() fallback, so it needs no write.
|
|
12407
|
-
if (defaults.runtime === undefined || defaults.runtime === null || defaults.runtime === '') {
|
|
12408
|
-
defaults.runtime = runtime;
|
|
12409
|
-
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
|
|
12410
|
-
console.log(` ${green}✓${reset} Set runtime: "${runtime}" in ~/.gsd/defaults.json`);
|
|
12411
|
-
}
|
|
12412
|
-
} catch (e) {
|
|
12413
|
-
console.log(` ${yellow}⚠${reset} Could not write ~/.gsd/defaults.json: ${e.message}`);
|
|
12414
|
-
}
|
|
12415
|
-
}
|
|
12547
|
+
// #2834: defaults.json (resolve_model_ids + runtime) is now written BEFORE
|
|
12548
|
+
// installCodexConfig via writeNonClaudeDefaults(runtime) — extracted into a
|
|
12549
|
+
// function so it can run at the right point in the flow (before agent TOML
|
|
12550
|
+
// generation reads it). This call is idempotent (preserves existing values).
|
|
12551
|
+
writeNonClaudeDefaults(runtime);
|
|
12416
12552
|
|
|
12417
12553
|
// program + command are now single-source lookups (ADR-1239 Phase B / #1679):
|
|
12418
12554
|
// program is the runtime display label; command is the per-host /gsd-new-project
|
|
@@ -13075,14 +13211,26 @@ function maybeSuggestPathExport(globalBin, homeDir) {
|
|
|
13075
13211
|
|
|
13076
13212
|
console.log('');
|
|
13077
13213
|
console.log(` ${yellow}⚠${reset} ${bold}${globalBin}${reset} is not on your PATH.`);
|
|
13078
|
-
console.log(` Add it with one of:`);
|
|
13079
13214
|
const projected = projectPersistentPathExportActions({
|
|
13080
13215
|
targetDir: globalBin,
|
|
13081
13216
|
platform: process.platform,
|
|
13082
13217
|
});
|
|
13083
|
-
|
|
13084
|
-
|
|
13085
|
-
|
|
13218
|
+
if (projected.reason === PATH_ACTION_REASON.WIN32_RESERVED_QUOTE) {
|
|
13219
|
+
// #3118 review MINOR: a win32 targetDir containing `"` makes
|
|
13220
|
+
// projectPathActionProjection return [] (no command can quote it safely
|
|
13221
|
+
// on Windows) — printing the "Add it with one of:" header with nothing
|
|
13222
|
+
// under it is a silent dead-end. Name the cause instead.
|
|
13223
|
+
console.log(` No command can be suggested: the path contains a ${cyan}"${reset} character, which cannot appear in a Windows path.`);
|
|
13224
|
+
} else if (projected.shellActions.length === 0) {
|
|
13225
|
+
// #3118: no target directory to talk about (reason === NO_TARGET_DIR, or
|
|
13226
|
+
// no reason at all) — there is nothing to print beyond the "not on your
|
|
13227
|
+
// PATH" line above.
|
|
13228
|
+
} else {
|
|
13229
|
+
console.log(` Add it with one of:`);
|
|
13230
|
+
for (const action of projected.shellActions) {
|
|
13231
|
+
const labelPrefix = action.label ? `${action.label}: ` : '';
|
|
13232
|
+
console.log(` ${cyan}${labelPrefix}${action.command}${reset}`);
|
|
13233
|
+
}
|
|
13086
13234
|
}
|
|
13087
13235
|
console.log('');
|
|
13088
13236
|
}
|
|
@@ -13297,7 +13445,6 @@ module.exports = {
|
|
|
13297
13445
|
applyRuntimeContentRewritesInPlace,
|
|
13298
13446
|
getCodexSkillAdapterHeader,
|
|
13299
13447
|
convertClaudeCommandToCursorSkill,
|
|
13300
|
-
convertClaudeCommandToCursorCommand,
|
|
13301
13448
|
convertClaudeAgentToCursorAgent,
|
|
13302
13449
|
convertClaudeAgentToCodexAgent,
|
|
13303
13450
|
generateCodexAgentToml,
|
|
@@ -13332,6 +13479,9 @@ module.exports = {
|
|
|
13332
13479
|
// #2086 — host-behavior resolution + the #338 privacy fail-safe floor (exported for tests)
|
|
13333
13480
|
_resolveHostBehaviors,
|
|
13334
13481
|
FALLBACK_HOST_BEHAVIORS,
|
|
13482
|
+
// #3023 — shared hook bundle directory name, descriptor-driven
|
|
13483
|
+
SHARED_HOOKS_DIR_DEFAULT,
|
|
13484
|
+
resolveSharedHooksDirName,
|
|
13335
13485
|
convertSlashCommandsToCodexSkillMentions,
|
|
13336
13486
|
convertClaudeCommandToCodexSkill,
|
|
13337
13487
|
convertClaudeCommandToKimiSkill,
|
|
@@ -13513,7 +13663,19 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
|
|
|
13513
13663
|
console.error('Usage: node install.js --skills-root <runtime>');
|
|
13514
13664
|
process.exit(1);
|
|
13515
13665
|
}
|
|
13516
|
-
|
|
13666
|
+
// #3024: validate the runtime id against the shipped capability registry
|
|
13667
|
+
// BEFORE resolving anything. getGlobalSkillsBase's bare `runtimes[runtime]`
|
|
13668
|
+
// lookup falls through the prototype chain to claude's skills root for an
|
|
13669
|
+
// unregistered/hostile id (`__proto__`, `constructor`, `prototype`, …)
|
|
13670
|
+
// instead of failing loudly. isRegisteredRuntimeId is the SAME validator
|
|
13671
|
+
// gsd-tools' `routeSkillsRoot` calls, so this entry point and the shipped
|
|
13672
|
+
// `gsd-tools query skills-root` entry point can never diverge on which
|
|
13673
|
+
// runtime ids they accept.
|
|
13674
|
+
if (!isRegisteredRuntimeId(runtimeArg)) {
|
|
13675
|
+
console.error(`Unknown runtime "${runtimeArg}" — must be a registered runtime id`);
|
|
13676
|
+
process.exit(1);
|
|
13677
|
+
}
|
|
13678
|
+
const skillsRoot = getGlobalSkillsBase(runtimeArg.trim());
|
|
13517
13679
|
if (skillsRoot === null) {
|
|
13518
13680
|
console.error(`${runtimeArg} does not use a skills directory`);
|
|
13519
13681
|
process.exit(1);
|