@opengsd/gsd-core 1.16.0 → 1.18.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.
Files changed (211) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -2
  3. package/.opencode/plugins/gsd-core.js +251 -1
  4. package/.opencode/plugins/gsd-statusline/tui.tsx +55 -0
  5. package/GEMINI.md +1 -1
  6. package/agents/gsd-advisor-researcher.compact.md +1 -1
  7. package/agents/gsd-advisor-researcher.md +1 -1
  8. package/agents/gsd-assumptions-analyzer.compact.md +1 -1
  9. package/agents/gsd-assumptions-analyzer.md +1 -1
  10. package/agents/gsd-code-fixer.compact.md +1 -1
  11. package/agents/gsd-code-fixer.md +1 -1
  12. package/agents/gsd-code-reviewer.compact.md +1 -1
  13. package/agents/gsd-code-reviewer.md +1 -1
  14. package/agents/gsd-codebase-mapper.compact.md +1 -1
  15. package/agents/gsd-codebase-mapper.md +1 -1
  16. package/agents/gsd-debug-session-manager.compact.md +6 -0
  17. package/agents/gsd-debug-session-manager.md +6 -0
  18. package/agents/gsd-debugger.md +1 -1
  19. package/agents/gsd-doc-verifier.compact.md +4 -4
  20. package/agents/gsd-doc-verifier.md +4 -3
  21. package/agents/gsd-doc-writer.compact.md +23 -5
  22. package/agents/gsd-doc-writer.md +19 -6
  23. package/agents/gsd-eval-auditor.compact.md +1 -1
  24. package/agents/gsd-eval-auditor.md +1 -1
  25. package/agents/gsd-executor.md +7 -3
  26. package/agents/gsd-integration-checker.compact.md +1 -1
  27. package/agents/gsd-integration-checker.md +1 -1
  28. package/agents/gsd-nyquist-auditor.compact.md +1 -1
  29. package/agents/gsd-nyquist-auditor.md +1 -1
  30. package/agents/gsd-phase-researcher.md +1 -1
  31. package/agents/gsd-plan-checker.md +2 -1
  32. package/agents/gsd-planner.md +5 -4
  33. package/agents/gsd-project-researcher.compact.md +1 -1
  34. package/agents/gsd-project-researcher.md +1 -1
  35. package/agents/gsd-research-synthesizer.compact.md +5 -5
  36. package/agents/gsd-research-synthesizer.md +5 -5
  37. package/agents/gsd-roadmapper.compact.md +1 -1
  38. package/agents/gsd-roadmapper.md +1 -1
  39. package/agents/gsd-security-auditor.compact.md +1 -1
  40. package/agents/gsd-security-auditor.md +1 -1
  41. package/agents/gsd-ui-auditor.compact.md +1 -1
  42. package/agents/gsd-ui-auditor.md +1 -1
  43. package/agents/gsd-ui-checker.compact.md +1 -1
  44. package/agents/gsd-ui-checker.md +1 -1
  45. package/agents/gsd-ui-researcher.compact.md +13 -1
  46. package/agents/gsd-ui-researcher.md +28 -1
  47. package/agents/gsd-verifier.md +1 -1
  48. package/bin/gsd-mcp-server.js +3 -1
  49. package/bin/install.js +398 -95
  50. package/commands/gsd/code-review.md +8 -0
  51. package/commands/gsd/pause-work.md +1 -1
  52. package/gsd-core/bin/gsd-tools.cjs +34 -12
  53. package/gsd-core/bin/lib/adr-parser.cjs +37 -5
  54. package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
  55. package/gsd-core/bin/lib/capability-lock.cjs +29 -7
  56. package/gsd-core/bin/lib/capability-registry.cjs +138 -80
  57. package/gsd-core/bin/lib/capability-source.cjs +3 -1
  58. package/gsd-core/bin/lib/capability-state.cjs +6 -3
  59. package/gsd-core/bin/lib/capability-validator.cjs +8 -5
  60. package/gsd-core/bin/lib/capability-writer.cjs +2 -1
  61. package/gsd-core/bin/lib/commands.cjs +33 -9
  62. package/gsd-core/bin/lib/config-loader.cjs +3 -3
  63. package/gsd-core/bin/lib/decision-coverage-support.cjs +19 -0
  64. package/gsd-core/bin/lib/gate-context-drift.cjs +5 -4
  65. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +15 -0
  66. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +13 -0
  67. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
  68. package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
  69. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +60 -5
  70. package/gsd-core/bin/lib/gate-ui-safety.cjs +2 -1
  71. package/gsd-core/bin/lib/graphify.cjs +3 -2
  72. package/gsd-core/bin/lib/init.cjs +43 -16
  73. package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
  74. package/gsd-core/bin/lib/install-engine.cjs +11 -0
  75. package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
  76. package/gsd-core/bin/lib/install-profiles.cjs +5 -3
  77. package/gsd-core/bin/lib/install-shadow-report.cjs +12 -0
  78. package/gsd-core/bin/lib/installer-migrations/011-opencode-mcp-no-prompts.cjs +99 -0
  79. package/gsd-core/bin/lib/installer-migrations.cjs +54 -1
  80. package/gsd-core/bin/lib/jsonc-edit.cjs +418 -0
  81. package/gsd-core/bin/lib/loop-host-contract.cjs +2 -1
  82. package/gsd-core/bin/lib/mcp-server.cjs +5 -1
  83. package/gsd-core/bin/lib/opencode-mcp-entry.cjs +39 -0
  84. package/gsd-core/bin/lib/opencode-version.cjs +486 -0
  85. package/gsd-core/bin/lib/phase.cjs +141 -11
  86. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +2 -1
  87. package/gsd-core/bin/lib/report-parser.cjs +57 -1
  88. package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
  89. package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
  90. package/gsd-core/bin/lib/roadmap.cjs +56 -5
  91. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +142 -6
  92. package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
  93. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +58 -3
  94. package/gsd-core/bin/lib/runtime-slash.cjs +13 -0
  95. package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
  96. package/gsd-core/bin/lib/smart-entry.cjs +100 -53
  97. package/gsd-core/bin/lib/state-document.cjs +37 -3
  98. package/gsd-core/bin/lib/state-transition.cjs +98 -19
  99. package/gsd-core/bin/lib/state.cjs +544 -126
  100. package/gsd-core/bin/lib/task-command-router.cjs +2 -3
  101. package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
  102. package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
  103. package/gsd-core/bin/lib/update-context.cjs +18 -1
  104. package/gsd-core/bin/lib/verification.cjs +107 -17
  105. package/gsd-core/bin/lib/verify.cjs +36 -1
  106. package/gsd-core/references/agent-contracts.md +1 -1
  107. package/gsd-core/references/agent-skills-bootstrap.md +26 -7
  108. package/gsd-core/references/git-integration.md +3 -3
  109. package/gsd-core/references/planner-reversibility.md +18 -4
  110. package/gsd-core/references/planner-verify-command-grounding.md +18 -0
  111. package/gsd-core/references/tdd.md +4 -3
  112. package/gsd-core/references/verifier-phase-gates.md +8 -1
  113. package/gsd-core/workflows/add-backlog.md +10 -2
  114. package/gsd-core/workflows/add-phase.md +5 -1
  115. package/gsd-core/workflows/add-tests.md +5 -1
  116. package/gsd-core/workflows/add-todo.md +5 -1
  117. package/gsd-core/workflows/ai-integration-phase.md +5 -1
  118. package/gsd-core/workflows/audit-milestone.md +21 -9
  119. package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
  120. package/gsd-core/workflows/autonomous.md +52 -64
  121. package/gsd-core/workflows/check-todos.md +5 -1
  122. package/gsd-core/workflows/code-review-fix.md +5 -1
  123. package/gsd-core/workflows/code-review.md +21 -2
  124. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
  125. package/gsd-core/workflows/complete-milestone.md +25 -5
  126. package/gsd-core/workflows/debug.md +5 -1
  127. package/gsd-core/workflows/diagnose-issues.md +5 -1
  128. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
  129. package/gsd-core/workflows/discuss-phase-assumptions.md +11 -3
  130. package/gsd-core/workflows/discuss-phase.md +5 -1
  131. package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
  132. package/gsd-core/workflows/docs-update.md +27 -5
  133. package/gsd-core/workflows/edit-phase.md +5 -1
  134. package/gsd-core/workflows/eval-review.md +11 -3
  135. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
  136. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
  137. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +12 -6
  138. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +19 -2
  139. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
  140. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +5 -1
  141. package/gsd-core/workflows/execute-phase.md +40 -7
  142. package/gsd-core/workflows/execute-plan.md +8 -4
  143. package/gsd-core/workflows/extract-learnings.md +6 -2
  144. package/gsd-core/workflows/forensics.md +5 -1
  145. package/gsd-core/workflows/import.md +5 -1
  146. package/gsd-core/workflows/insert-phase.md +5 -1
  147. package/gsd-core/workflows/manager.md +10 -2
  148. package/gsd-core/workflows/map-codebase.md +5 -1
  149. package/gsd-core/workflows/milestone-summary.md +10 -2
  150. package/gsd-core/workflows/new-milestone.md +13 -5
  151. package/gsd-core/workflows/new-project.md +9 -5
  152. package/gsd-core/workflows/next.md +8 -4
  153. package/gsd-core/workflows/pause-work.md +3 -3
  154. package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
  155. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +5 -1
  156. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
  157. package/gsd-core/workflows/plan-phase.md +41 -73
  158. package/gsd-core/workflows/plan-review-convergence.md +17 -5
  159. package/gsd-core/workflows/plant-seed.md +8 -4
  160. package/gsd-core/workflows/progress.md +6 -2
  161. package/gsd-core/workflows/quick-batch.md +5 -1
  162. package/gsd-core/workflows/quick.md +22 -7
  163. package/gsd-core/workflows/remove-phase.md +5 -1
  164. package/gsd-core/workflows/resume-project.md +19 -11
  165. package/gsd-core/workflows/review.md +6 -2
  166. package/gsd-core/workflows/scan.md +5 -1
  167. package/gsd-core/workflows/secure-phase.md +6 -2
  168. package/gsd-core/workflows/settings.md +3 -3
  169. package/gsd-core/workflows/ship.md +13 -10
  170. package/gsd-core/workflows/thread.md +10 -2
  171. package/gsd-core/workflows/transition.md +5 -1
  172. package/gsd-core/workflows/ui-phase.md +5 -1
  173. package/gsd-core/workflows/ui-review.md +6 -2
  174. package/gsd-core/workflows/ultraplan-phase.md +5 -1
  175. package/gsd-core/workflows/update.md +5 -1
  176. package/gsd-core/workflows/validate-phase.md +6 -2
  177. package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
  178. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
  179. package/gsd-core/workflows/verify-work.md +11 -3
  180. package/hooks/dist/gsd-agent-isolation-guard.js +3 -1
  181. package/hooks/dist/gsd-check-update.js +26 -20
  182. package/hooks/dist/gsd-context-monitor.js +14 -5
  183. package/hooks/dist/gsd-cursor-subagent-start.js +2 -1
  184. package/hooks/dist/gsd-secret-read-guard.js +189 -15
  185. package/hooks/dist/gsd-statusline.js +321 -22
  186. package/hooks/dist/gsd-validate-commit.sh +27 -5
  187. package/hooks/gsd-agent-isolation-guard.js +3 -1
  188. package/hooks/gsd-check-update.js +26 -20
  189. package/hooks/gsd-context-monitor.js +14 -5
  190. package/hooks/gsd-cursor-subagent-start.js +2 -1
  191. package/hooks/gsd-secret-read-guard.js +189 -15
  192. package/hooks/gsd-statusline.js +321 -22
  193. package/hooks/gsd-validate-commit.sh +27 -5
  194. package/package.json +1 -1
  195. package/scripts/audit-workflow-script-paths.cjs +1 -1
  196. package/scripts/diagnose-validate-commit-in-tier.cjs +270 -0
  197. package/scripts/diagnose-validate-commit-phases.cjs +348 -0
  198. package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
  199. package/scripts/docs-guard-registry.cjs +2 -0
  200. package/scripts/gen-capability-registry.cjs +7 -1
  201. package/scripts/gen-loop-host-contract.cjs +4 -1
  202. package/scripts/lib/macos-conformance-tier.generated.cjs +13 -0
  203. package/scripts/lib/platform-conformance-tier.generated.cjs +26 -0
  204. package/scripts/lint-phase-arg-assignment.cjs +2 -2
  205. package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
  206. package/scripts/mutation-matrix.cjs +3 -1
  207. package/scripts/release-tarball-smoke.cjs +15 -7
  208. package/scripts/xtrace-phase-timing.cjs +205 -0
  209. package/skills/gsd-code-review/SKILL.md +8 -0
  210. package/skills/gsd-pause-work/SKILL.md +1 -1
  211. package/vscode/package.json +1 -1
