@opengsd/gsd-core 1.16.0 → 1.17.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 (58) 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-debug-session-manager.compact.md +6 -0
  7. package/agents/gsd-debug-session-manager.md +6 -0
  8. package/bin/gsd-mcp-server.js +3 -1
  9. package/bin/install.js +361 -94
  10. package/gsd-core/bin/gsd-tools.cjs +17 -2
  11. package/gsd-core/bin/lib/adr-parser.cjs +37 -5
  12. package/gsd-core/bin/lib/capability-registry.cjs +132 -80
  13. package/gsd-core/bin/lib/capability-validator.cjs +2 -1
  14. package/gsd-core/bin/lib/capability-writer.cjs +2 -1
  15. package/gsd-core/bin/lib/commands.cjs +28 -8
  16. package/gsd-core/bin/lib/gate-context-drift.cjs +5 -4
  17. package/gsd-core/bin/lib/gate-ui-safety.cjs +2 -1
  18. package/gsd-core/bin/lib/graphify.cjs +3 -2
  19. package/gsd-core/bin/lib/init.cjs +32 -12
  20. package/gsd-core/bin/lib/install-engine.cjs +11 -0
  21. package/gsd-core/bin/lib/install-shadow-report.cjs +12 -0
  22. package/gsd-core/bin/lib/installer-migrations/011-opencode-mcp-no-prompts.cjs +99 -0
  23. package/gsd-core/bin/lib/installer-migrations.cjs +54 -1
  24. package/gsd-core/bin/lib/jsonc-edit.cjs +418 -0
  25. package/gsd-core/bin/lib/loop-host-contract.cjs +2 -1
  26. package/gsd-core/bin/lib/mcp-server.cjs +5 -1
  27. package/gsd-core/bin/lib/opencode-mcp-entry.cjs +39 -0
  28. package/gsd-core/bin/lib/opencode-version.cjs +486 -0
  29. package/gsd-core/bin/lib/phase.cjs +21 -0
  30. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +2 -1
  31. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +95 -4
  32. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +58 -3
  33. package/gsd-core/bin/lib/runtime-slash.cjs +13 -0
  34. package/gsd-core/bin/lib/smart-entry.cjs +92 -53
  35. package/gsd-core/bin/lib/state-transition.cjs +67 -9
  36. package/gsd-core/bin/lib/state.cjs +454 -66
  37. package/gsd-core/bin/lib/update-context.cjs +18 -1
  38. package/gsd-core/bin/lib/verification.cjs +107 -17
  39. package/gsd-core/workflows/code-review.md +16 -1
  40. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
  41. package/gsd-core/workflows/execute-phase.md +3 -1
  42. package/gsd-core/workflows/resume-project.md +14 -10
  43. package/hooks/dist/gsd-check-update.js +26 -20
  44. package/hooks/dist/gsd-context-monitor.js +14 -5
  45. package/hooks/dist/gsd-secret-read-guard.js +117 -1
  46. package/hooks/dist/gsd-statusline.js +321 -22
  47. package/hooks/gsd-check-update.js +26 -20
  48. package/hooks/gsd-context-monitor.js +14 -5
  49. package/hooks/gsd-secret-read-guard.js +117 -1
  50. package/hooks/gsd-statusline.js +321 -22
  51. package/package.json +1 -1
  52. package/scripts/gen-capability-registry.cjs +7 -1
  53. package/scripts/gen-loop-host-contract.cjs +4 -1
  54. package/scripts/lib/macos-conformance-tier.generated.cjs +4 -0
  55. package/scripts/lib/platform-conformance-tier.generated.cjs +10 -0
  56. package/scripts/mutation-matrix.cjs +3 -1
  57. package/scripts/release-tarball-smoke.cjs +14 -7
  58. package/vscode/package.json +1 -1
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "gsd-core",
11
11
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
12
- "version": "1.16.0",
12
+ "version": "1.17.0",
13
13
  "source": "./",
14
14
  "author": {
15
15
  "name": "open-gsd",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gsd-core",
3
3
  "displayName": "GSD Core",
4
- "version": "1.16.0",
4
+ "version": "1.17.0",
5
5
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
6
6
  "author": {
7
7
  "name": "open-gsd",
@@ -18,6 +18,5 @@
18
18
  "claude-code",
19
19
  "gsd"
20
20
  ],
21
- "commands": "./commands/gsd/",
22
21
  "skills": "./skills/"
23
22
  }
