@opengsd/gsd-core 1.5.0 → 1.6.0-rc.2

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.
Files changed (95) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-plan-checker.md +34 -0
  3. package/agents/gsd-planner.md +2 -0
  4. package/agents/gsd-roadmapper.md +6 -0
  5. package/bin/install.js +199 -365
  6. package/commands/gsd/capture.md +5 -1
  7. package/gemini-extension.json +1 -1
  8. package/gsd-core/bin/gsd-tools.cjs +695 -5
  9. package/gsd-core/bin/lib/adr-parser.cjs +45 -23
  10. package/gsd-core/bin/lib/audit.cjs +2 -2
  11. package/gsd-core/bin/lib/capability-consent.cjs +763 -0
  12. package/gsd-core/bin/lib/capability-ledger.cjs +831 -0
  13. package/gsd-core/bin/lib/capability-lifecycle.cjs +1551 -0
  14. package/gsd-core/bin/lib/capability-loader.cjs +764 -0
  15. package/gsd-core/bin/lib/capability-lock.cjs +553 -0
  16. package/gsd-core/bin/lib/capability-registry.cjs +198 -4
  17. package/gsd-core/bin/lib/capability-source.cjs +1242 -0
  18. package/gsd-core/bin/lib/capability-state.cjs +9 -6
  19. package/gsd-core/bin/lib/capability-trust.cjs +550 -0
  20. package/gsd-core/bin/lib/capability-validator.cjs +2066 -0
  21. package/gsd-core/bin/lib/capability-writer.cjs +14 -5
  22. package/gsd-core/bin/lib/check-command-router.cjs +69 -18
  23. package/gsd-core/bin/lib/command-aliases.cjs +8 -0
  24. package/gsd-core/bin/lib/commands.cjs +247 -0
  25. package/gsd-core/bin/lib/config-loader.cjs +98 -84
  26. package/gsd-core/bin/lib/config-schema.cjs +26 -7
  27. package/gsd-core/bin/lib/config.cjs +7 -1
  28. package/gsd-core/bin/lib/decisions.cjs +149 -60
  29. package/gsd-core/bin/lib/frontmatter.cjs +7 -3
  30. package/gsd-core/bin/lib/gap-checker.cjs +126 -11
  31. package/gsd-core/bin/lib/init.cjs +91 -22
  32. package/gsd-core/bin/lib/legacy-cleanup.cjs +96 -0
  33. package/gsd-core/bin/lib/loop-resolver.cjs +26 -2
  34. package/gsd-core/bin/lib/markdown-sectionizer.cjs +471 -0
  35. package/gsd-core/bin/lib/milestone.cjs +41 -2
  36. package/gsd-core/bin/lib/phase-command-router.cjs +5 -0
  37. package/gsd-core/bin/lib/phase-id.cjs +25 -11
  38. package/gsd-core/bin/lib/phase-lifecycle.cjs +14 -5
  39. package/gsd-core/bin/lib/phase.cjs +33 -4
  40. package/gsd-core/bin/lib/probe-core.cjs +7 -0
  41. package/gsd-core/bin/lib/prohibition-enforcement.cjs +59 -26
  42. package/gsd-core/bin/lib/project-root.cjs +89 -2
  43. package/gsd-core/bin/lib/resolution.cjs +26 -0
  44. package/gsd-core/bin/lib/roadmap-command-router.cjs +16 -3
  45. package/gsd-core/bin/lib/roadmap-parser.cjs +73 -106
  46. package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -17
  47. package/gsd-core/bin/lib/roadmap.cjs +5 -2
  48. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +423 -3
  49. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +77 -0
  50. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -28
  51. package/gsd-core/bin/lib/runtime-homes.cjs +53 -1
  52. package/gsd-core/bin/lib/runtime-name-policy.cjs +44 -0
  53. package/gsd-core/bin/lib/semver-compare.cjs +127 -0
  54. package/gsd-core/bin/lib/shell-command-projection.cjs +55 -1
  55. package/gsd-core/bin/lib/state-document.cjs +4 -2
  56. package/gsd-core/bin/lib/state.cjs +317 -161
  57. package/gsd-core/bin/lib/surface.cjs +12 -19
  58. package/gsd-core/bin/lib/uat-predicate.cjs +7 -47
  59. package/gsd-core/bin/lib/uat.cjs +39 -26
  60. package/gsd-core/bin/lib/validate.cjs +5 -2
  61. package/gsd-core/bin/lib/verify.cjs +40 -15
  62. package/gsd-core/bin/lib/worktree-safety.cjs +202 -0
  63. package/gsd-core/bin/shared/config-defaults.manifest.json +6 -1
  64. package/gsd-core/bin/shared/config-schema.manifest.json +5 -1
  65. package/gsd-core/references/context-budget.md +8 -8
  66. package/gsd-core/references/execute-phase-between-wave-reset.md +43 -0
  67. package/gsd-core/references/execute-phase-context-guard.md +16 -0
  68. package/gsd-core/references/execute-phase-wave-guard.md +33 -0
  69. package/gsd-core/references/planner-antipatterns.md +48 -0
  70. package/gsd-core/references/planning-config.md +4 -0
  71. package/gsd-core/references/prohibition-probe.md +15 -9
  72. package/gsd-core/references/scout-codebase.md +2 -2
  73. package/gsd-core/workflows/autonomous.md +33 -33
  74. package/gsd-core/workflows/diagnose-issues.md +6 -1
  75. package/gsd-core/workflows/discuss-phase/templates/context.md +1 -1
  76. package/gsd-core/workflows/discuss-phase.md +1 -2
  77. package/gsd-core/workflows/execute-phase.md +12 -12
  78. package/gsd-core/workflows/help/modes/full.md +10 -0
  79. package/gsd-core/workflows/list-seeds.md +63 -0
  80. package/gsd-core/workflows/manager.md +37 -37
  81. package/gsd-core/workflows/pr-branch.md +156 -0
  82. package/gsd-core/workflows/quick.md +6 -1
  83. package/gsd-core/workflows/review.md +10 -2
  84. package/gsd-core/workflows/spec-phase.md +8 -3
  85. package/gsd-core/workflows/verify-phase.md +2 -2
  86. package/package.json +6 -3
  87. package/scripts/gen-capability-matrix.cjs +284 -0
  88. package/scripts/gen-capability-registry.cjs +96 -1853
  89. package/scripts/lint-regression-test-names.allowlist.json +1 -0
  90. package/scripts/lint-resolution-provenance.allowlist.json +1 -0
  91. package/scripts/lint-resolution-provenance.cjs +192 -0
  92. package/scripts/lint-test-file-count.allowlist.json +9 -0
  93. package/scripts/prompt-injection-scan.sh +1 -0
  94. package/scripts/run-tests.cjs +14 -0
  95. package/scripts/sync-manifest-versions.cjs +77 -5