package/bin/install.js CHANGED
@@ -35,6 +35,7 @@ const {
35
35
  getGlobalSkillsBase,
36
36
  resolveKimiHooksTomlDir,
37
37
  isRegisteredRuntimeId,
38
+ resolveGsdHome,
38
39
  } = require('../gsd-core/bin/lib/runtime-homes.cjs');
39
40
  // #2870: the Install Scope Module — turns a bare 'global' | 'local' scope id
40
41
  // plus a runtime into one resolved value (configHome, settingsFile,
@@ -190,7 +191,7 @@ function isCodexHooksFeatureKey(key) {
190
191
  // GSD_CLAUDE_LEGACY_DENY_PERMISSIONS below.)
191
192
  //
192
193
  // Format: each string uses Claude Code's documented permission rule syntax \u2014
193
- // "Tool(pattern)" e.g. "Bash(npx gsd-core *)", "Read(.planning/*)"
194
+ // "Tool(pattern)" e.g. "Read(.planning/*)", "Edit(STATE.md)"
194
195
  // "Tool" (bare tool name, no pattern)
195
196
  //
196
197
  // Merge policy: additive, non-destructive \u2014 existing user entries are preserved;
@@ -199,7 +200,6 @@ function isCodexHooksFeatureKey(key) {
199
200
  // instead of scattered literal 'claude' defaults/rosters (#2086).
200
201
  const DEFAULT_RUNTIME = 'claude';
201
202
  const GSD_CLAUDE_ALLOW_PERMISSIONS = Object.freeze([
202
- 'Bash(npx gsd-core *)',
203
203
  'Read(.planning/*)',
204
204
  'Edit(.planning/*)',
205
205
  'Read(STATE.md)',
@@ -232,9 +232,14 @@ const GSD_CLAUDE_LEGACY_DENY_PERMISSIONS = Object.freeze([
232
232
  // mergeClaudePermissions and uninstall cleanup retire those stale entries on
233
233
  // existing installs while the current GSD_CLAUDE_ALLOW_PERMISSIONS above
234
234
  // carries the working `Edit(...)` forms.
235
+ // #5054 — `Bash(npx gsd-core *)` is retired the same way. The project ships
236
+ // as `@opengsd/gsd-core`; the unscoped `gsd-core` npm name belongs to someone
237
+ // else, so the rule pre-authorized whatever that name's owner publishes next.
238
+ // GSD never runs the bare form (update.md uses `--package=@opengsd/gsd-core`).
235
239
  const GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS = Object.freeze([
236
240
  'Write(.planning/*)',
237
241
  'Write(STATE.md)',
242
+ 'Bash(npx gsd-core *)',
238
243
  ]);
239
244
 
240
245
  /**
@@ -249,6 +254,7 @@ const GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS = Object.freeze([
249
254
  * `Write(...)` forms from before the fix) is removed from permissions.allow,
250
255
  * so existing installs end up with the working `Edit(...)` forms instead of
251
256
  * both the dead legacy entry and its replacement sitting side by side.
257
+ * The same filter retires the unscoped `Bash(npx gsd-core *)` rule (#5054).
252
258
  *
253
259
  * Migration (#4221): the retired GSD_CLAUDE_LEGACY_DENY_PERMISSIONS entries
254
260
  * are removed from permissions.deny (byte-equal only). Nothing is added to
@@ -408,6 +414,26 @@ const GSD_WINDSURF_HOOK_SCRIPTS = hooksSurface.GSD_WINDSURF_HOOK_SCRIPTS;
408
414
  // two guards require so their copies cannot drift.
409
415
  const GSD_HOOK_LIB_FILES = ['git-cmd.js', 'gsd-graphify-rebuild.sh', 'cursor-workspace.js', 'injection-patterns.js'];
410
416
 
417
+ /**
418
+ * Every file GSD stages under `hooks/lib/` (#5335). The install copies `hooks/dist/lib/` wholesale (and the
419
+ * allow-listed names from `hooks/lib/`), so a fixed list drifts the moment a helper is added: the new file is
420
+ * staged but never removed, which also keeps `hooks/lib/` non-empty and blocks its rmdir. The set is the
421
+ * allow-list plus the top-level files of the directories the installer copies from, so uninstall and the
422
+ * manifest cannot disagree with install. Only these names are ever removed; a user's own helper beside them
423
+ * is left alone.
424
+ */
425
+ function gsdHookLibFiles() {
426
+ const names = new Set(GSD_HOOK_LIB_FILES);
427
+ for (const parts of [['hooks', 'dist', 'lib'], ['hooks', 'lib']]) {
428
+ try {
429
+ for (const entry of fs.readdirSync(path.join(__dirname, '..', ...parts), { withFileTypes: true })) {
430
+ if (entry.isFile()) names.add(entry.name);
431
+ }
432
+ } catch { /* the directory is absent from this package layout */ }
433
+ }
434
+ return [...names];
435
+ }
436
+
411
437
  /**
412
438
  * Directory name GSD stages its shared hook bundle under, inside a runtime's
413
439
  * install root. Defaults to 'hooks' — the name every runtime used before #3023.
@@ -762,8 +788,11 @@ const {
762
788
  discoverInstallerMigrations,
763
789
  MANIFEST_SCHEMA_VERSION,
764
790
  readInstallManifest,
791
+ recordHostGeneration,
765
792
  runInstallerMigrations,
766
793
  } = require(path.join(_gsdLibDir, 'installer-migrations.cjs'));
794
+ const { setJsoncValue, removeJsoncValue, hasJsoncFeatures, parseJsonc } = require(path.join(_gsdLibDir, 'jsonc-edit.cjs'));
795
+ const { isGsdMcpEntry, GSD_MCP_NO_PROMPTS_FLAG } = require(path.join(_gsdLibDir, 'opencode-mcp-entry.cjs'));
767
796
  const {
768
797
  assertInstallerMigrationsUnblocked,
769
798
  resolveInstallerMigrationPromptsForNonTty,
@@ -1256,6 +1285,17 @@ const applySettingsJsonHooks = hooksSurface.applySettingsJsonHooks;
1256
1285
  // writeKimiHooksToml / removeKimiHooksToml: kimi's native config.toml [[hooks]]
1257
1286
  // surface (#2095 EoS/kimi Upgrade 1) — separate from settings.json entirely.
1258
1287
  const writeKimiHooksToml = hooksSurface.writeKimiHooksToml;
1288
+
1289
+ // #4840: false when the runtime declares `context-warnings` unsupported in its
1290
+ // descriptor (`hostBehaviors.unsupportedFeatures`). gsd-context-monitor.js reads
1291
+ // the metrics bridge file only the statusline hook writes, and the statusline
1292
+ // is wired for Claude Code alone, so on any other host the hook fires on every
1293
+ // tool event, finds no file and exits silently — forever. Such a host gets no
1294
+ // monitor registration (stale ones are stripped), as Codex does since #2586.
1295
+ function runtimeSupportsContextWarnings(runtime) {
1296
+ const unsupported = hostBehaviorsFor(runtime).unsupportedFeatures;
1297
+ return !(Array.isArray(unsupported) && unsupported.includes('context-warnings'));
1298
+ }
1259
1299
  const removeKimiHooksToml = hooksSurface.removeKimiHooksToml;
1260
1300
  // processAttribution: pure Co-Authored-By content transform, relocated to the
1261
1301
  // conversion module (ADR-1508 / #1510 Phase 1). Bound here so install.js
@@ -6959,6 +6999,82 @@ function writeCopilotHookConfig(targetDir) {
6959
6999
  * Reads agent .md files from source, extracts metadata, writes .toml configs.
6960
7000
  */
6961
7001
 
7002
+ // #5335: ~/.gsd/defaults.json is machine-wide, so uninstalling one runtime must not strip a default another
7003
+ // still needs, nor one the user chose. A sidecar records which keys an install SET (not ones it found), the
7004
+ // value it set, and which non-Claude runtimes have installed since; uninstall reverts a key only when its
7005
+ // last such runtime is gone and the value is still the one the installer wrote.
7006
+ const DEFAULTS_PROVENANCE_NAME = 'defaults-provenance.json';
7007
+
7008
+ function readDefaultsProvenance(gsdDir) {
7009
+ try {
7010
+ const p = JSON.parse(fs.readFileSync(path.join(gsdDir, DEFAULTS_PROVENANCE_NAME), 'utf8'));
7011
+ if (p && typeof p === 'object' && p.keys && typeof p.keys === 'object') return p;
7012
+ } catch { /* absent or unreadable */ }
7013
+ return { version: 1, keys: {} };
7014
+ }
7015
+
7016
+ function recordDefaultsProvenance(gsdDir, runtime, defaults, setNow) {
7017
+ const prov = readDefaultsProvenance(gsdDir);
7018
+ let changed = false;
7019
+ for (const key of ['resolve_model_ids', 'runtime']) {
7020
+ let entry = prov.keys[key];
7021
+ if (setNow[key]) {
7022
+ entry = { setValue: defaults[key], runtimes: [] };
7023
+ prov.keys[key] = entry;
7024
+ changed = true;
7025
+ }
7026
+ if (entry && Array.isArray(entry.runtimes) && !entry.runtimes.includes(runtime)) {
7027
+ entry.runtimes.push(runtime);
7028
+ changed = true;
7029
+ }
7030
+ }
7031
+ if (changed) atomicWriteFileSync(path.join(gsdDir, DEFAULTS_PROVENANCE_NAME), JSON.stringify(prov, null, 2) + '\n', 'utf8');
7032
+ }
7033
+
7034
+ /** @returns {boolean} true when it removed or reverted something */
7035
+ function revertNonClaudeDefaults(runtime) {
7036
+ if (hostBehaviorsFor(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return false;
7037
+ const gsdDir = path.join(resolveGsdHome(), '.gsd'); // #5017: GSD_HOME-aware, like the loader
7038
+ const provPath = path.join(gsdDir, DEFAULTS_PROVENANCE_NAME);
7039
+ if (!fs.existsSync(provPath)) return false;
7040
+ let releaseLock = null;
7041
+ try {
7042
+ releaseLock = acquireInstallMigrationLock(gsdDir);
7043
+ const prov = readDefaultsProvenance(gsdDir);
7044
+ const defaultsPath = path.join(gsdDir, 'defaults.json');
7045
+ let defaults = null;
7046
+ try { defaults = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')); } catch { /* absent or corrupt */ }
7047
+ const reverted = [];
7048
+ for (const [key, entry] of Object.entries(prov.keys)) {
7049
+ if (!entry || !Array.isArray(entry.runtimes)) { delete prov.keys[key]; continue; }
7050
+ entry.runtimes = entry.runtimes.filter((r) => r !== runtime);
7051
+ if (entry.runtimes.length > 0) continue;
7052
+ if (defaults && typeof defaults === 'object' && defaults[key] === entry.setValue) {
7053
+ delete defaults[key];
7054
+ reverted.push(key);
7055
+ }
7056
+ delete prov.keys[key];
7057
+ }
7058
+ if (reverted.length > 0 && defaults) {
7059
+ if (Object.keys(defaults).length === 0) fs.rmSync(defaultsPath, { force: true });
7060
+ else atomicWriteFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n', 'utf8');
7061
+ console.log(` ${green}✓${reset} Reverted ${reverted.join(', ')} in ~/.gsd/defaults.json (set by the ${runtime} install)`);
7062
+ }
7063
+ if (Object.keys(prov.keys).length === 0) fs.rmSync(provPath, { force: true });
7064
+ else atomicWriteFileSync(provPath, JSON.stringify(prov, null, 2) + '\n', 'utf8');
7065
+ return reverted.length > 0;
7066
+ } catch (e) {
7067
+ console.log(` ${yellow}⚠${reset} Could not update ~/.gsd/defaults.json: ${e.message}`);
7068
+ return false;
7069
+ } finally {
7070
+ if (releaseLock) {
7071
+ try { releaseLock(); } catch { /* the stale-lock reaper clears it */ }
7072
+ }
7073
+ // ~/.gsd is GSD's own directory; drop it once the lock is released and nothing else lives in it.
7074
+ try { fs.rmdirSync(gsdDir); } catch { /* other GSD state lives here */ }
7075
+ }
7076
+ }
7077
+
6962
7078
  /**
6963
7079
  * #2834: Write ~/.gsd/defaults.json for non-Claude runtimes — sets
6964
7080
  * resolve_model_ids="omit" (so resolveModelInternal() returns '' instead of
@@ -6971,7 +7087,7 @@ function writeCopilotHookConfig(targetDir) {
6971
7087
  */
6972
7088
  function writeNonClaudeDefaults(runtime) {
6973
7089
  if (hostBehaviorsFor(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return;
6974
- const gsdDir = path.join(os.homedir(), '.gsd');
7090
+ const gsdDir = path.join(resolveGsdHome(), '.gsd'); // #5017: GSD_HOME-aware, like the loader
6975
7091
  const defaultsPath = path.join(gsdDir, 'defaults.json');
6976
7092
  let releaseLock = null;
6977
7093
  try {
@@ -7005,6 +7121,10 @@ function writeNonClaudeDefaults(runtime) {
7005
7121
  atomicWriteFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n', 'utf8');
7006
7122
  for (const message of applied) console.log(` ${green}✓${reset} ${message}`);
7007
7123
  }
7124
+ recordDefaultsProvenance(gsdDir, runtime, defaults, {
7125
+ resolve_model_ids: shouldDefaultToOmit,
7126
+ runtime: applied.some((m) => m.startsWith('Set runtime:')),
7127
+ });
7008
7128
  } catch (e) {
7009
7129
  console.log(` ${yellow}⚠${reset} Could not write ~/.gsd/defaults.json: ${e.message}`);
7010
7130
  } finally {
@@ -7212,7 +7332,11 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
7212
7332
  const newLines = [];
7213
7333
  let inAllowedTools = false;
7214
7334
  let inSkippedArray = false;
7335
+ let inAgentTools = false;
7336
+ let inDisallowedTools = false;
7215
7337
  const allowedTools = [];
7338
+ const agentTools = [];
7339
+ const agentDisallowed = [];
7216
7340
 
7217
7341
  for (const line of lines) {
7218
7342
  const trimmed = line.trim();
@@ -7228,11 +7352,31 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
7228
7352
  continue;
7229
7353
  }
7230
7354
 
7355
+ // Agents: collect the `tools:` allowlist and `disallowedTools:` denylist (block-list continuation
7356
+ // lines) so they can be rendered as `permission:` entries below (mirrors
7357
+ // src/runtime-artifact-conversion.cts's convertClaudeToOpencodeFrontmatter, #5338).
7358
+ if (isAgent && (inAgentTools || inDisallowedTools)) {
7359
+ if (trimmed.startsWith('- ')) {
7360
+ const tool = runtimeArtifactConversion._decodeToolScalar(trimmed.substring(2));
7361
+ if (tool !== null) (inAgentTools ? agentTools : agentDisallowed).push(tool);
7362
+ continue;
7363
+ }
7364
+ if (trimmed && !trimmed.startsWith('-')) {
7365
+ inAgentTools = false;
7366
+ inDisallowedTools = false;
7367
+ }
7368
+ }
7369
+
7231
7370
  // Detect inline tools: field (comma-separated string)
7232
7371
  if (trimmed.startsWith('tools:')) {
7233
7372
  if (isAgent) {
7234
- // Agents: strip tools entirely (not supported in OpenCode agent frontmatter)
7235
- inSkippedArray = true;
7373
+ const toolsValue = trimmed.substring(6).trim();
7374
+ // A comment-only value (`tools: # note`) is not inline content: read the block list instead.
7375
+ if (toolsValue && !toolsValue.startsWith('#')) {
7376
+ agentTools.push(...runtimeArtifactConversion._splitToolScalars(toolsValue).map(runtimeArtifactConversion._decodeToolScalar).filter((tool) => tool !== null));
7377
+ } else {
7378
+ inAgentTools = true;
7379
+ }
7236
7380
  continue;
7237
7381
  }
7238
7382
  const toolsValue = trimmed.substring(6).trim();
@@ -7244,8 +7388,18 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
7244
7388
  continue;
7245
7389
  }
7246
7390
 
7247
- // For agents: strip skills:, color:, memory:, maxTurns:, permissionMode:, disallowedTools:
7248
- if (isAgent && /^(skills|color|memory|maxTurns|permissionMode|disallowedTools):/.test(trimmed)) {
7391
+ if (isAgent && trimmed.startsWith('disallowedTools:')) {
7392
+ const value = trimmed.substring(16).trim();
7393
+ if (value && !value.startsWith('#')) {
7394
+ agentDisallowed.push(...runtimeArtifactConversion._splitToolScalars(value).map(runtimeArtifactConversion._decodeToolScalar).filter((tool) => tool !== null));
7395
+ } else {
7396
+ inDisallowedTools = true;
7397
+ }
7398
+ continue;
7399
+ }
7400
+
7401
+ // For agents: strip skills:, color:, memory:, maxTurns:, permissionMode:
7402
+ if (isAgent && /^(skills|color|memory|maxTurns|permissionMode):/.test(trimmed)) {
7249
7403
  inSkippedArray = true;
7250
7404
  continue;
7251
7405
  }
@@ -7318,6 +7472,7 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
7318
7472
  if (modelOverride) {
7319
7473
  newLines.push(`model: ${modelOverride}`);
7320
7474
  }
7475
+ newLines.push(...runtimeArtifactConversion._buildOpencodeAgentPermissionBlock(agentTools, agentDisallowed));
7321
7476
  }
7322
7477
 
7323
7478
  // For commands: add tools object if we had allowed-tools or tools
@@ -8239,7 +8394,7 @@ function reclaimKimiHooksRoot(kimiHooksRoot) {
8239
8394
  const kimiHooksLibDir = path.join(kimiHooksDir, 'lib');
8240
8395
  if (fs.existsSync(kimiHooksLibDir)) {
8241
8396
  let removedKimiLibFiles = 0;
8242
- for (const file of GSD_HOOK_LIB_FILES) {
8397
+ for (const file of gsdHookLibFiles()) {
8243
8398
  try {
8244
8399
  fs.unlinkSync(path.join(kimiHooksLibDir, file));
8245
8400
  removedKimiLibFiles++;
@@ -8875,7 +9030,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8875
9030
  const hooksLibDir = path.join(hooksDir, 'lib');
8876
9031
  if (fs.existsSync(hooksLibDir)) {
8877
9032
  let removedLibFiles = 0;
8878
- for (const file of GSD_HOOK_LIB_FILES) {
9033
+ for (const file of gsdHookLibFiles()) {
8879
9034
  const filePath = path.join(hooksLibDir, file);
8880
9035
  try {
8881
9036
  fs.unlinkSync(filePath);
@@ -8911,6 +9066,9 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8911
9066
  console.log(` ${green}✓${reset} Removed GSD hooks/package.json (CommonJS marker)`);
8912
9067
  }
8913
9068
  } catch { /* best-effort */ }
9069
+ // Once GSD's hooks, helpers and marker are gone, an empty hooks dir is GSD's own leftover (#5335).
9070
+ // rmdir fails on a non-empty dir, so a user's own hook scripts keep it in place.
9071
+ try { fs.rmdirSync(hooksDir); } catch { /* not empty or absent */ }
8914
9072
  }
8915
9073
 
8916
9074
  // 4z. Remove the native plugin adapter (#1914, extended to Kilo by #2093).
@@ -8944,6 +9102,20 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8944
9102
  // unlink above failed, pulling the marker out from under a still-present
8945
9103
  // CommonJS adapter would leave it unloadable. The exact content match
8946
9104
  // still leaves any user-authored package.json in place.
9105
+ // The terminal-client plugin directory (OpenCode 2.x statusline): GSD's file, then its now-empty directory.
9106
+ const _cliU = hostBehaviorsFor(runtime).nativeCliPlugin;
9107
+ if (_cliU) {
9108
+ const cliFile = path.join(targetDir, _cliU.dir, _cliU.file);
9109
+ if (fs.existsSync(cliFile)) {
9110
+ try {
9111
+ fs.unlinkSync(cliFile);
9112
+ removedCount++;
9113
+ removedFromPluginsDir = true;
9114
+ console.log(` ${green}✓${reset} Removed terminal-client plugin (${runtime})`);
9115
+ } catch (_) { /* best-effort */ }
9116
+ }
9117
+ try { fs.rmdirSync(path.join(targetDir, _cliU.dir)); } catch (_) { /* not empty or absent */ }
9118
+ }
8947
9119
  if (!fs.existsSync(pluginPath) && removeCommonJsMarker(pluginsDir)) {
8948
9120
  removedCount++;
8949
9121
  removedFromPluginsDir = true;
@@ -9171,40 +9343,56 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9171
9343
  }
9172
9344
  }
9173
9345
 
9174
- // 6. For OpenCode, clean up permissions from opencode.json or opencode.jsonc
9346
+ // 6. For OpenCode, clean up permissions and the companion MCP entry from opencode.json or opencode.jsonc
9175
9347
  if (resolveInstallPlan(runtime).finishPermissionWriter === 'opencode') {
9176
9348
  const configPath = resolveOpencodeConfigPath(targetDir);
9177
9349
  if (fs.existsSync(configPath)) {
9178
9350
  try {
9179
- const config = parseJsonc(fs.readFileSync(configPath, 'utf8'));
9180
- let modified = false;
9351
+ const originalText = fs.readFileSync(configPath, 'utf8');
9352
+ const config = parseJsonc(originalText);
9353
+ const edits = [];
9181
9354
 
9182
9355
  // Remove GSD permission entries
9183
- if (config.permission) {
9356
+ if (config.permission && typeof config.permission === 'object') {
9184
9357
  for (const permType of ['read', 'external_directory']) {
9185
- if (config.permission[permType]) {
9186
- const keys = Object.keys(config.permission[permType]);
9187
- for (const key of keys) {
9188
- if (key.includes('gsd-core')) {
9189
- delete config.permission[permType][key];
9190
- modified = true;
9191
- }
9192
- }
9193
- // Clean up empty objects
9194
- if (Object.keys(config.permission[permType]).length === 0) {
9195
- delete config.permission[permType];
9358
+ const section = config.permission[permType];
9359
+ if (!section || typeof section !== 'object') continue;
9360
+ for (const key of Object.keys(section)) {
9361
+ if (key.includes('gsd-core')) {
9362
+ delete section[key];
9363
+ edits.push({ path: ['permission', permType, key], remove: true });
9196
9364
  }
9197
9365
  }
9366
+ // Clean up empty objects
9367
+ if (Object.keys(section).length === 0) delete config.permission[permType];
9198
9368
  }
9199
- if (Object.keys(config.permission).length === 0) {
9200
- delete config.permission;
9369
+ if (Object.keys(config.permission).length === 0) delete config.permission;
9370
+ }
9371
+
9372
+ // #5333: remove the companion MCP entry, but only while it is still the entry GSD wrote — a
9373
+ // user-edited or user-defined `gsd` server stays. Both OpenCode generations' shapes: the 1.x
9374
+ // `mcp.gsd` and the 2.x native `mcp.servers.gsd`.
9375
+ let mcpRemoved = false;
9376
+ if (config.mcp && typeof config.mcp === 'object') {
9377
+ if (isGsdMcpEntry(config.mcp.gsd)) {
9378
+ delete config.mcp.gsd;
9379
+ edits.push({ path: ['mcp', 'gsd'], remove: true });
9380
+ mcpRemoved = true;
9201
9381
  }
9382
+ const servers = config.mcp.servers;
9383
+ if (servers && typeof servers === 'object' && isGsdMcpEntry(servers.gsd)) {
9384
+ delete servers.gsd;
9385
+ edits.push({ path: ['mcp', 'servers', 'gsd'], remove: true });
9386
+ if (Object.keys(servers).length === 0) delete config.mcp.servers;
9387
+ mcpRemoved = true;
9388
+ }
9389
+ if (Object.keys(config.mcp).length === 0) delete config.mcp;
9202
9390
  }
9203
9391
 
9204
- if (modified) {
9205
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
9392
+ if (edits.length > 0) {
9393
+ writeJsoncConfig(configPath, originalText, config, edits);
9206
9394
  removedCount++;
9207
- console.log(` ${green}✓${reset} Removed GSD permissions from ${path.basename(configPath)}`);
9395
+ console.log(` ${green}✓${reset} Removed GSD permissions${mcpRemoved ? ' and MCP server' : ''} from ${path.basename(configPath)}`);
9208
9396
  }
9209
9397
  } catch (e) {
9210
9398
  // Ignore JSON parse errors
@@ -9219,8 +9407,9 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9219
9407
  const configPath = resolveKiloConfigPath(targetDir);
9220
9408
  if (fs.existsSync(configPath)) {
9221
9409
  try {
9222
- const config = parseJsonc(fs.readFileSync(configPath, 'utf8'));
9223
- let modified = false;
9410
+ const originalText = fs.readFileSync(configPath, 'utf8');
9411
+ const config = parseJsonc(originalText);
9412
+ const edits = [];
9224
9413
 
9225
9414
  // Remove GSD permission entries
9226
9415
  if (config.permission) {
@@ -9230,7 +9419,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9230
9419
  for (const key of keys) {
9231
9420
  if (key.includes('gsd-core')) {
9232
9421
  delete config.permission[permType][key];
9233
- modified = true;
9422
+ edits.push({ path: ['permission', permType, key], remove: true });
9234
9423
  }
9235
9424
  }
9236
9425
  // Clean up empty objects
@@ -9244,8 +9433,8 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9244
9433
  }
9245
9434
  }
9246
9435
 
9247
- if (modified) {
9248
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
9436
+ if (edits.length > 0) {
9437
+ writeJsoncConfig(configPath, originalText, config, edits);
9249
9438
  removedCount++;
9250
9439
  console.log(` ${green}✓${reset} Removed GSD permissions from ${path.basename(configPath)}`);
9251
9440
  }
@@ -9287,6 +9476,67 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9287
9476
  console.log(` ${green}✓${reset} Removed ${MANIFEST_NAME}`);
9288
9477
  }
9289
9478
 
9479
+ // #5335: the installer's own bookkeeping goes with it — the applied-migrations state and the migration
9480
+ // journal. The journal's `-backups/` folders hold copies of files the USER had modified, so a folder that
9481
+ // still has content is kept; everything GSD generated for itself (run records, rollback snapshots, empty
9482
+ // folders) is removed.
9483
+ const installStatePath = path.join(targetDir, 'gsd-install-state.json');
9484
+ if (fs.existsSync(installStatePath)) {
9485
+ fs.rmSync(installStatePath, { force: true });
9486
+ removedCount++;
9487
+ console.log(` ${green}✓${reset} Removed gsd-install-state.json`);
9488
+ }
9489
+ const journalDir = path.join(targetDir, 'gsd-migration-journal');
9490
+ if (fs.existsSync(journalDir)) {
9491
+ let journalEntriesRemoved = 0;
9492
+ try {
9493
+ for (const entry of fs.readdirSync(journalDir, { withFileTypes: true })) {
9494
+ const full = path.join(journalDir, entry.name);
9495
+ if (entry.isSymbolicLink()) continue;
9496
+ if (entry.isFile() && entry.name.endsWith('.json')) {
9497
+ fs.rmSync(full, { force: true });
9498
+ journalEntriesRemoved++;
9499
+ } else if (entry.isDirectory() && entry.name.endsWith('-rollback')) {
9500
+ fs.rmSync(full, { recursive: true, force: true });
9501
+ journalEntriesRemoved++;
9502
+ } else if (entry.isDirectory() && entry.name.endsWith('-backups')) {
9503
+ try { fs.rmdirSync(full); journalEntriesRemoved++; } catch { /* holds the user's backed-up files: keep */ }
9504
+ }
9505
+ }
9506
+ fs.rmdirSync(journalDir);
9507
+ } catch { /* a non-empty journal dir (kept backups) stays */ }
9508
+ if (journalEntriesRemoved > 0) {
9509
+ removedCount++;
9510
+ console.log(` ${green}✓${reset} Removed gsd-migration-journal/ records`);
9511
+ }
9512
+ }
9513
+
9514
+ // #5335: the installer's user-artifact staging area is empty once the install it served is over. rmdir only
9515
+ // succeeds on an empty directory, so anything still staged there is left for its owner.
9516
+ try {
9517
+ fs.rmdirSync(path.join(targetDir, '.gsd-staging', 'user-artifacts'));
9518
+ fs.rmdirSync(path.join(targetDir, '.gsd-staging'));
9519
+ } catch { /* absent or not empty */ }
9520
+
9521
+ // #5335: OpenCode never reads settings.json, but the shared-settings install step creates it as `{}`.
9522
+ // An empty one carries nothing of the user's; a populated one is left alone.
9523
+ if (resolveInstallPlan(runtime).finishPermissionWriter === 'opencode') {
9524
+ const emptySettingsPath = path.join(targetDir, 'settings.json');
9525
+ try {
9526
+ const parsed = JSON.parse(fs.readFileSync(emptySettingsPath, 'utf8'));
9527
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && Object.keys(parsed).length === 0) {
9528
+ fs.rmSync(emptySettingsPath, { force: true });
9529
+ removedCount++;
9530
+ console.log(` ${green}✓${reset} Removed empty settings.json`);
9531
+ }
9532
+ } catch { /* absent, unreadable or populated: leave it */ }
9533
+ }
9534
+
9535
+ // #5335: the machine-wide defaults the install wrote for this runtime, when nothing else needs them.
9536
+ if (revertNonClaudeDefaults(runtime)) {
9537
+ removedCount++;
9538
+ }
9539
+
9290
9540
  if (removedCount === 0) {
9291
9541
  console.log(` ${yellow}⚠${reset} No GSD files found to remove.`);
9292
9542
  }
@@ -9298,64 +9548,30 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9298
9548
  }
9299
9549
 
9300
9550
  /**
9301
- * Parse JSONC (JSON with Comments) by stripping comments and trailing commas.
9302
- * OpenCode supports JSONC format via jsonc-parser, so users may have comments.
9303
- * This is a lightweight inline parser to avoid adding dependencies.
9304
- */
9305
- function parseJsonc(content) {
9306
- // Strip BOM if present
9307
- if (content.charCodeAt(0) === 0xFEFF) {
9308
- content = content.slice(1);
9309
- }
9310
-
9311
- // Remove single-line and block comments while preserving strings
9312
- let result = '';
9313
- let inString = false;
9314
- let i = 0;
9315
- while (i < content.length) {
9316
- const char = content[i];
9317
- const next = content[i + 1];
9318
-
9319
- if (inString) {
9320
- result += char;
9321
- // Handle escape sequences
9322
- if (char === '\\' && i + 1 < content.length) {
9323
- result += next;
9324
- i += 2;
9325
- continue;
9326
- }
9327
- if (char === '"') {
9328
- inString = false;
9329
- }
9330
- i++;
9331
- } else {
9332
- if (char === '"') {
9333
- inString = true;
9334
- result += char;
9335
- i++;
9336
- } else if (char === '/' && next === '/') {
9337
- // Skip single-line comment until end of line
9338
- while (i < content.length && content[i] !== '\n') {
9339
- i++;
9340
- }
9341
- } else if (char === '/' && next === '*') {
9342
- // Skip block comment
9343
- i += 2;
9344
- while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/')) {
9345
- i++;
9346
- }
9347
- i += 2; // Skip closing */
9348
- } else {
9349
- result += char;
9350
- i++;
9351
- }
9551
+ * Write back an OpenCode/Kilo config the installer changed (#5334).
9552
+ *
9553
+ * A file that carries JSONC syntax (a `.jsonc` name, comments or trailing commas) is edited as text, so
9554
+ * only the keys GSD owns change and the user's annotations and layout survive. Anything the text editor
9555
+ * cannot handle, and plain JSON, keeps the previous `JSON.stringify` rewrite.
9556
+ *
9557
+ * @param {string} configPath
9558
+ * @param {string|null} originalText current file text, or null when the file is new
9559
+ * @param {object} config the mutated parsed config (used for the fallback)
9560
+ * @param {Array<{path: string[], value?: unknown, remove?: boolean}>} edits
9561
+ */
9562
+ function writeJsoncConfig(configPath, originalText, config, edits) {
9563
+ if (typeof originalText === 'string' && (configPath.endsWith('.jsonc') || hasJsoncFeatures(originalText))) {
9564
+ let text = originalText;
9565
+ for (const edit of edits) {
9566
+ text = edit.remove ? removeJsoncValue(text, edit.path) : setJsoncValue(text, edit.path, edit.value);
9567
+ if (text === null) break;
9568
+ }
9569
+ if (text !== null) {
9570
+ fs.writeFileSync(configPath, text);
9571
+ return;
9352
9572
  }
9353
9573
  }
9354
-
9355
- // Remove trailing commas before } or ]
9356
- result = result.replace(/,(\s*[}\]])/g, '$1');
9357
-
9358
- return JSON.parse(result);
9574
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
9359
9575
  }
9360
9576
 
9361
9577
  /**
@@ -9377,9 +9593,11 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
9377
9593
 
9378
9594
  // Read existing config or create empty object
9379
9595
  let config = {};
9596
+ let originalText = null;
9380
9597
  if (fs.existsSync(configPath)) {
9381
9598
  try {
9382
9599
  const content = fs.readFileSync(configPath, 'utf8');
9600
+ originalText = content;
9383
9601
  config = parseJsonc(content);
9384
9602
  } catch (e) {
9385
9603
  // Cannot parse - DO NOT overwrite user's config
@@ -9410,6 +9628,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
9410
9628
  : `${opencodeConfigDir.replace(/\\/g, '/')}/gsd-core/*`;
9411
9629
 
9412
9630
  let modified = false;
9631
+ const edits = [];
9413
9632
 
9414
9633
  // Configure read permission
9415
9634
  if (!config.permission.read || typeof config.permission.read !== 'object') {
@@ -9417,6 +9636,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
9417
9636
  }
9418
9637
  if (config.permission.read[gsdPath] !== 'allow') {
9419
9638
  config.permission.read[gsdPath] = 'allow';
9639
+ edits.push({ path: ['permission', 'read', gsdPath], value: 'allow' });
9420
9640
  modified = true;
9421
9641
  }
9422
9642
 
@@ -9426,6 +9646,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
9426
9646
  }
9427
9647
  if (config.permission.external_directory[gsdPath] !== 'allow') {
9428
9648
  config.permission.external_directory[gsdPath] = 'allow';
9649
+ edits.push({ path: ['permission', 'external_directory', gsdPath], value: 'allow' });
9429
9650
  modified = true;
9430
9651
  }
9431
9652
 
@@ -9442,9 +9663,10 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
9442
9663
  if (config.mcp.gsd === undefined) {
9443
9664
  config.mcp.gsd = {
9444
9665
  type: 'local',
9445
- command: ['npx', '-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'],
9666
+ command: ['npx', '-y', '-p', PACKAGE_NAME, 'gsd-mcp-server', GSD_MCP_NO_PROMPTS_FLAG],
9446
9667
  enabled: true,
9447
9668
  };
9669
+ edits.push({ path: ['mcp', 'gsd'], value: config.mcp.gsd });
9448
9670
  modified = true;
9449
9671
  }
9450
9672
 
@@ -9453,7 +9675,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
9453
9675
  }
9454
9676
 
9455
9677
  // Write config back
9456
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
9678
+ writeJsoncConfig(configPath, originalText, config, edits);
9457
9679
  console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
9458
9680
  }
9459
9681
 
@@ -9476,9 +9698,11 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
9476
9698
 
9477
9699
  // Read existing config or create empty object
9478
9700
  let config = {};
9701
+ let originalText = null;
9479
9702
  if (fs.existsSync(configPath)) {
9480
9703
  try {
9481
9704
  const content = fs.readFileSync(configPath, 'utf8');
9705
+ originalText = content;
9482
9706
  config = parseJsonc(content);
9483
9707
  } catch (e) {
9484
9708
  // Cannot parse - DO NOT overwrite user's config
@@ -9503,6 +9727,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
9503
9727
  : `${kiloConfigDir.replace(/\\/g, '/')}/gsd-core/*`;
9504
9728
 
9505
9729
  let modified = false;
9730
+ const edits = [];
9506
9731
 
9507
9732
  // Configure read permission
9508
9733
  if (!config.permission.read || typeof config.permission.read !== 'object') {
@@ -9510,6 +9735,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
9510
9735
  }
9511
9736
  if (config.permission.read[gsdPath] !== 'allow') {
9512
9737
  config.permission.read[gsdPath] = 'allow';
9738
+ edits.push({ path: ['permission', 'read', gsdPath], value: 'allow' });
9513
9739
  modified = true;
9514
9740
  }
9515
9741
 
@@ -9519,6 +9745,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
9519
9745
  }
9520
9746
  if (config.permission.external_directory[gsdPath] !== 'allow') {
9521
9747
  config.permission.external_directory[gsdPath] = 'allow';
9748
+ edits.push({ path: ['permission', 'external_directory', gsdPath], value: 'allow' });
9522
9749
  modified = true;
9523
9750
  }
9524
9751
 
@@ -9527,7 +9754,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
9527
9754
  }
9528
9755
 
9529
9756
  // Write config back
9530
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
9757
+ writeJsoncConfig(configPath, originalText, config, edits);
9531
9758
  console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
9532
9759
  }
9533
9760
 
@@ -10012,8 +10239,9 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
10012
10239
  // to git-cmd.js (validate-commit classifier) and gsd-graphify-rebuild.sh.
10013
10240
  const hooksLibDir = path.join(hooksDir, 'lib');
10014
10241
  if (fs.existsSync(hooksLibDir)) {
10242
+ const hookLibNames = new Set(gsdHookLibFiles());
10015
10243
  for (const file of fs.readdirSync(hooksLibDir)) {
10016
- if (GSD_HOOK_LIB_FILES.includes(file)) {
10244
+ if (hookLibNames.has(file)) {
10017
10245
  manifest.files[sharedHooksDirName + '/lib/' + file] = fileHash(path.join(hooksLibDir, file));
10018
10246
  }
10019
10247
  }
@@ -10063,6 +10291,14 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
10063
10291
  manifest.files[`${_npM.dir}/${_npM.file}`] = fileHash(pluginInstallPath);
10064
10292
  }
10065
10293
  }
10294
+ // The OpenCode 2.x terminal-client plugin (statusline / update banner) is tracked beside it.
10295
+ const _cliM = hostBehaviorsFor(runtime).nativeCliPlugin;
10296
+ if (_cliM) {
10297
+ const cliInstallPath = path.join(configDir, _cliM.dir, _cliM.file);
10298
+ if (fs.existsSync(cliInstallPath)) {
10299
+ manifest.files[`${_cliM.dir}/${_cliM.file}`] = fileHash(cliInstallPath);
10300
+ }
10301
+ }
10066
10302
 
10067
10303
  fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
10068
10304
  return manifest;
@@ -12330,6 +12566,41 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12330
12566
  }
12331
12567
  }
12332
12568
  }
12569
+ // #5123: the Copilot and Antigravity converters write their DEFAULT home
12570
+ // into `@` includes and Kimi's prompt converter leaves a prose "GSD core/"
12571
+ // path, none of which exist when the payload lives somewhere else. Point
12572
+ // them at the real install root (the same `$HOME/…` / absolute prefix the
12573
+ // other runtimes use). Global installs only: a local install resolves
12574
+ // against its project, which the converters already handle.
12575
+ const includeRetarget = hostBehaviorsFor(runtime);
12576
+ if (isGlobal && (includeRetarget.defaultHomeIncludeRoot || includeRetarget.retargetsGsdCoreProsePath === true)) {
12577
+ const includePrefix = runtimeArtifactConversion._computePathPrefix({
12578
+ isGlobal: true,
12579
+ isOpencode: false,
12580
+ resolvedTarget: targetDir,
12581
+ homeDir: os.homedir(),
12582
+ });
12583
+ for (const relPath of manifestFiles) {
12584
+ const fileName = path.basename(relPath);
12585
+ if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
12586
+ if (fileName === 'CHANGELOG.md') continue;
12587
+ const retargetPath = path.join(targetDir, relPath);
12588
+ let retargetContent;
12589
+ try {
12590
+ retargetContent = fs.readFileSync(retargetPath, 'utf8');
12591
+ } catch (_retargetReadErr) {
12592
+ continue; // inaccessible or missing — never fail the install over this pass
12593
+ }
12594
+ const retargeted = runtimeArtifactConversion.retargetInstalledIncludes(retargetContent, includeRetarget, includePrefix);
12595
+ if (retargeted !== retargetContent) {
12596
+ try {
12597
+ fs.writeFileSync(retargetPath, retargeted);
12598
+ } catch (_retargetWriteErr) {
12599
+ continue;
12600
+ }
12601
+ }
12602
+ }
12603
+ }
12333
12604
  for (const relPath of manifestFiles) {
12334
12605
  const fileName = path.basename(relPath);
12335
12606
  if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
@@ -13024,7 +13295,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13024
13295
  }
13025
13296
  const kimiHookOpts = { portableHooks: hasPortableHooks, runtime };
13026
13297
  const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
13027
- const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts });
13298
+ const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts, contextWarnings: runtimeSupportsContextWarnings(runtime) });
13028
13299
  configuredEntrypoints.push(...kimiHooksResult.configuredEntrypoints);
13029
13300
  if (kimiHooksResult.changed) {
13030
13301
  console.log(` ${green}✓${reset} Configured ${kimiHooksResult.entryCount} GSD hook(s) in ${kimiHooksTomlPath}`);
@@ -13306,6 +13577,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13306
13577
  const updateCheckCommand = isGlobal
13307
13578
  ? buildHookCommand(targetDir, 'gsd-check-update.js', hookOpts)
13308
13579
  : localCmd('gsd-check-update.js');
13580
+ // #4840: no metrics writer on this host -> no monitor to register (see runtimeSupportsContextWarnings).
13581
+ const contextWarnings = runtimeSupportsContextWarnings(runtime);
13582
+ // The command stays built: the extended lifecycle events (SubagentStop, PreCompact, ...) still route to it.
13309
13583
  const contextMonitorCommand = isGlobal
13310
13584
  ? buildHookCommand(targetDir, 'gsd-context-monitor.js', hookOpts)
13311
13585
  : localCmd('gsd-context-monitor.js');
@@ -13350,6 +13624,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13350
13624
  hooksSurface: plan.hooksSurface,
13351
13625
  updateCheckCommand,
13352
13626
  contextMonitorCommand,
13627
+ contextWarnings,
13353
13628
  promptGuardCommand,
13354
13629
  readGuardCommand,
13355
13630
  readInjectionScannerCommand,
@@ -13358,6 +13633,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13358
13633
  localCmd,
13359
13634
  localShellCmd,
13360
13635
  });
13636
+ if (!contextWarnings) {
13637
+ console.log(` ${dim}↳${reset} ${getRuntimeLabel(runtime)}: agent-facing context warnings are unsupported (no statusline writes the context metrics the monitor reads), so the context monitor hook is not registered`);
13638
+ }
13361
13639
 
13362
13640
  // Compute the update-banner hook command alongside the others so
13363
13641
  // installAllRuntimes can register it at finalize time when the user opts
@@ -13626,6 +13904,31 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
13626
13904
  // Configure OpenCode permissions
13627
13905
  if (plan.finishPermissionWriter === 'opencode' && !process.env.GSD_TEST_MODE) {
13628
13906
  configureOpencodePermissions(isGlobal, configDir);
13907
+ // ADR-5331: say which OpenCode generation this install will run under. Informational only —
13908
+ // a failed probe never affects the install.
13909
+ try {
13910
+ const ocv = require('../gsd-core/bin/lib/opencode-version.cjs');
13911
+ const gen = ocv.detectOpencodeGeneration();
13912
+ if (gen.generation !== null && typeof configDir === 'string' && configDir) {
13913
+ recordHostGeneration(configDir, { generation: gen.generation, version: gen.version });
13914
+ }
13915
+ if (gen.generation === 2) {
13916
+ console.log(` ${green}✓${reset} OpenCode ${gen.version || '2.x'} detected — GSD loads its OpenCode 2.x plugin entrypoint`);
13917
+ } else if (gen.generation === 1) {
13918
+ console.log(` ${green}✓${reset} OpenCode ${gen.version || '1.x'} detected — GSD loads its OpenCode 1.x plugin entrypoint`);
13919
+ }
13920
+ // ADR-5331 D6 / E8: OpenCode 2.x migrates its data directory one way. Say so before it happens, and say
13921
+ // when 1.x is installed over a data directory 2.x already migrated (1.x cannot open it).
13922
+ const dataDir = ocv.resolveOpencodeDataDir();
13923
+ const notice = ocv.describeDataDirNotice(gen.generation, ocv.classifyOpencodeDatabase(path.join(dataDir, 'opencode.db')), dataDir);
13924
+ if (notice) {
13925
+ console.log(notice.level === 'warn' ? ` ${yellow}⚠${reset} ${notice.text}` : ` ${dim}${notice.text}${reset}`);
13926
+ }
13927
+ // Phase 3 (#5339): OpenCode 1.x is bug-fix-only until the freeze date. The `opencode` runtime only —
13928
+ // this block does not run for Kilo.
13929
+ const deprecation = ocv.describeDeprecationNotice(gen.generation);
13930
+ if (deprecation) console.log(` ${yellow}⚠${reset} ${deprecation}`);
13931
+ } catch { /* detection is advisory */ }
13629
13932
  }
13630
13933
 
13631
13934
  // Configure Kilo permissions