@@ -146,6 +146,11 @@ const TOOL_NAME_MAP = {
146
146
  websearch: "WebSearch",
147
147
  task: "Task",
148
148
  subagent: "Task",
149
+ // >>> gsd:opencode-v2
150
+ // OpenCode 2.x tool ids (ADR-5331 E4): `shell` replaces `bash`, `patch` replaces `apply_patch`.
151
+ shell: "Bash",
152
+ patch: "MultiEdit",
153
+ // <<< gsd:opencode-v2
149
154
  };
150
155
 
151
156
  function mapToolName(tool) {
@@ -239,6 +244,18 @@ function gitProbingGuardTimeoutMs() {
239
244
  return GIT_PROBING_GUARD_FALLBACK_TIMEOUT_MS;
240
245
  }
241
246
 
247
+ // Hooks are Node scripts. `process.execPath` is only a JS runtime when the host is Node or Bun
248
+ // itself; inside OpenCode's single-file binary (1.x and 2.x) spawning it with a .js path boots
249
+ // OpenCode instead and exits 0, silently disabling every guard (#4849). So: an explicit
250
+ // GSD_HOOK_RUNTIME wins, then a Node/Bun host's own executable, else `node` from PATH.
251
+ let warnedMissingRuntime = false;
252
+ function hookRuntime() {
253
+ const fromEnv = process.env.GSD_HOOK_RUNTIME;
254
+ if (typeof fromEnv === "string" && fromEnv.length > 0) return fromEnv;
255
+ const base = path.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
256
+ return base === "node" || base === "bun" ? process.execPath : "node";
257
+ }
258
+
242
259
  function runHook(hookFile, payload, opts = {}) {
243
260
  const hookPath = path.join(HOOKS_DIR, hookFile);
244
261
  if (!fs.existsSync(hookPath)) {
@@ -261,7 +278,7 @@ function runHook(hookFile, payload, opts = {}) {
261
278
  (GIT_PROBING_GUARDS.has(hookFile) ? gitProbingGuardTimeoutMs() : 8000);
262
279
  let result;
263
280
  try {
264
- result = spawnSync(process.execPath, [hookPath], {
281
+ result = spawnSync(hookRuntime(), [hookPath], {
265
282
  input: JSON.stringify(payload),
266
283
  encoding: "utf8",
267
284
  timeout,
@@ -273,6 +290,15 @@ function runHook(hookFile, payload, opts = {}) {
273
290
  return { stdout: "", exitCode: 0, timedOut: false };
274
291
  }
275
292
 
293
+ if (result.error && !warnedMissingRuntime) {
294
+ // The runtime could not be started: the guard did not run. Fail open (adapter contract) but
295
+ // never silently (#4849).
296
+ warnedMissingRuntime = true;
297
+ console.error(
298
+ `[gsd-core] cannot start hook runtime '${hookRuntime()}' (${result.error.code || result.error.message}) — ` +
299
+ "GSD hooks are NOT enforced. Install Node.js or set GSD_HOOK_RUNTIME to a node/bun executable.",
300
+ );
301
+ }
276
302
  const stdout = (result.stdout || "").trim();
277
303
  const exitCode = result.status == null ? 0 : result.status;
278
304
  return { stdout, exitCode, timedOut: result.signal === "SIGTERM" };
@@ -358,9 +384,81 @@ function handleHookResult(hookResult, output) {
358
384
  }
359
385
  // Best-effort visibility when metadata isn't surfaced to the model
360
386
  console.error(advisory);
387
+ // >>> gsd:opencode-parity
388
+ deliverAdvisory(advisory, output);
389
+ // <<< gsd:opencode-parity
390
+ }
391
+ }
392
+ // >>> gsd:opencode-parity (ADR-5331 Phase 3; OpenCode-only — the Kilo mirror omits this block)
393
+ // The four opt-in community hooks are bash scripts. Their gates (hooks.community, graphify.*) are
394
+ // re-checked here in-process so a project that has not opted in never pays a bash spawn per tool call.
395
+ function readProjectConfig(cwd) {
396
+ try {
397
+ return JSON.parse(fs.readFileSync(path.join(cwd, ".planning", "config.json"), "utf8"));
398
+ } catch {
399
+ return null;
361
400
  }
362
401
  }
363
402
 
403
+ function communityHooksEnabled(cwd) {
404
+ const config = readProjectConfig(cwd);
405
+ return !!config && config.hooks && config.hooks.community === true;
406
+ }
407
+
408
+ function graphifyAutoUpdateEnabled(cwd) {
409
+ const config = readProjectConfig(cwd);
410
+ return !!config && !process.env.CI && !!config.graphify &&
411
+ config.graphify.enabled === true && config.graphify.auto_update === true;
412
+ }
413
+
414
+ /**
415
+ * Run a GSD `.sh` hook under bash with the hook payload on stdin. Fails open: a missing script or no
416
+ * bash on PATH (a Windows host without Git Bash) is the same as the hook not being installed.
417
+ */
418
+ function runShellHook(hookFile, payload, opts = {}) {
419
+ const hookPath = path.join(HOOKS_DIR, hookFile);
420
+ if (!fs.existsSync(hookPath)) return { stdout: "", exitCode: 0, timedOut: false };
421
+ let result;
422
+ try {
423
+ result = spawnSync(process.env.GSD_HOOK_SHELL || "bash", [hookPath], {
424
+ input: JSON.stringify(payload),
425
+ encoding: "utf8",
426
+ timeout: opts.timeout ?? 15000,
427
+ cwd: opts.cwd || currentCwd,
428
+ windowsHide: true,
429
+ });
430
+ } catch {
431
+ return { stdout: "", exitCode: 0, timedOut: false };
432
+ }
433
+ if (result.error) return { stdout: "", exitCode: 0, timedOut: false };
434
+ return {
435
+ stdout: (result.stdout || "").trim(),
436
+ exitCode: result.status == null ? 0 : result.status,
437
+ timedOut: result.signal === "SIGTERM",
438
+ };
439
+ }
440
+
441
+ // The SessionStart orientation text, produced once per session and handed to the model with its next request.
442
+ let pendingSessionContext = null;
443
+
444
+ // Advisories (read-before-edit, injection findings, phase-boundary, context warnings) were logged to stderr
445
+ // only — OpenCode does not show a plugin's stderr or metadata to the model. They are appended to the tool's
446
+ // own output instead, which is the one channel the model reads on both generations. An advisory raised before
447
+ // the tool runs waits here for that tool's result.
448
+ const pendingAdvisories = [];
449
+ function deliverAdvisory(text, output) {
450
+ if (output && typeof output.output === "string") {
451
+ output.output += "\n\n[GSD advisory] " + text;
452
+ return;
453
+ }
454
+ if (!pendingAdvisories.includes(text) && pendingAdvisories.length < 5) pendingAdvisories.push(text);
455
+ }
456
+ function flushPendingAdvisories(output) {
457
+ if (!output || typeof output.output !== "string") return;
458
+ while (pendingAdvisories.length > 0) output.output += "\n\n[GSD advisory] " + pendingAdvisories.shift();
459
+ }
460
+ // <<< gsd:opencode-parity
461
+
364
462
  // ---------------------------------------------------------------------------
365
463
  // Frontmatter helpers (for config registration)
366
464
  // ---------------------------------------------------------------------------
@@ -667,6 +765,13 @@ const GsdCorePlugin = async ({ directory } = {}) => {
667
765
  const r = runHook("gsd-secret-read-guard.js", prePayload());
668
766
  handleHookResult(r, output);
669
767
  }
768
+ // >>> gsd:opencode-parity
769
+ // 7. gsd-validate-commit.sh — Conventional Commits gate (opt-in: hooks.community)
770
+ if (claudeTool === "Bash" && communityHooksEnabled(cwd)) {
771
+ const r = runShellHook("gsd-validate-commit.sh", prePayload());
772
+ handleHookResult(r, output);
773
+ }
774
+ // <<< gsd:opencode-parity
670
775
  },
671
776
 
672
777
  // ── tool.execute.after — PostToolUse hooks ─────────────────────────
@@ -690,6 +795,25 @@ const GsdCorePlugin = async ({ directory } = {}) => {
690
795
  : String(output.output);
691
796
  output.output = rewriteContent(content);
692
797
  }
798
+ // >>> gsd:opencode-parity
799
+ flushPendingAdvisories(output);
800
+ // gsd-phase-boundary.sh — reminder when .planning/ files change outside the workflow (opt-in)
801
+ // gsd-graphify-update.sh — rebuild the knowledge graph after a HEAD-advancing git op (opt-in)
802
+ {
803
+ const postPayload = {
804
+ hook_event_name: "PostToolUse",
805
+ tool_name: claudeTool,
806
+ tool_input: toolInput,
807
+ cwd,
808
+ };
809
+ if ((claudeTool === "Write" || claudeTool === "Edit") && communityHooksEnabled(cwd)) {
810
+ handleHookResult(runShellHook("gsd-phase-boundary.sh", postPayload), output);
811
+ }
812
+ if (claudeTool === "Bash" && graphifyAutoUpdateEnabled(cwd)) {
813
+ handleHookResult(runShellHook("gsd-graphify-update.sh", postPayload, { timeout: 8000 }), output);
814
+ }
815
+ }
816
+ // <<< gsd:opencode-parity
693
817
 
694
818
  // gsd-read-injection-scanner.js — scan Read/WebFetch/WebSearch results
695
819
  if (
@@ -727,6 +851,16 @@ const GsdCorePlugin = async ({ directory } = {}) => {
727
851
  handleHookResult(r, output);
728
852
  }
729
853
  },
854
+ // >>> gsd:opencode-parity
855
+ // ── experimental.chat.system.transform — SessionStart orientation (gsd-session-state.sh) ──
856
+ "experimental.chat.system.transform": async (_input, output) => {
857
+ if (!pendingSessionContext) return;
858
+ const text = pendingSessionContext;
859
+ pendingSessionContext = null;
860
+ output.system = output.system || [];
861
+ output.system.push(text);
862
+ },
863
+ // <<< gsd:opencode-parity
730
864
 
731
865
  // ── experimental.session.compacting — PreCompact ───────────────────
732
866
  "experimental.session.compacting": async (_input, output) => {
@@ -770,6 +904,23 @@ const GsdCorePlugin = async ({ directory } = {}) => {
770
904
  session_id: currentSessionId,
771
905
  cwd: currentCwd,
772
906
  });
907
+ // >>> gsd:opencode-parity
908
+ // gsd-session-state.sh — project state orientation for the session (opt-in: hooks.community)
909
+ if (communityHooksEnabled(currentCwd)) {
910
+ const r = runShellHook("gsd-session-state.sh", {
911
+ hook_event_name: "SessionStart",
912
+ session_id: currentSessionId,
913
+ cwd: currentCwd,
914
+ });
915
+ try {
916
+ const parsed = JSON.parse(r.stdout);
917
+ const text = parsed && parsed.hookSpecificOutput && parsed.hookSpecificOutput.additionalContext;
918
+ pendingSessionContext = typeof text === "string" && text ? text : null;
919
+ } catch {
920
+ pendingSessionContext = null;
921
+ }
922
+ }
923
+ // <<< gsd:opencode-parity
773
924
  return;
774
925
  }
775
926
 
@@ -864,7 +1015,106 @@ GsdCorePlugin._internals = {
864
1015
  GsdCorePlugin,
865
1016
  };
866
1017
 
1018
+ // >>> gsd:opencode-v2 (ADR-5331 D2; OpenCode 2.x entrypoint — the Kilo mirror omits this block)
1019
+ // OpenCode 2.x calls `setup(ctx)` with a `ctx` exposing `ctx.tool.hook(...)`; 1.x loads `server`
1020
+ // and never calls `setup` with such a ctx. `setup` therefore returns at once unless it is
1021
+ // handed a 2.x ctx. The 1.x hook handlers are reused through thin adapters so both generations
1022
+ // run the same guards.
1023
+ function v2ResultText(result) {
1024
+ const first = result && Array.isArray(result.content) ? result.content[0] : null;
1025
+ return first && typeof first.text === "string" ? first.text : null;
1026
+ }
1027
+
1028
+ async function setupV2(ctx) {
1029
+ if (!(ctx && ctx.tool && typeof ctx.tool.hook === "function")) return undefined;
1030
+ const hooks = await GsdCorePlugin({ directory: ctx.directory });
1031
+ // A thrown error in a 2.x before-hook blocks the tool (verified on 2.0.27), as in 1.x.
1032
+ await ctx.tool.hook("execute.before", async (e) => {
1033
+ await hooks["tool.execute.before"]({ tool: e.tool }, { args: e.input || {} });
1034
+ });
1035
+ await ctx.tool.hook("execute.after", async (e) => {
1036
+ if (e.status !== "completed") return;
1037
+ const text = v2ResultText(e.result);
1038
+ const out = { output: text, metadata: {} };
1039
+ await hooks["tool.execute.after"]({ tool: e.tool, args: e.input || {} }, out);
1040
+ if (text !== null && typeof out.output === "string" && out.output !== text) {
1041
+ e.result.content[0].text = out.output;
1042
+ }
1043
+ // 2.x emits no file.edited event (observed on 2.0.27), so derive it from the edit tools: the
1044
+ // only consumer is the .planning/config.json reload hook.
1045
+ const claudeTool = mapToolName(e.tool);
1046
+ if (claudeTool === "Write" || claudeTool === "Edit" || claudeTool === "MultiEdit") {
1047
+ const edited = mapToolInput(e.input || {}).file_path;
1048
+ if (typeof edited === "string" && edited.endsWith("config.json")) {
1049
+ await hooks.event({
1050
+ event: { type: "file.edited", properties: { file: edited, cwd: path.dirname(path.dirname(edited)) } },
1051
+ });
1052
+ }
1053
+ }
1054
+ });
1055
+ if (ctx.session && typeof ctx.session.hook === "function") {
1056
+ // 2.x compaction event: { sessionID, system: [{ type: "text", text }], messages, … }; items added
1057
+ // to `system` reach the summarizer (observed on 2.0.27).
1058
+ // gsd:opencode-parity: SessionStart orientation reaches the model through the 2.x context hook.
1059
+ await ctx.session.hook("context", async (e) => {
1060
+ if (!e || !Array.isArray(e.system)) return;
1061
+ const out = { system: [] };
1062
+ await hooks["experimental.chat.system.transform"]({}, out);
1063
+ for (const line of out.system) e.system.push({ type: "text", text: String(line) });
1064
+ });
1065
+ await ctx.session.hook("compaction", async (e) => {
1066
+ if (!e || !Array.isArray(e.system)) return;
1067
+ const out = { context: [] };
1068
+ await hooks["experimental.session.compacting"]({}, out);
1069
+ for (const line of out.context) e.system.push({ type: "text", text: String(line) });
1070
+ });
1071
+ }
1072
+ if (ctx.shell && typeof ctx.shell.hook === "function") {
1073
+ await ctx.shell.hook("create.before", async (e) => {
1074
+ if (e && e.env && typeof e.env === "object") e.env.GSD_DIR = GSD_CORE;
1075
+ });
1076
+ }
1077
+ let stop = null;
1078
+ const seenSessions = new Set();
1079
+ if (ctx.event && typeof ctx.event.subscribe === "function") {
1080
+ const ac = new AbortController();
1081
+ stop = () => ac.abort();
1082
+ (async () => {
1083
+ for await (const ev of ctx.event.subscribe({ signal: ac.signal })) {
1084
+ // 2.0.27 shape (observed): { type, location: { directory }, data: { sessionID, … } }.
1085
+ const d = (ev && ev.data) || {};
1086
+ const dir = (ev && ev.location && ev.location.directory) || (d.location && d.location.directory);
1087
+ try {
1088
+ // A 2.x `run` never emits `session.created` (observed on 2.0.27: the first event of a session is
1089
+ // `session.inbox.enqueued`). The first event that names a session is its SessionStart.
1090
+ if (ev.type !== "session.created" && typeof d.sessionID === "string" && !seenSessions.has(d.sessionID)) {
1091
+ seenSessions.add(d.sessionID);
1092
+ await hooks.event({
1093
+ event: { type: "session.created", properties: { info: { id: d.sessionID, directory: dir } }, sessionID: d.sessionID },
1094
+ });
1095
+ } else if (ev.type === "session.created" && typeof d.sessionID === "string") {
1096
+ seenSessions.add(d.sessionID);
1097
+ }
1098
+ await hooks.event({
1099
+ event: {
1100
+ type: ev.type,
1101
+ properties: { info: { id: d.sessionID, directory: dir }, file: d.file, cwd: dir },
1102
+ sessionID: d.sessionID,
1103
+ },
1104
+ });
1105
+ } catch {
1106
+ // one bad event must not end the subscription
1107
+ }
1108
+ }
1109
+ })().catch(() => {});
1110
+ }
1111
+ return stop || undefined;
1112
+ }
1113
+ // <<< gsd:opencode-v2
867
1114
  const gsdCorePluginExport = { server: GsdCorePlugin };
1115
+ // >>> gsd:opencode-v2
1116
+ Object.defineProperty(gsdCorePluginExport, "setup", { value: setupV2, enumerable: false });
1117
+ // <<< gsd:opencode-v2
868
1118
  Object.defineProperty(gsdCorePluginExport, "id", {
869
1119
  value: "gsd-core",
870
1120
  enumerable: false,
@@ -0,0 +1,55 @@
1
+ // GSD statusline and update banner for the OpenCode 2.x terminal client (ADR-5331 Phase 3).
2
+ //
3
+ // A CLI plugin (https://opencode.ai/v2/docs/build/plugins/cli): OpenCode 2.x loads `tui.tsx` from a
4
+ // directory under plugins/ and renders the slots registered here. The line itself comes from GSD's own
5
+ // hooks/gsd-statusline.js — the script Claude Code runs — so state, progress, git and update-available
6
+ // output stay one implementation. OpenCode 1.x never loads this directory (it scans plugins/*.js|ts only).
7
+ import { Plugin } from "@opencode/plugin/tui"
8
+ import { execFile } from "node:child_process"
9
+ import { existsSync } from "node:fs"
10
+ import path from "node:path"
11
+ import { fileURLToPath } from "node:url"
12
+
13
+ const REFRESH_MS = 10_000
14
+ // Strip terminal colour codes: the host styles its own footer.
15
+ const ANSI = /\u001b\[[0-9;?]*[ -/]*[@-~]/g
16
+
17
+ function hooksDir() {
18
+ // <config>/plugins/gsd-statusline/tui.tsx → <config>/hooks
19
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "hooks")
20
+ }
21
+
22
+ function runtime() {
23
+ return process.env.GSD_HOOK_RUNTIME || "node"
24
+ }
25
+
26
+ function render(directory: string, model: string, done: (line: string) => void) {
27
+ const script = path.join(hooksDir(), "gsd-statusline.js")
28
+ // Only inside a GSD project, and only when the script is installed.
29
+ if (!existsSync(script) || !existsSync(path.join(directory, ".planning"))) return done("")
30
+ const child = execFile(runtime(), [script], { cwd: directory, timeout: 4000, windowsHide: true }, (error, stdout) => {
31
+ done(error ? "" : String(stdout).replace(ANSI, "").split("\n")[0].trim())
32
+ })
33
+ child.stdin?.end(JSON.stringify({ model: { display_name: model }, workspace: { current_dir: directory } }))
34
+ }
35
+
36
+ export default Plugin.define({
37
+ id: "gsd.statusline",
38
+ setup(context) {
39
+ const directory = context.location?.directory ?? context.data.location.default().directory
40
+ const [state, update] = context.storage.memory("line", { initial: { text: "" } })
41
+ const refresh = () => {
42
+ const selected = context.ui.model.current()
43
+ render(directory, selected?.modelID || "OpenCode", (text) => update((draft: { text: string }) => { draft.text = text }))
44
+ }
45
+ refresh()
46
+ const timer = setInterval(refresh, REFRESH_MS)
47
+ const stopHome = context.ui.slot({ append: "home.footer.status", render: () => <text>{state.text}</text> })
48
+ const stopPrompt = context.ui.slot({ append: "prompt.footer.status", render: () => <text>{state.text}</text> })
49
+ return () => {
50
+ clearInterval(timer)
51
+ stopHome?.()
52
+ stopPrompt?.()
53
+ }
54
+ },
55
+ })
package/GEMINI.md CHANGED
@@ -25,7 +25,7 @@ files rather than in the conversation.
25
25
  > run the dedicated installer:
26
26
  >
27
27
  > ```bash
28
- > npx gsd-core --antigravity --global
28
+ > npx @opengsd/gsd-core@latest --antigravity --global
29
29
  > ```
30
30
  >
31
31
  > The commands below are available only once that installer has run.
@@ -111,6 +111,12 @@ only "spawn continuation agent" without naming a format, so they inherit this ru
111
111
  a flag written at each one — which is exactly why Step 2 must remain the only `Agent()` spawn
112
112
  literal in this file.
113
113
 
114
+ **Never spawn the debugger through Bash — #5214.** The only way to start `gsd-debugger` is the Step 2
115
+ `Agent()` call above. A `Bash` command such as `claude --agent gsd-debugger …`, with or without
116
+ `run_in_background`, starts a detached process that returns no structured header, so the checkpoint
117
+ never advances. The Bash guard hook denies it (`debugger-cli-spawn`). If the `Agent` call itself fails,
118
+ stop and return that error to the orchestrator; do not improvise a shell spawn.
119
+
114
120
  Resolve the debugger model before spawning (canonical `gsd_run` preamble — established once here, the single definition this agent carries):
115
121
  ```bash
116
122
  _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _gsd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') return 0;; *) return 1;; esac; }; _gsd_homes() { set -- "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}" "$HOME/.gemini/antigravity-ide" "$HOME/.gemini/antigravity-cli" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}" "${CLINE_CONFIG_DIR:-$HOME/.cline}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}" "${CODEX_HOME:-$HOME/.codex}" "${COPILOT_CONFIG_DIR:-${COPILOT_HOME:-$HOME/.copilot}}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}" "${HERMES_HOME:-$HOME/.hermes}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}" "${KIMI_CONFIG_DIR:-$HOME/.config/agents}" "$HOME/.agents" "${KIMI_CODE_HOME:-$HOME/.kimi-code}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}" "${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}" "${TRAE_CONFIG_DIR:-$HOME/.trae}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}" "${ZCODE_CONFIG_DIR:-$HOME/.zcode}" "${GROK_AGENTS_HOME:-$HOME/.agents}"; for _h; do _gsd_at "$_h/gsd-core/bin/${_GSD_SHIM_NAME}" && return 0; done; return 1; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif _gsd_homes; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; [ -n "$_G" ] && _gsd_id_ok "$_G"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and no identity-proving gsd_run is on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; _gsd_id_ok gsd_run && GSD_IDENTITY_STATUS=ok; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
@@ -117,6 +117,12 @@ only "spawn continuation agent" without naming a format, so they inherit this ru
117
117
  a flag written at each one — which is exactly why Step 2 must remain the only `Agent()` spawn
118
118
  literal in this file.
119
119
 
120
+ **Never spawn the debugger through Bash — #5214.** The only way to start `gsd-debugger` is the Step 2
121
+ `Agent()` call above. A `Bash` command such as `claude --agent gsd-debugger …`, with or without
122
+ `run_in_background`, starts a detached process that returns no structured header, so the checkpoint
123
+ never advances. The Bash guard hook denies it (`debugger-cli-spawn`). If the `Agent` call itself fails,
124
+ stop and return that error to the orchestrator; do not improvise a shell spawn.
125
+
120
126
  Resolve the debugger model before spawning:
121
127
  ```bash
122
128
  _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _gsd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') return 0;; *) return 1;; esac; }; _gsd_homes() { set -- "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}" "$HOME/.gemini/antigravity-ide" "$HOME/.gemini/antigravity-cli" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}" "${CLINE_CONFIG_DIR:-$HOME/.cline}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}" "${CODEX_HOME:-$HOME/.codex}" "${COPILOT_CONFIG_DIR:-${COPILOT_HOME:-$HOME/.copilot}}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}" "${HERMES_HOME:-$HOME/.hermes}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}" "${KIMI_CONFIG_DIR:-$HOME/.config/agents}" "$HOME/.agents" "${KIMI_CODE_HOME:-$HOME/.kimi-code}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}" "${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}" "${TRAE_CONFIG_DIR:-$HOME/.trae}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}" "${ZCODE_CONFIG_DIR:-$HOME/.zcode}" "${GROK_AGENTS_HOME:-$HOME/.agents}"; for _h; do _gsd_at "$_h/gsd-core/bin/${_GSD_SHIM_NAME}" && return 0; done; return 1; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif _gsd_homes; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; [ -n "$_G" ] && _gsd_id_ok "$_G"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and no identity-proving gsd_run is on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; _gsd_id_ok gsd_run && GSD_IDENTITY_STATUS=ok; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
@@ -24,7 +24,9 @@ const { runServer } = require('../gsd-core/bin/lib/mcp-server.cjs');
24
24
  runServer({
25
25
  input: process.stdin,
26
26
  output: process.stdout,
27
- ctx: { cwd: process.cwd() },
27
+ // --no-prompts: do not advertise the prompt catalog (hosts that turn MCP prompts into slash commands
28
+ // would list every GSD command twice).
29
+ ctx: { cwd: process.cwd(), prompts: !process.argv.includes('--no-prompts') },
28
30
  }).catch((err) => {
29
31
  process.stderr.write(String((err && err.message) || err) + '\n');
30
32
  process.exit(1);