@@ -19,9 +19,52 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
19
19
  * Installer Module for conversion behavior.
20
20
  */
21
21
  const node_path_1 = __importDefault(require("node:path"));
22
+ const node_os_1 = __importDefault(require("node:os"));
23
+ const node_fs_1 = __importDefault(require("node:fs"));
22
24
  const commandRoster = require("./command-roster.cjs");
23
25
  const { readGsdCommandNames, transformContentToHyphen } = commandRoster;
24
- const pkg = require('../../../package.json');
26
+ const runtimeNamePolicy = require("./runtime-name-policy.cjs");
27
+ const { getDirName } = runtimeNamePolicy;
28
+ // #1383: resolve GSD's version WITHOUT a top-level
29
+ // `require('../../../package.json')`. That require ran at module load on every
30
+ // gsd-tools invocation (this module sits in the gsd-tools loader chain) and
31
+ // threw `Cannot find module '../../../package.json'` on runtimes whose root has
32
+ // no package.json — notably Codex, where the installer omits the synthetic root
33
+ // package.json — taking the entire CLI down before it did anything. And even
34
+ // where it resolved (Claude's synthetic `{"type":"commonjs"}`), there is no
35
+ // `version` field, so the single consumer below already emitted
36
+ // `version: undefined`. Resolve lazily and defensively instead:
37
+ // 1. Installed trees carry <root>/gsd-core/VERSION (written by the installer);
38
+ // this module lives at <root>/gsd-core/bin/lib, so VERSION is two dirs up.
39
+ // 2. The source / npm-package tree has no gsd-core/VERSION but carries a real
40
+ // package.json three dirs up — read it lazily, never at module-load time.
41
+ // A failed/invalid lookup degrades to '' (the caller omits the field) rather
42
+ // than crashing or emitting `version: undefined`. Both sources are validated
43
+ // against the same semver shape the repo's other VERSION reader enforces
44
+ // (src/update-context.cts) so a garbled VERSION file is never emitted verbatim.
45
+ // Exported for the #1383 regression.
46
+ const SEMVER_PREFIX = /^\d+\.\d+\.\d+/; // mirrors src/update-context.cts SEMVER_PREFIX
47
+ function resolveVersionFrom(libDir) {
48
+ try {
49
+ const v = node_fs_1.default.readFileSync(node_path_1.default.join(libDir, '..', '..', 'VERSION'), 'utf8').trim();
50
+ if (SEMVER_PREFIX.test(v))
51
+ return v;
52
+ }
53
+ catch { /* not an installed tree (no gsd-core/VERSION) */ }
54
+ try {
55
+ const pkg = require(node_path_1.default.join(libDir, '..', '..', '..', 'package.json'));
56
+ if (pkg && typeof pkg.version === 'string' && SEMVER_PREFIX.test(pkg.version))
57
+ return pkg.version;
58
+ }
59
+ catch { /* runtime root has no package.json (e.g. Codex) */ }
60
+ return '';
61
+ }
62
+ let cachedVersion;
63
+ function gsdVersion() {
64
+ if (cachedVersion === undefined)
65
+ cachedVersion = resolveVersionFrom(__dirname);
66
+ return cachedVersion;
67
+ }
25
68
  const colorNameToHex = {
26
69
  cyan: '#00FFFF',
27
70
  red: '#FF0000',
@@ -363,8 +406,11 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
363
406
  // Hermes' SKILL.md spec lists `version` as a required frontmatter field.
364
407
  // Track GSD's package version so Hermes' skill_view() reports a stable
365
408
  // identifier per install.
366
- if (runtime === 'hermes')
367
- fm += `version: ${yamlQuote(pkg.version)}\n`;
409
+ if (runtime === 'hermes') {
410
+ const version = gsdVersion();
411
+ if (version)
412
+ fm += `version: ${yamlQuote(version)}\n`;
413
+ }
368
414
  // #778 (b) — Qwen-only numeric priority for /skills ordering. Scoped to qwen
369
415
  // so Claude/Hermes skill frontmatter is unchanged (they ignore the field, but
370
416
  // we keep their output byte-stable). skillName is the `gsd-<stem>` dir name.
@@ -1895,7 +1941,366 @@ function convertClaudeCommandToOpencodeSkill(content, skillName) {
1895
1941
  function convertClaudeCommandToKiloSkill(content, skillName) {
1896
1942
  return convertClaudeCommandToOpencodeFamilySkill(content, skillName, (c) => convertClaudeToKiloFrontmatter(c));
1897
1943
  }
1944
+ // ── Rewrite engine — ADR-1508 Phase 2 ───────────────────────────────────────
1945
+ // Relocated from bin/install.js (#1511). Behavior is byte-for-behavior identical
1946
+ // to the originals; the only change is the injected `attribution` 5th param in
1947
+ // _applyRuntimeRewrites (replacing the internal getCommitAttribution() call).
1948
+ /**
1949
+ * Compute the path prefix for a runtime install.
1950
+ * Global installs under $HOME use $HOME/... form; others use the resolved target.
1951
+ * isOpencode excludes OpenCode (uses ~/.config/opencode which breaks $HOME shorthand).
1952
+ * isWindowsHost is not used today but reserved for future Windows-specific logic.
1953
+ *
1954
+ * @private — exported as `_computePathPrefix` for tests.
1955
+ */
1956
+ function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost, resolvedTarget, homeDir }) {
1957
+ if (isGlobal && resolvedTarget.startsWith(homeDir) && !isOpencode) {
1958
+ return '$HOME' + resolvedTarget.slice(homeDir.length) + '/';
1959
+ }
1960
+ return `${resolvedTarget}/`;
1961
+ }
1962
+ /**
1963
+ * Canonical list of every non-Claude runtime that gsd-core emits artifacts for.
1964
+ * Exported so test files can import this single source of truth rather than
1965
+ * maintaining divergent hand-rolled arrays (#1521).
1966
+ *
1967
+ * Keep in sync with the runtime flags in bin/install.js and getDirName().
1968
+ */
1969
+ const NON_CLAUDE_RUNTIMES = [
1970
+ 'codex', 'opencode', 'kilo', 'gemini', 'copilot', 'antigravity',
1971
+ 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'kimi',
1972
+ 'codebuddy', 'cline',
1973
+ ];
1974
+ /**
1975
+ * #1521: Every non-Claude runtime resolves its own runtime identity from a
1976
+ * runtime-neutral config, and defaults workflow.use_worktrees to false —
1977
+ * GSD's worktree isolation uses Claude Code's isolation="worktree" spawn
1978
+ * parameter, which no other runtime honors. Stamped into the emitted
1979
+ * workflow runtime-resolution blocks. (Generalizes the Codex-only #1515 fix.)
1980
+ *
1981
+ * @private — exported as `_stampNonClaudeRuntimeDefaults` for tests.
1982
+ */
1983
+ function _stampNonClaudeRuntimeDefaults(content, runtime) {
1984
+ content = content.replace(/config-get workflow\.use_worktrees --raw 2>\/dev\/null \|\| echo "true"/g, 'config-get workflow.use_worktrees --default false --raw 2>/dev/null || echo "false"');
1985
+ content = content.replace(/config-get runtime --default claude --raw 2>\/dev\/null \|\| echo "claude"/g, `config-get runtime --default ${runtime} --raw 2>/dev/null || echo "${runtime}"`);
1986
+ return content;
1987
+ }
1988
+ /**
1989
+ * Apply the per-runtime rewrite table to a single content string.
1990
+ * Relocated from bin/install.js `_applyRuntimeRewrites`.
1991
+ *
1992
+ * The 5th `attribution` param replaces the internal getCommitAttribution() call
1993
+ * so the function is pure (no config I/O). Pass the resolved attribution value
1994
+ * from the installer; pass `undefined` to leave Co-Authored-By lines untouched.
1995
+ *
1996
+ * @private — exported as `_applyRuntimeRewrites` for tests.
1997
+ */
1998
+ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, attribution = undefined) {
1999
+ const dirName = getDirName(runtime);
2000
+ const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
2001
+ // #1521: stamp runtime identity + use_worktrees=false for every non-Claude runtime
2002
+ // before brand-specific path rewrites, so the replace operates on the pristine
2003
+ // source line and is idempotent regardless of subsequent path substitutions.
2004
+ if (runtime !== 'claude') {
2005
+ content = _stampNonClaudeRuntimeDefaults(content, runtime);
2006
+ }
2007
+ switch (runtime) {
2008
+ case 'codex':
2009
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
2010
+ content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2011
+ content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2012
+ content = content.replace(/~\/\.codex\//g, pathPrefix);
2013
+ // #1515 stamp moved to _stampNonClaudeRuntimeDefaults (#1521 generalisation).
2014
+ content = processAttribution(content, attribution);
2015
+ break;
2016
+ case 'cline':
2017
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
2018
+ content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2019
+ content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2020
+ content = content.replace(/~\/\.cline\//g, pathPrefix);
2021
+ content = content.replace(/\$HOME\/\.cline\//g, pathPrefix);
2022
+ content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
2023
+ content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
2024
+ content = content.replace(/~\/\.cline\b/g, normalizedPathPrefix);
2025
+ content = content.replace(/\$HOME\/\.cline\b/g, normalizedPathPrefix);
2026
+ content = processAttribution(content, attribution);
2027
+ break;
2028
+ case 'cursor':
2029
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
2030
+ content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2031
+ content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2032
+ content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
2033
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
2034
+ content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
2035
+ content = content.replace(/~\/\.cursor\//g, pathPrefix);
2036
+ content = processAttribution(content, attribution);
2037
+ break;
2038
+ case 'windsurf': {
2039
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
2040
+ content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2041
+ content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2042
+ content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
2043
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
2044
+ content = content.replace(/~\/\.codeium\/windsurf\//g, pathPrefix);
2045
+ if (isGlobal) {
2046
+ content = content.replace(/\.devin\/skills\//g, `${pathPrefix}skills/`);
2047
+ content = content.replace(/\.\/\.devin\//g, pathPrefix);
2048
+ content = content.replace(/~\/\.devin(?![\w-])/g, normalizedPathPrefix);
2049
+ content = content.replace(/\$HOME\/\.devin(?![\w-])/g, normalizedPathPrefix);
2050
+ }
2051
+ content = processAttribution(content, attribution);
2052
+ break;
2053
+ }
2054
+ case 'augment':
2055
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
2056
+ content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2057
+ content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2058
+ content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
2059
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
2060
+ content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
2061
+ content = content.replace(/~\/\.augment\//g, pathPrefix);
2062
+ content = content.replace(/\$HOME\/\.augment\//g, pathPrefix);
2063
+ content = content.replace(/~\/\.augment(?![\w-])/g, normalizedPathPrefix);
2064
+ content = content.replace(/\$HOME\/\.augment(?![\w-])/g, normalizedPathPrefix);
2065
+ content = processAttribution(content, attribution);
2066
+ break;
2067
+ case 'trae':
2068
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
2069
+ content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2070
+ content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2071
+ content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
2072
+ content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
2073
+ content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
2074
+ content = content.replace(/~\/\.trae\//g, pathPrefix);
2075
+ content = processAttribution(content, attribution);
2076
+ break;
2077
+ case 'codebuddy':
2078
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
2079
+ content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2080
+ content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2081
+ content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
2082
+ content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
2083
+ content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
2084
+ content = content.replace(/~\/\.codebuddy\//g, pathPrefix);
2085
+ content = content.replace(/\$HOME\/\.codebuddy\//g, pathPrefix);
2086
+ content = content.replace(/~\/\.codebuddy\b/g, normalizedPathPrefix);
2087
+ content = content.replace(/\$HOME\/\.codebuddy\b/g, normalizedPathPrefix);
2088
+ content = processAttribution(content, attribution);
2089
+ break;
2090
+ case 'copilot':
2091
+ content = processAttribution(content, attribution);
2092
+ break;
2093
+ case 'antigravity':
2094
+ content = processAttribution(content, attribution);
2095
+ break;
2096
+ case 'claude':
2097
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
2098
+ content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2099
+ content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2100
+ content = processAttribution(content, attribution);
2101
+ break;
2102
+ case 'qwen':
2103
+ content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
2104
+ content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
2105
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
2106
+ content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2107
+ content = content.replace(/~\/\.qwen\//g, pathPrefix);
2108
+ content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
2109
+ content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
2110
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
2111
+ content = content.replace(/~\/\.qwen(?![\w-])/g, normalizedPathPrefix);
2112
+ content = content.replace(/\$HOME\/\.qwen(?![\w-])/g, normalizedPathPrefix);
2113
+ content = content.replace(/\.claude\//g, '.qwen/');
2114
+ content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2115
+ content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
2116
+ content = processAttribution(content, attribution);
2117
+ break;
2118
+ case 'hermes':
2119
+ content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
2120
+ content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
2121
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
2122
+ content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2123
+ content = content.replace(/~\/\.hermes\//g, pathPrefix);
2124
+ content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix);
2125
+ content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
2126
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
2127
+ content = content.replace(/~\/\.hermes(?![\w-])/g, normalizedPathPrefix);
2128
+ content = content.replace(/\$HOME\/\.hermes(?![\w-])/g, normalizedPathPrefix);
2129
+ content = content.replace(/\.claude\//g, '.hermes/');
2130
+ content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2131
+ content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
2132
+ content = processAttribution(content, attribution);
2133
+ break;
2134
+ case 'kimi':
2135
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
2136
+ content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2137
+ content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2138
+ content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
2139
+ content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
2140
+ content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
2141
+ content = processAttribution(content, attribution);
2142
+ break;
2143
+ default:
2144
+ // Unknown runtime — no rewrites (OpenCode/Kilo handled by their own install path).
2145
+ break;
2146
+ }
2147
+ return content;
2148
+ }
2149
+ /**
2150
+ * LOW-LEVEL: In-place fs walk: rewrite all .md files under stagedDir.
2151
+ *
2152
+ * pathPrefix and attribution are passed in (already resolved by the caller).
2153
+ * Single owner of the walk loop — both the high-level rewriteStagedSkillBodies
2154
+ * and the install.js compat wrapper delegate here.
2155
+ *
2156
+ * @param stagedDir directory of staged skill/agent files
2157
+ * @param runtime canonical runtime ID
2158
+ * @param pathPrefix trailing-slash path prefix (e.g. '$HOME/.cursor/')
2159
+ * @param isGlobal true for global scope installs
2160
+ * @param attribution Co-Authored-By value (string | null | undefined)
2161
+ */
2162
+ function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix, isGlobal = false, attribution = undefined) {
2163
+ if (!node_fs_1.default.existsSync(stagedDir))
2164
+ return;
2165
+ const walkAndRewrite = (dir) => {
2166
+ for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
2167
+ const fullPath = node_path_1.default.join(dir, entry.name);
2168
+ if (entry.isDirectory()) {
2169
+ walkAndRewrite(fullPath);
2170
+ }
2171
+ else if (entry.name.endsWith('.md')) {
2172
+ let content = node_fs_1.default.readFileSync(fullPath, 'utf8');
2173
+ content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal, attribution);
2174
+ node_fs_1.default.writeFileSync(fullPath, content);
2175
+ }
2176
+ }
2177
+ };
2178
+ walkAndRewrite(stagedDir);
2179
+ }
2180
+ /**
2181
+ * LOW-LEVEL: Copy-to-temp then rewrite all .md files.
2182
+ *
2183
+ * pathPrefix and attribution are passed in (already resolved by the caller).
2184
+ * Single owner of the copy+rewrite loop — both the high-level
2185
+ * rewriteStagedCommandBodies and the install.js compat wrapper delegate here.
2186
+ *
2187
+ * IMPORTANT: always copies to a fresh mkdtemp dir — never mutates the source dir
2188
+ * (stageSkillsForProfile returns the source dir on full profile; mutation would
2189
+ * corrupt the package source).
2190
+ *
2191
+ * @param stagedDir directory of staged flat .md command files
2192
+ * @param runtime canonical runtime ID
2193
+ * @param pathPrefix trailing-slash path prefix
2194
+ * @param isGlobal true for global scope installs
2195
+ * @param attribution Co-Authored-By value (string | null | undefined)
2196
+ * @returns {string} path to the temp dir (caller is responsible for cleanup)
2197
+ */
2198
+ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathPrefix, isGlobal = false, attribution = undefined) {
2199
+ if (!node_fs_1.default.existsSync(stagedDir))
2200
+ return stagedDir;
2201
+ const tempDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'gsd-cmd-rewrites-'));
2202
+ try {
2203
+ for (const entry of node_fs_1.default.readdirSync(stagedDir, { withFileTypes: true })) {
2204
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
2205
+ continue;
2206
+ let content = node_fs_1.default.readFileSync(node_path_1.default.join(stagedDir, entry.name), 'utf8');
2207
+ content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal, attribution);
2208
+ if (runtime === 'augment') {
2209
+ content = convertClaudeToAugmentMarkdown(content);
2210
+ }
2211
+ node_fs_1.default.writeFileSync(node_path_1.default.join(tempDir, entry.name), content);
2212
+ }
2213
+ }
2214
+ catch (err) {
2215
+ try {
2216
+ node_fs_1.default.rmSync(tempDir, { recursive: true, force: true });
2217
+ }
2218
+ catch { /* best-effort */ }
2219
+ throw err;
2220
+ }
2221
+ return tempDir;
2222
+ }
2223
+ /**
2224
+ * HIGH-LEVEL: In-place fs walk: rewrite all .md files under stagedDir for the given runtime.
2225
+ *
2226
+ * Deep public seam (ADR-1508 Phase 2). Derives resolvedTarget/homeDir/isGlobal/pathPrefix/
2227
+ * attribution from opts, then delegates to applyRuntimeContentRewritesInPlace (single walk owner).
2228
+ *
2229
+ * @param stagedDir directory of staged skill/agent files
2230
+ * @param opts.runtime canonical runtime ID
2231
+ * @param opts.configDir runtime config directory (absolute path)
2232
+ * @param opts.scope 'global' | 'local'
2233
+ * @param opts.homedir optional homedir resolver (injectable for tests; defaults to os.homedir)
2234
+ * @param opts.platform optional platform string (injectable for tests; defaults to process.platform)
2235
+ * @param opts.resolveAttribution optional fn(runtime)→string|null|undefined; called once per invocation
2236
+ */
2237
+ function rewriteStagedSkillBodies(stagedDir, opts) {
2238
+ const { runtime, configDir, scope = 'global', homedir = () => node_os_1.default.homedir(), platform = process.platform, resolveAttribution, } = opts;
2239
+ if (!node_fs_1.default.existsSync(stagedDir))
2240
+ return;
2241
+ const resolvedTarget = node_path_1.default.resolve(configDir).replace(/\\/g, '/');
2242
+ const homeDir = homedir().replace(/\\/g, '/');
2243
+ const isGlobal = scope === 'global';
2244
+ const isOpencode = runtime === 'opencode';
2245
+ const isWindowsHost = platform === 'win32';
2246
+ const pathPrefix = computePathPrefix({ isGlobal, isOpencode, isWindowsHost, resolvedTarget, homeDir });
2247
+ const attribution = resolveAttribution ? resolveAttribution(runtime) : undefined;
2248
+ applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix, isGlobal, attribution);
2249
+ }
2250
+ /**
2251
+ * HIGH-LEVEL: Copy-to-temp then rewrite all .md files for the given runtime.
2252
+ *
2253
+ * Deep public seam (ADR-1508 Phase 2). Derives resolvedTarget/homeDir/isGlobal/pathPrefix/
2254
+ * attribution from opts, then delegates to applyRuntimeContentRewritesForCommandsInPlace
2255
+ * (single copy+rewrite owner).
2256
+ *
2257
+ * @internal — symmetric companion to rewriteStagedSkillBodies; retained as the deep-seam
2258
+ * API for command bodies. No production caller today (install rewrites commands via
2259
+ * copyWithPathReplacement → applyRuntimeContentRewritesForCommandsInPlace). Kept for
2260
+ * API symmetry + test coverage.
2261
+ *
2262
+ * @returns {string} path to the temp dir (caller is responsible for cleanup)
2263
+ */
2264
+ function rewriteStagedCommandBodies(stagedDir, opts) {
2265
+ const { runtime, configDir, scope = 'global', homedir = () => node_os_1.default.homedir(), platform = process.platform, resolveAttribution, } = opts;
2266
+ if (!node_fs_1.default.existsSync(stagedDir))
2267
+ return stagedDir;
2268
+ const resolvedTarget = node_path_1.default.resolve(configDir).replace(/\\/g, '/');
2269
+ const homeDir = homedir().replace(/\\/g, '/');
2270
+ const isGlobal = scope === 'global';
2271
+ const isOpencode = runtime === 'opencode';
2272
+ const isWindowsHost = platform === 'win32';
2273
+ const pathPrefix = computePathPrefix({ isGlobal, isOpencode, isWindowsHost, resolvedTarget, homeDir });
2274
+ const attribution = resolveAttribution ? resolveAttribution(runtime) : undefined;
2275
+ return applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathPrefix, isGlobal, attribution);
2276
+ }
2277
+ // ── End rewrite engine ────────────────────────────────────────────────────────
2278
+ /**
2279
+ * Apply Co-Authored-By attribution policy to file content.
2280
+ * - null -> remove the Co-Authored-By line and its preceding blank line
2281
+ * - undefined -> leave content unchanged
2282
+ * - string -> replace the value ($ escaped to block backreference injection)
2283
+ *
2284
+ * Pure content transform, relocated from bin/install.js per ADR-1508
2285
+ * (epic #1507, #1510 Phase 1). NOTE: getCommitAttribution stays in the
2286
+ * installer — it is impure install-time config I/O (reads runtime
2287
+ * settings.json, uses the install-time config-dir + cache), not a content
2288
+ * transform, so it does not belong behind this content-conversion seam.
2289
+ */
2290
+ function processAttribution(content, attribution) {
2291
+ if (attribution === null) {
2292
+ // Remove Co-Authored-By lines and the preceding blank line
2293
+ return content.replace(/(\r?\n){2}Co-Authored-By:.*$/gim, '');
2294
+ }
2295
+ if (attribution === undefined) {
2296
+ return content;
2297
+ }
2298
+ // Replace with custom attribution (escape $ to prevent backreference injection)
2299
+ const safeAttribution = attribution.replace(/\$/g, '$$$$');
2300
+ return content.replace(/Co-Authored-By:.*$/gim, `Co-Authored-By: ${safeAttribution}`);
2301
+ }
1898
2302
  module.exports = {
2303
+ processAttribution,
1899
2304
  yamlIdentifier,
1900
2305
  yamlQuote,
1901
2306
  toSingleLine,
@@ -1932,6 +2337,9 @@ module.exports = {
1932
2337
  convertClaudeCommandToKiloSkill,
1933
2338
  readGsdCommandNames,
1934
2339
  transformContentToHyphen,
2340
+ // #1383: version resolver (exported for regression test of the Codex
2341
+ // missing-package.json crash + the VERSION-file source of truth).
2342
+ resolveVersionFrom,
1935
2343
  // #1182: agent converters + tool-name table dependency closure
1936
2344
  claudeToCopilotTools,
1937
2345
  convertCopilotToolName,
@@ -1946,4 +2354,16 @@ module.exports = {
1946
2354
  convertClaudeAgentToCodebuddyAgent,
1947
2355
  convertClaudeAgentToClineAgent,
1948
2356
  convertClaudeAgentToCodexAgent,
2357
+ // #1511 ADR-1508 Phase 2: rewrite engine deep seam
2358
+ // Low-level walkers (pathPrefix + attribution pre-resolved by caller):
2359
+ applyRuntimeContentRewritesInPlace,
2360
+ applyRuntimeContentRewritesForCommandsInPlace,
2361
+ // High-level wrappers (derive pathPrefix + attribution from opts):
2362
+ rewriteStagedSkillBodies,
2363
+ rewriteStagedCommandBodies,
2364
+ _computePathPrefix: computePathPrefix,
2365
+ _applyRuntimeRewrites,
2366
+ _stampNonClaudeRuntimeDefaults,
2367
+ // #1521: canonical non-Claude runtime list for test files and tooling
2368
+ NON_CLAUDE_RUNTIMES,
1949
2369
  };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+ /**
3
+ * Runtime Artifact Install Plan Module.
4
+ *
5
+ * Turns a pre-resolved runtime artifact layout into staged copy inputs. The
6
+ * installer adapter still owns pruning, copying, migrations, output, and final
7
+ * cleanup execution.
8
+ */
9
+ // In .cts (CommonJS output) files, `require` is available as a global.
10
+ const _require = require;
11
+ const path = _require('node:path');
12
+ function errorMessage(err) {
13
+ if (err instanceof Error)
14
+ return err.message;
15
+ return String(err);
16
+ }
17
+ function addCleanupDir(cleanupDirs, stagedDir, rewrittenDir) {
18
+ const sourceDir = rewrittenDir ?? stagedDir;
19
+ if (sourceDir !== stagedDir)
20
+ cleanupDirs.push(sourceDir);
21
+ return sourceDir;
22
+ }
23
+ function createRuntimeArtifactInstallPlan(args) {
24
+ const { layout, resolvedProfile, homedir, platform, resolveAttribution, deps = {}, } = args;
25
+ const conversionExports = _require('./runtime-artifact-conversion.cjs');
26
+ const rewriteStagedSkillBodies = deps.rewriteStagedSkillBodies ?? conversionExports.rewriteStagedSkillBodies;
27
+ const rewriteStagedCommandBodies = deps.rewriteStagedCommandBodies ?? conversionExports.rewriteStagedCommandBodies;
28
+ const cleanupDirs = [];
29
+ const items = [];
30
+ const scope = layout.scope ?? 'global';
31
+ const rewriteOpts = {
32
+ runtime: layout.runtime,
33
+ configDir: layout.configDir,
34
+ scope,
35
+ homedir,
36
+ platform,
37
+ resolveAttribution,
38
+ };
39
+ for (const kind of layout.kinds) {
40
+ let stagedDir;
41
+ try {
42
+ stagedDir = kind.stage(resolvedProfile);
43
+ }
44
+ catch (err) {
45
+ return { ok: false, kind: 'stage_failed', message: errorMessage(err), cleanupDirs, failedKind: kind.kind };
46
+ }
47
+ let sourceDir = stagedDir;
48
+ try {
49
+ if (kind.kind === 'commands') {
50
+ const rewrittenDir = rewriteStagedCommandBodies(stagedDir, rewriteOpts);
51
+ sourceDir = addCleanupDir(cleanupDirs, stagedDir, rewrittenDir);
52
+ }
53
+ else if (kind.kind === 'skills' || kind.kind === 'kimi-agents') {
54
+ const rewrittenDir = rewriteStagedSkillBodies(stagedDir, rewriteOpts);
55
+ sourceDir = addCleanupDir(cleanupDirs, stagedDir, rewrittenDir);
56
+ }
57
+ }
58
+ catch (err) {
59
+ return { ok: false, kind: 'rewrite_failed', message: errorMessage(err), cleanupDirs, failedKind: kind.kind };
60
+ }
61
+ items.push({
62
+ kind: kind.kind,
63
+ sourceDir,
64
+ destDir: path.join(layout.configDir, kind.destSubpath),
65
+ });
66
+ }
67
+ return { ok: true, plan: { items, cleanupDirs } };
68
+ }
69
+ function createRuntimeArtifactUninstallPlan(layout) {
70
+ return {
71
+ items: layout.kinds.map((kind) => ({
72
+ kind: kind.kind,
73
+ destDir: path.join(layout.configDir, kind.destSubpath),
74
+ })),
75
+ };
76
+ }
77
+ module.exports = { createRuntimeArtifactInstallPlan, createRuntimeArtifactUninstallPlan };
@@ -25,33 +25,6 @@ const runtimeArtifactConversion = require("./runtime-artifact-conversion.cjs");
25
25
  const conversionExports = runtimeArtifactConversion;
26
26
  // In .cts (CommonJS output) files, `require` is available as a global.
27
27
  const _require = require;
28
- /**
29
- * Load bin/install.js exports in a test-safe way.
30
- * Sets GSD_TEST_MODE only for the duration of the require() call and only if
31
- * it was not already set, restoring the original value in a finally block so
32
- * the module-level environment is never permanently mutated.
33
- */
34
- function loadInstallExports() {
35
- const savedTestMode = process.env['GSD_TEST_MODE'];
36
- if (savedTestMode === undefined)
37
- process.env['GSD_TEST_MODE'] = '1';
38
- try {
39
- return _require('../../../bin/install.js');
40
- }
41
- finally {
42
- if (savedTestMode === undefined)
43
- delete process.env['GSD_TEST_MODE'];
44
- else
45
- process.env['GSD_TEST_MODE'] = savedTestMode;
46
- }
47
- }
48
- /** Cache after first successful load. */
49
- let _installExports = null;
50
- function getInstallExports() {
51
- if (!_installExports)
52
- _installExports = loadInstallExports();
53
- return _installExports;
54
- }
55
28
  // ---------------------------------------------------------------------------
56
29
  // Source root finders
57
30
  // ---------------------------------------------------------------------------
@@ -330,4 +303,4 @@ function resolveRuntimeArtifactLayoutFromRegistry(registry, runtime, configDir,
330
303
  const kinds = entries.map((entry) => dispatchKindEntry(entry, runtime, configDir, scope));
331
304
  return { runtime, configDir, scope, kinds };
332
305
  }
333
- module.exports = { resolveRuntimeArtifactLayout, resolveRuntimeArtifactLayoutFromRegistry, findInstallSourceRoot, getInstallExports };
306
+ module.exports = { resolveRuntimeArtifactLayout, resolveRuntimeArtifactLayoutFromRegistry, findInstallSourceRoot };
@@ -36,6 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  exports.resolveConfigHomeFromDescriptor = resolveConfigHomeFromDescriptor;
38
38
  exports.resolveAntigravityGlobalDir = resolveAntigravityGlobalDir;
39
+ exports.detectAntigravityDirAmbiguity = detectAntigravityDirAmbiguity;
39
40
  exports.resolveKimiGlobalDir = resolveKimiGlobalDir;
40
41
  exports.getGlobalConfigDir = getGlobalConfigDir;
41
42
  exports.resolveSkillsBaseFromDescriptor = resolveSkillsBaseFromDescriptor;
@@ -97,7 +98,20 @@ function resolveConfigHomeFromDescriptor(configHome, opts = {}) {
97
98
  }
98
99
  const base = node_path_1.default.join(home, configHome.parent);
99
100
  if (configHome.probe && configHome.probe.length > 0) {
100
- // probe each candidate under base; return first that exists
101
+ // Pass 1 (marker-priority): when probeExists is declared, prefer the
102
+ // candidate GSD actually owns (its `<candidate>/<probeExists>` exists).
103
+ // This disambiguates an active-but-shadowing sibling dir (e.g. the
104
+ // Antigravity-IDE `~/.gemini/antigravity` dir) from the dir GSD was
105
+ // installed into, instead of blindly taking the first dir that exists.
106
+ if (configHome.probeExists) {
107
+ for (const candidate of configHome.probe) {
108
+ const resolved = node_path_1.default.join(base, candidate);
109
+ if (existsSyncFn(node_path_1.default.join(resolved, configHome.probeExists))) {
110
+ return resolved;
111
+ }
112
+ }
113
+ }
114
+ // Pass 2 (legacy bare-existence): first candidate dir that exists.
101
115
  for (const candidate of configHome.probe) {
102
116
  const resolved = node_path_1.default.join(base, candidate);
103
117
  if (existsSyncFn(resolved))
@@ -171,8 +185,46 @@ function resolveAntigravityGlobalDir(opts = {}) {
171
185
  parent: '.gemini',
172
186
  env: ['ANTIGRAVITY_CONFIG_DIR'],
173
187
  probe: ['antigravity', 'antigravity-ide', 'antigravity-cli'],
188
+ // Prefer the candidate GSD installed into (carries gsd-core/VERSION) over
189
+ // a bare-existing sibling. Without this, a CLI user (antigravity-cli) who
190
+ // also has the IDE's ~/.gemini/antigravity dir is shadowed to the legacy
191
+ // dir because it is probed first. See #213/#217. The posix-slash literal
192
+ // matches capabilities/antigravity/capability.json; both normalize via
193
+ // path.join at the check site, so Windows backslash handling is covered.
194
+ probeExists: 'gsd-core/VERSION',
174
195
  }, { env, home, existsSync: existsSyncFn });
175
196
  }
197
+ /**
198
+ * Detect whether the Antigravity config-dir resolution is ambiguous — i.e. more
199
+ * than one of ~/.gemini/{antigravity,antigravity-ide,antigravity-cli} exists, so
200
+ * a user upgrading from a pre-#217 install may have had GSD written into the
201
+ * wrong sibling dir (the legacy/IDE dir shadowing an active CLI dir).
202
+ *
203
+ * This is a pure, side-effect-free probe intended for the installer and
204
+ * /gsd-update to surface operator guidance (set ANTIGRAVITY_CONFIG_DIR or move
205
+ * gsd-core/ into the intended dir). The migration framework cannot relocate an
206
+ * install across sibling config dirs (it is bounded to a single configDir and
207
+ * has no cross-dir move primitive — see installer-migrations 004), so existing
208
+ * misinstalls are corrected by re-detection + operator guidance, not an
209
+ * automatic move.
210
+ */
211
+ function detectAntigravityDirAmbiguity(opts = {}) {
212
+ const env = opts.env ?? process.env;
213
+ const home = opts.home ?? node_os_1.default.homedir();
214
+ const existsSyncFn = opts.existsSync ?? node_fs_1.default.existsSync;
215
+ const marker = node_path_1.default.join('gsd-core', 'VERSION');
216
+ const base = node_path_1.default.join(home, '.gemini');
217
+ const candidates = ['antigravity', 'antigravity-ide', 'antigravity-cli'].map((c) => node_path_1.default.join(base, c));
218
+ const presentDirs = candidates.filter((dir) => existsSyncFn(dir));
219
+ const gsdMarkedDirs = candidates.filter((dir) => existsSyncFn(node_path_1.default.join(dir, marker)));
220
+ return {
221
+ ambiguous: presentDirs.length > 1,
222
+ resolved: resolveAntigravityGlobalDir({ env, home, existsSync: existsSyncFn }),
223
+ presentDirs,
224
+ gsdMarkedDirs,
225
+ envOverridden: Boolean(env['ANTIGRAVITY_CONFIG_DIR']),
226
+ };
227
+ }
176
228
  /**
177
229
  * Resolve Kimi's generic user root using Kimi CLI's documented first-existing
178
230
  * generic skills directory policy: