@gajae-code/coding-agent 0.11.3 → 0.11.5

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 (213) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/types/cli/notify-cli.d.ts +5 -1
  3. package/dist/types/cli/worktree-cli.d.ts +8 -25
  4. package/dist/types/commands/gc.d.ts +4 -0
  5. package/dist/types/commands/worktree.d.ts +7 -32
  6. package/dist/types/config/settings-schema.d.ts +20 -0
  7. package/dist/types/export/html/template.generated.d.ts +1 -1
  8. package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
  9. package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
  10. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  11. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
  12. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
  13. package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
  14. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
  15. package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
  16. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  17. package/dist/types/harness-control-plane/owner.d.ts +3 -1
  18. package/dist/types/internal-urls/local-protocol.d.ts +18 -0
  19. package/dist/types/main.d.ts +2 -2
  20. package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
  21. package/dist/types/modes/components/custom-editor.d.ts +8 -3
  22. package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
  23. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
  24. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  25. package/dist/types/modes/components/tool-transcript-format.d.ts +77 -0
  26. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
  27. package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
  28. package/dist/types/modes/controllers/input-controller.d.ts +8 -2
  29. package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
  30. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
  31. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
  32. package/dist/types/plan-mode/approved-plan.d.ts +1 -0
  33. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  34. package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
  35. package/dist/types/sdk/broker/broker.d.ts +1 -0
  36. package/dist/types/sdk/broker/discovery.d.ts +10 -0
  37. package/dist/types/sdk/broker/ensure.d.ts +24 -1
  38. package/dist/types/sdk/broker/internal.d.ts +3 -0
  39. package/dist/types/sdk/broker/session-index.d.ts +12 -0
  40. package/dist/types/sdk/broker/state-version.d.ts +2 -1
  41. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
  43. package/dist/types/sdk/bus/config-commands.d.ts +8 -0
  44. package/dist/types/sdk/bus/config.d.ts +22 -0
  45. package/dist/types/sdk/bus/index.d.ts +162 -2
  46. package/dist/types/sdk/bus/notification-service.d.ts +71 -8
  47. package/dist/types/sdk/bus/session-control.d.ts +11 -8
  48. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
  49. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
  50. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
  51. package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
  52. package/dist/types/sdk/session.d.ts +16 -1
  53. package/dist/types/secrets/index.d.ts +4 -2
  54. package/dist/types/secrets/obfuscator.d.ts +1 -1
  55. package/dist/types/session/agent-session.d.ts +31 -2
  56. package/dist/types/session/artifacts.d.ts +30 -8
  57. package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
  58. package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
  59. package/dist/types/session/session-manager.d.ts +88 -18
  60. package/dist/types/session/session-storage.d.ts +27 -0
  61. package/dist/types/session/streaming-output.d.ts +4 -0
  62. package/dist/types/task/executor.d.ts +9 -0
  63. package/dist/types/tool-discovery/tool-index.d.ts +2 -0
  64. package/dist/types/tools/ask.d.ts +87 -12
  65. package/dist/types/tools/bash-skill-urls.d.ts +2 -2
  66. package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
  67. package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
  68. package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
  69. package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
  70. package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
  71. package/dist/types/tools/index.d.ts +4 -0
  72. package/dist/types/tools/path-utils.d.ts +9 -14
  73. package/dist/types/tools/render-utils.d.ts +1 -8
  74. package/dist/types/tools/resource-gc.d.ts +14 -0
  75. package/dist/types/utils/image-loading.d.ts +23 -0
  76. package/dist/types/utils/image-resize.d.ts +2 -0
  77. package/dist/types/utils/pasted-image-loading.d.ts +39 -0
  78. package/dist/types/utils/pasted-image-path.d.ts +21 -19
  79. package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
  80. package/dist/types/web/scrapers/types.d.ts +1 -5
  81. package/package.json +25 -9
  82. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  83. package/scripts/run-threat-model-check.ts +60 -0
  84. package/src/cli/notify-cli.ts +40 -18
  85. package/src/cli/worktree-cli.ts +9 -287
  86. package/src/cli.ts +10 -9
  87. package/src/commands/gc.ts +11 -1
  88. package/src/commands/sdk.ts +10 -4
  89. package/src/commands/worktree.ts +10 -53
  90. package/src/config/model-profiles.ts +15 -15
  91. package/src/config/settings-schema.ts +17 -0
  92. package/src/daemon/runtime.ts +2 -2
  93. package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
  94. package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
  95. package/src/defaults/gjc/skills/team/SKILL.md +7 -9
  96. package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
  97. package/src/eval/js/shared/runtime.ts +1 -1
  98. package/src/eval/py/index.ts +2 -2
  99. package/src/export/html/template.generated.ts +1 -1
  100. package/src/extensibility/plugins/marketplace/manager.ts +94 -10
  101. package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
  102. package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
  103. package/src/gjc-runtime/deep-interview-state.ts +127 -3
  104. package/src/gjc-runtime/gc-render.ts +19 -1
  105. package/src/gjc-runtime/gc-runtime.ts +68 -3
  106. package/src/gjc-runtime/launch-tmux.ts +61 -6
  107. package/src/gjc-runtime/managed-owner-admission.ts +352 -0
  108. package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
  109. package/src/gjc-runtime/state-argv.ts +292 -0
  110. package/src/gjc-runtime/state-runtime.ts +80 -222
  111. package/src/gjc-runtime/team-runtime.ts +1 -1
  112. package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
  113. package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
  114. package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
  115. package/src/harness-control-plane/owner.ts +59 -3
  116. package/src/hindsight/client.ts +65 -2
  117. package/src/internal-urls/agent-protocol.ts +76 -5
  118. package/src/internal-urls/docs-index.generated.ts +7 -6
  119. package/src/internal-urls/local-protocol.ts +416 -18
  120. package/src/lsp/index.ts +4 -88
  121. package/src/main.ts +37 -13
  122. package/src/modes/acp/acp-agent.ts +8 -0
  123. package/src/modes/components/ansi-display-validator.ts +54 -0
  124. package/src/modes/components/custom-editor.ts +103 -32
  125. package/src/modes/components/custom-provider-wizard.ts +26 -1
  126. package/src/modes/components/notifications-settings-editor.ts +26 -2
  127. package/src/modes/components/session-observer-overlay.ts +40 -37
  128. package/src/modes/components/settings-selector.ts +3 -1
  129. package/src/modes/components/tool-transcript-format.ts +393 -0
  130. package/src/modes/components/transcript-viewer-overlay.ts +63 -1
  131. package/src/modes/controllers/goal-mode-controller.ts +20 -1
  132. package/src/modes/controllers/input-controller.ts +98 -37
  133. package/src/modes/controllers/plan-mode-controller.ts +2 -0
  134. package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
  135. package/src/modes/controllers/selector-controller.ts +71 -9
  136. package/src/modes/interactive-mode.ts +14 -2
  137. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
  138. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
  139. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
  140. package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
  141. package/src/plan-mode/approved-plan.ts +3 -1
  142. package/src/prompts/agents/architect.md +1 -0
  143. package/src/prompts/agents/critic.md +1 -0
  144. package/src/prompts/system/custom-system-prompt.md +1 -1
  145. package/src/prompts/system/system-prompt.md +2 -1
  146. package/src/prompts/tools/lsp.md +3 -3
  147. package/src/runtime-mcp/manager.ts +16 -1
  148. package/src/runtime-mcp/oauth-discovery.ts +116 -26
  149. package/src/sdk/broker/broker.ts +139 -32
  150. package/src/sdk/broker/discovery.ts +125 -0
  151. package/src/sdk/broker/ensure.ts +103 -19
  152. package/src/sdk/broker/internal.ts +10 -0
  153. package/src/sdk/broker/session-index.ts +309 -38
  154. package/src/sdk/broker/state-version.ts +5 -2
  155. package/src/sdk/bus/chat-daemon-cli.ts +72 -10
  156. package/src/sdk/bus/chat-daemon-control.ts +526 -130
  157. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  158. package/src/sdk/bus/config-commands.ts +22 -0
  159. package/src/sdk/bus/config.ts +37 -0
  160. package/src/sdk/bus/index.ts +384 -92
  161. package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
  162. package/src/sdk/bus/notification-service.ts +284 -62
  163. package/src/sdk/bus/recent-activity.ts +14 -5
  164. package/src/sdk/bus/session-control.ts +150 -44
  165. package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
  166. package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
  167. package/src/sdk/bus/telegram-daemon-control.ts +294 -76
  168. package/src/sdk/bus/telegram-daemon.ts +1842 -281
  169. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  170. package/src/sdk/session.ts +155 -27
  171. package/src/secrets/index.ts +53 -10
  172. package/src/secrets/obfuscator.ts +21 -18
  173. package/src/secrets/regex.ts +2 -0
  174. package/src/session/agent-session.ts +495 -253
  175. package/src/session/artifacts.ts +188 -23
  176. package/src/session/context-estimation.ts +7 -2
  177. package/src/session/internal/managed-session-scope.ts +215 -39
  178. package/src/session/internal/managed-session-storage.ts +1152 -46
  179. package/src/session/session-manager.ts +1055 -148
  180. package/src/session/session-storage.ts +109 -11
  181. package/src/session/streaming-output.ts +6 -0
  182. package/src/task/executor.ts +71 -26
  183. package/src/task/index.ts +63 -20
  184. package/src/tool-discovery/tool-index.ts +18 -0
  185. package/src/tools/ask.ts +329 -57
  186. package/src/tools/bash-allowed-prefixes.ts +79 -29
  187. package/src/tools/bash-skill-urls.ts +14 -3
  188. package/src/tools/bash.ts +2 -2
  189. package/src/tools/browser/launch.ts +3 -1
  190. package/src/tools/browser/readable.ts +1 -1
  191. package/src/tools/browser/screenshot-format.ts +34 -0
  192. package/src/tools/browser/tab-protocol.ts +3 -0
  193. package/src/tools/browser/tab-supervisor.ts +125 -122
  194. package/src/tools/browser/tab-worker-entry.ts +33 -14
  195. package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
  196. package/src/tools/browser/tab-worker-smoke.ts +87 -0
  197. package/src/tools/browser/tab-worker.ts +7 -5
  198. package/src/tools/index.ts +4 -0
  199. package/src/tools/path-utils.ts +42 -35
  200. package/src/tools/plan-mode-guard.ts +1 -0
  201. package/src/tools/read.ts +7 -1
  202. package/src/tools/render-utils.ts +1 -27
  203. package/src/tools/resource-gc.ts +123 -34
  204. package/src/tools/sqlite-reader.ts +3 -2
  205. package/src/tools/subagent.ts +2 -0
  206. package/src/utils/image-loading.ts +85 -34
  207. package/src/utils/image-resize.ts +16 -9
  208. package/src/utils/pasted-image-loading.ts +514 -0
  209. package/src/utils/pasted-image-path.ts +223 -98
  210. package/src/web/scrapers/html-to-markdown.ts +68 -0
  211. package/src/web/scrapers/readthedocs.ts +1 -1
  212. package/src/web/scrapers/types.ts +1 -68
  213. package/src/web/search/providers/openai-compatible.ts +76 -23
@@ -14,6 +14,61 @@ function assert(condition: unknown, message: string): asserts condition {
14
14
  if (!condition) throw new Error(message);
15
15
  }
16
16
 
17
+ async function sourceFiles(root: string): Promise<string[]> {
18
+ const files: string[] = [];
19
+ for (const entry of await fs.readdir(root, { withFileTypes: true })) {
20
+ const file = path.join(root, entry.name);
21
+ if (entry.isDirectory()) files.push(...(await sourceFiles(file)));
22
+ else if (/\.(?:ts|tsx|js|mjs|cjs)$/.test(entry.name)) files.push(file);
23
+ }
24
+ return files;
25
+ }
26
+
27
+ function hasDiscoverySignalAuthority(source: string): boolean {
28
+ return /(?:process\.)?kill\s*\(\s*(?:discovery|brokerDiscovery|record)\.pid\s*,\s*["']SIG(?:TERM|KILL)["']/m.test(
29
+ source,
30
+ );
31
+ }
32
+
33
+ async function assertNoDiscoverySignalAuthority(): Promise<void> {
34
+ const roots = [path.resolve(import.meta.dir, "../src/sdk"), path.resolve(import.meta.dir, "../test")];
35
+ const offenders: string[] = [];
36
+ for (const root of roots) {
37
+ for (const file of await sourceFiles(root)) {
38
+ if (hasDiscoverySignalAuthority(await fs.readFile(file, "utf8")))
39
+ offenders.push(path.relative(process.cwd(), file));
40
+ }
41
+ }
42
+ assert(offenders.length === 0, `Discovery-derived TERM/KILL authority remains in: ${offenders.join(", ")}`);
43
+ assert(
44
+ hasDiscoverySignalAuthority('process.kill(discovery.pid, "SIGTERM")'),
45
+ "Synthetic forbidden discovery signal was not detected",
46
+ );
47
+ assert(
48
+ !hasDiscoverySignalAuthority("process.kill(discovery.pid, 0)"),
49
+ "PID-zero observation was escalated to signal authority",
50
+ );
51
+ }
52
+
53
+ async function assertFixtureControlsStayTestOnly(): Promise<void> {
54
+ const productionRoot = path.resolve(import.meta.dir, "../src");
55
+ const offenders: string[] = [];
56
+ for (const file of await sourceFiles(productionRoot)) {
57
+ const source = await fs.readFile(file, "utf8");
58
+ if (/\b(?:GSF1|SSH1)\b/.test(source)) offenders.push(path.relative(process.cwd(), file));
59
+ }
60
+ assert(
61
+ offenders.length === 0,
62
+ `Fixture control frames are reachable from production source: ${offenders.join(", ")}`,
63
+ );
64
+ const ensureSource = await fs.readFile(path.resolve(import.meta.dir, "../src/sdk/broker/ensure.ts"), "utf8");
65
+ assert(
66
+ ensureSource.includes("startFixtureBrokerCommandWithLeaseForTest"),
67
+ "Dedicated fixture launch boundary is missing its explicit ForTest label",
68
+ );
69
+ assert(!/GJC_.*(?:GSF1|SSH1|SELF_REAP)/.test(ensureSource), "Production environment can select fixture controls");
70
+ }
71
+
17
72
  async function rejectedWebSocket(url: string): Promise<void> {
18
73
  const ws = new WebSocket(url);
19
74
  await new Promise<void>((resolve, reject) => {
@@ -132,6 +187,11 @@ async function main(): Promise<void> {
132
187
  assert(!discovery.url.includes("0.0.0.0"), "non-loopback broker URL exists");
133
188
  await rejectedWebSocket(`${discovery.url}/?token=wrong-token`);
134
189
  },
190
+ "broker-discovery-derived-signal-authority-forbidden": assertNoDiscoverySignalAuthority,
191
+ "broker-pid-zero-observation-allowed": async () => {
192
+ assert(!hasDiscoverySignalAuthority("process.kill(discovery.pid, 0)"), "PID-zero observation was rejected");
193
+ },
194
+ "broker-fixture-control-nonproduction-reachability": assertFixtureControlsStayTestOnly,
135
195
  };
136
196
  for (const check of manifest.checks) {
137
197
  try {
@@ -7,6 +7,7 @@ import { createInterface } from "node:readline/promises";
7
7
  import { APP_NAME } from "@gajae-code/utils/dirs";
8
8
  import chalk from "chalk";
9
9
  import { Settings, type SettingsAtomicPatch } from "../config/settings";
10
+ import { isProcessIncarnation, processIncarnation } from "../sdk/broker/process-incarnation";
10
11
  import { type EnsureChatDaemonResult, ensureDiscordDaemon, ensureSlackDaemon } from "../sdk/bus/chat-daemon-control";
11
12
  import { getNotificationConfig, maskToken, tokenFingerprint } from "../sdk/bus/config";
12
13
  import {
@@ -29,7 +30,11 @@ import {
29
30
  sanitizeDiagnostic,
30
31
  sendNotificationTest,
31
32
  } from "../sdk/bus/notification-service";
32
- import { ensureTelegramDaemonRunningDetailed, readDaemonState } from "../sdk/bus/telegram-daemon";
33
+ import {
34
+ type EnsureTelegramDaemonDetailedResult,
35
+ ensureTelegramDaemonRunningDetailed,
36
+ readDaemonState,
37
+ } from "../sdk/bus/telegram-daemon";
33
38
  import { runDaemonInternal } from "../sdk/bus/telegram-daemon-cli";
34
39
  import {
35
40
  runTelegramSetup as runTelegramPairingSetup,
@@ -82,10 +87,10 @@ export interface NotifyCommandDeps {
82
87
  setupTimers?: TelegramSetupTimers;
83
88
  setupAbortSignal?: AbortSignal;
84
89
  setupPidAlive?: (pid: number) => boolean;
85
- ensureProviderDaemon?: (
86
- provider: "discord" | "slack",
87
- settings: Settings,
88
- ) => Promise<EnsureChatDaemonResult | "failed">;
90
+ /** Injectable process-start provenance reader for ambient Telegram setup preflight. */
91
+ setupPidIncarnation?: (pid: number) => string | undefined;
92
+ ensureProviderDaemon?: (provider: "discord" | "slack", settings: Settings) => Promise<EnsureChatDaemonResult>;
93
+ ensureTelegramDaemon?: (settings: Settings) => Promise<EnsureTelegramDaemonDetailedResult>;
89
94
  }
90
95
 
91
96
  export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
@@ -313,12 +318,22 @@ async function ensureConfiguredProviderDaemon(
313
318
  provider: "discord" | "slack",
314
319
  settings: Settings,
315
320
  deps: NotifyCommandDeps,
316
- ): Promise<EnsureChatDaemonResult | "failed"> {
321
+ ): Promise<EnsureChatDaemonResult> {
317
322
  try {
318
323
  if (deps.ensureProviderDaemon) return await deps.ensureProviderDaemon(provider, settings);
319
324
  return provider === "discord" ? await ensureDiscordDaemon(settings) : await ensureSlackDaemon(settings);
320
- } catch {
321
- return "failed";
325
+ } catch (error) {
326
+ const cfg = getNotificationConfig(settings);
327
+ const detail = sanitizeDiagnostic(
328
+ sanitizeDiagnostic(
329
+ error instanceof Error ? error.message : String(error),
330
+ provider === "discord" ? cfg.discord.botToken : cfg.slack.appToken,
331
+ ),
332
+ provider === "discord" ? cfg.discord.botToken : cfg.slack.botToken,
333
+ );
334
+ throw new Error(`${provider === "discord" ? "Discord" : "Slack"} daemon did not become ready: ${detail}`, {
335
+ cause: error,
336
+ });
322
337
  }
323
338
  }
324
339
 
@@ -393,11 +408,13 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
393
408
  reconcileCurrentSession: async () => undefined,
394
409
  },
395
410
  reconnect: async () =>
396
- await ensureTelegramDaemonRunningDetailed({
397
- settings,
398
- cwd: process.cwd(),
399
- sessionId: `notify-cli-${process.pid}`,
400
- }),
411
+ deps.ensureTelegramDaemon
412
+ ? await deps.ensureTelegramDaemon(settings)
413
+ : await ensureTelegramDaemonRunningDetailed({
414
+ settings,
415
+ cwd: process.cwd(),
416
+ sessionId: `notify-cli-${process.pid}`,
417
+ }),
401
418
  persistInactive: async marker => await persistTelegramActivationMarker(settings, marker),
402
419
  clearInactive: async marker => await clearTelegramActivationMarker(settings, marker),
403
420
  marker: activationMarker,
@@ -446,14 +463,19 @@ async function resolveSetupPreflight(settings: Settings, deps: NotifyCommandDeps
446
463
  const state = await readDaemonState(settings);
447
464
  if (!state) return { storedChatId: cfg.chatId };
448
465
  const validPid = Number.isSafeInteger(state.pid) && state.pid > 0;
449
- // Owner-proof: only block discovery for a daemon we can positively prove is
450
- // live (present state with a valid, alive pid). A malformed/no-pid record is
451
- // not evidence of a live poller, mirroring recoverNotifications' semantics.
452
- if (!validPid) return { storedChatId: cfg.chatId };
466
+ if (!validPid || !(deps.setupPidAlive ?? daemonPidAlive)(state.pid)) return { storedChatId: cfg.chatId };
467
+ const persistedIncarnation = state.incarnation;
468
+ const currentIncarnation = (deps.setupPidIncarnation ?? processIncarnation)(state.pid);
469
+ if (
470
+ !isProcessIncarnation(persistedIncarnation) ||
471
+ !isProcessIncarnation(currentIncarnation) ||
472
+ persistedIncarnation !== currentIncarnation
473
+ )
474
+ return { storedChatId: cfg.chatId };
453
475
  return {
454
476
  storedChatId: cfg.chatId,
455
477
  daemon: {
456
- live: (deps.setupPidAlive ?? daemonPidAlive)(state.pid),
478
+ live: true,
457
479
  tokenFingerprint: typeof state.tokenFingerprint === "string" ? state.tokenFingerprint : undefined,
458
480
  chatId: typeof state.chatId === "string" ? state.chatId : undefined,
459
481
  },
@@ -1,291 +1,13 @@
1
1
  /**
2
- * CLI handler for `gjc worktree` — list and clean up agent-managed worktrees.
2
+ * Tombstone for the removed legacy worktree cleanup API.
3
3
  *
4
- * Layout under `~/.gjc/wt/`:
5
- *
6
- * - **PR-checkout worktrees** (`tools/gh.ts`): a regular git worktree dir
7
- * containing a `.git` *file* that points back at
8
- * `<parent-repo>/.git/worktrees/<name>/`.
9
- * - **Task-isolation dirs** (`task/worktree.ts`): a wrapper dir with a
10
- * `merged` subdir mounted/cloned by `natives.isoStart`. These are ephemeral
11
- * — `ensureIsolation` always `rm -rf`s the base before re-creating it, so
12
- * any leftover on disk is a leak from a crashed run.
13
- *
14
- * Legacy entries from before the encoding change keep working because git still
15
- * tracks them by branch name. This command exists to GC them on demand.
4
+ * The recursive-deletion implementation that previously lived here
5
+ * (`listWorktrees`/`clearWorktrees`) was deliberately removed and does not
6
+ * come back through this subpath. This module exists only to fail imports
7
+ * with actionable migration guidance.
16
8
  */
17
- import * as fs from "node:fs/promises";
18
- import * as path from "node:path";
19
- import { getWorktreesDir, isEnoent } from "@gajae-code/utils";
20
- import chalk from "chalk";
21
- import * as git from "../utils/git";
22
-
23
- type WorktreeKind = "pr-checkout" | "task-isolation" | "empty" | "stray";
24
-
25
- export interface WorktreeEntry {
26
- /** Absolute path to the worktree dir (or stray container) under `~/.gjc/wt/`. */
27
- path: string;
28
- /** Classification of what we found on disk. */
29
- kind: WorktreeKind;
30
- /** Parent repo root, when this is a registered git worktree. */
31
- parentRepo?: string;
32
- /** Branch name extracted from the parent's tracking file, when available. */
33
- branch?: string;
34
- /** When set, the entry is unhealthy and `gjc worktree clear` will remove it. */
35
- orphanReason?: string;
36
- }
37
-
38
- export interface ListWorktreesOptions {
39
- json: boolean;
40
- }
41
-
42
- export interface ClearWorktreesOptions {
43
- /** Remove every entry, including live PR-checkout worktrees. */
44
- all: boolean;
45
- /** Print what would be removed without touching the filesystem. */
46
- dryRun: boolean;
47
- json: boolean;
48
- }
49
-
50
- export async function listWorktrees(options: ListWorktreesOptions): Promise<void> {
51
- const entries = await scanWorktrees();
52
- if (options.json) {
53
- console.log(JSON.stringify(entries, null, 2));
54
- return;
55
- }
56
- if (entries.length === 0) {
57
- console.log(chalk.dim(`No agent-managed worktrees found under ${getWorktreesDir()}.`));
58
- return;
59
- }
60
- let live = 0;
61
- let orphaned = 0;
62
- for (const entry of entries) {
63
- const tag = entry.orphanReason ? chalk.yellow("orphaned") : chalk.green("live ");
64
- const detail = formatEntryDetail(entry);
65
- console.log(`${tag} ${entry.path}`);
66
- if (detail) console.log(` ${chalk.dim(detail)}`);
67
- if (entry.orphanReason) orphaned += 1;
68
- else live += 1;
69
- }
70
- console.log(chalk.dim(`\n${live} live · ${orphaned} orphaned · ${entries.length} total`));
71
- }
72
-
73
- export async function clearWorktrees(options: ClearWorktreesOptions): Promise<void> {
74
- const entries = await scanWorktrees();
75
- const targets = options.all ? entries : entries.filter(entry => entry.orphanReason !== undefined);
76
-
77
- if (targets.length === 0) {
78
- if (options.json) {
79
- console.log(JSON.stringify({ removed: 0, kept: entries.length }));
80
- } else {
81
- console.log(chalk.dim(options.all ? "No worktrees to remove." : "No orphaned worktrees to remove."));
82
- }
83
- return;
84
- }
85
-
86
- if (options.dryRun) {
87
- if (options.json) {
88
- console.log(JSON.stringify({ wouldRemove: targets.map(t => t.path) }, null, 2));
89
- } else {
90
- for (const target of targets) {
91
- console.log(`${chalk.yellow("would remove")} ${target.path}`);
92
- }
93
- console.log(chalk.dim(`\n${targets.length} dir${targets.length === 1 ? "" : "s"} would be removed.`));
94
- }
95
- return;
96
- }
97
-
98
- const results: { path: string; ok: boolean; error?: string }[] = [];
99
- const parentsToPrune = new Set<string>();
100
- for (const target of targets) {
101
- try {
102
- if (target.kind === "pr-checkout" && target.parentRepo && !target.orphanReason) {
103
- // Live worktree: ask git to remove it cleanly. If git refuses (locked,
104
- // dirty, etc.), fall back to fs.rm and rely on `worktree prune` to
105
- // clean the bookkeeping on the parent side.
106
- const removed = await git.worktree.tryRemove(target.parentRepo, target.path, { force: true });
107
- if (!removed) {
108
- await fs.rm(target.path, { recursive: true, force: true });
109
- parentsToPrune.add(target.parentRepo);
110
- }
111
- } else {
112
- await fs.rm(target.path, { recursive: true, force: true });
113
- if (target.parentRepo) parentsToPrune.add(target.parentRepo);
114
- }
115
- results.push({ path: target.path, ok: true });
116
- } catch (err) {
117
- results.push({ path: target.path, ok: false, error: err instanceof Error ? err.message : String(err) });
118
- }
119
- }
120
-
121
- // Best-effort: drop stale entries from each affected parent's `.git/worktrees/`.
122
- for (const parent of parentsToPrune) {
123
- try {
124
- await git.worktree.prune(parent);
125
- } catch {
126
- /* parent repo may already be gone or pruned — ignore */
127
- }
128
- }
129
-
130
- const succeeded = results.filter(r => r.ok).length;
131
- const failed = results.length - succeeded;
132
-
133
- if (options.json) {
134
- console.log(JSON.stringify({ removed: succeeded, failed, results }, null, 2));
135
- if (failed > 0) process.exitCode = 1;
136
- return;
137
- }
138
-
139
- for (const result of results) {
140
- if (result.ok) {
141
- console.log(`${chalk.green("removed")} ${result.path}`);
142
- } else {
143
- console.log(`${chalk.red("failed ")} ${result.path}`);
144
- if (result.error) console.log(` ${chalk.dim(result.error)}`);
145
- }
146
- }
147
- console.log(chalk.dim(`\n${succeeded} removed${failed > 0 ? ` · ${chalk.red(`${failed} failed`)}` : ""}`));
148
- if (failed > 0) process.exitCode = 1;
149
- }
150
-
151
- // ───────────────────────────────────────────────────────────────────────────
152
- // Scanner
153
- // ───────────────────────────────────────────────────────────────────────────
154
-
155
- async function scanWorktrees(): Promise<WorktreeEntry[]> {
156
- const root = getWorktreesDir();
157
- let topLevel: string[];
158
- try {
159
- topLevel = await fs.readdir(root);
160
- } catch (err) {
161
- if (isEnoent(err)) return [];
162
- throw err;
163
- }
164
-
165
- const entries: WorktreeEntry[] = [];
166
- for (const name of topLevel) {
167
- const dir = path.join(root, name);
168
- const stat = await fs.stat(dir).catch(() => null);
169
- if (!stat?.isDirectory()) continue;
170
-
171
- const direct = await classifyDir(dir);
172
- if (direct) {
173
- entries.push(direct);
174
- continue;
175
- }
176
-
177
- // Legacy nesting: ~/.gjc/wt/<encoded-project>/<branch-or-id>
178
- let children: string[];
179
- try {
180
- children = await fs.readdir(dir);
181
- } catch {
182
- continue;
183
- }
184
- let nested = 0;
185
- for (const child of children) {
186
- const childDir = path.join(dir, child);
187
- const childStat = await fs.stat(childDir).catch(() => null);
188
- if (!childStat?.isDirectory()) continue;
189
- const childClassified = await classifyDir(childDir);
190
- if (childClassified) {
191
- entries.push(childClassified);
192
- nested += 1;
193
- }
194
- }
195
- if (nested === 0) {
196
- entries.push({
197
- path: dir,
198
- kind: children.length === 0 ? "empty" : "stray",
199
- orphanReason: children.length === 0 ? "empty directory" : "no recognizable worktree contents",
200
- });
201
- }
202
- }
203
- return entries;
204
- }
205
-
206
- async function classifyDir(dir: string): Promise<WorktreeEntry | null> {
207
- const gitEntry = path.join(dir, ".git");
208
- const gitStat = await fs.stat(gitEntry).catch(() => null);
209
- if (gitStat?.isFile()) {
210
- return classifyPrCheckout(dir, gitEntry);
211
- }
212
- const mergedStat = await fs.stat(path.join(dir, "merged")).catch(() => null);
213
- if (mergedStat?.isDirectory()) {
214
- return {
215
- path: dir,
216
- kind: "task-isolation",
217
- orphanReason: "task-isolation leftover (no live task owns it)",
218
- };
219
- }
220
- return null;
221
- }
222
-
223
- async function classifyPrCheckout(dir: string, gitEntry: string): Promise<WorktreeEntry> {
224
- let contents: string;
225
- try {
226
- contents = await fs.readFile(gitEntry, "utf8");
227
- } catch (err) {
228
- return {
229
- path: dir,
230
- kind: "pr-checkout",
231
- orphanReason: `cannot read .git file: ${err instanceof Error ? err.message : String(err)}`,
232
- };
233
- }
234
- const match = /^gitdir:\s*(.+?)\s*$/m.exec(contents);
235
- const parentGitDir = match?.[1];
236
- if (!parentGitDir) {
237
- return { path: dir, kind: "pr-checkout", orphanReason: "malformed .git file (no gitdir line)" };
238
- }
239
- // parentGitDir is `<parent-repo>/.git/worktrees/<name>`; back out the repo root.
240
- const parentRepo = path.dirname(path.dirname(path.dirname(parentGitDir)));
241
- const branch = await readWorktreeBranch(path.join(parentGitDir, "HEAD"));
242
-
243
- const parentDirStat = await fs.stat(parentGitDir).catch(() => null);
244
- if (!parentDirStat?.isDirectory()) {
245
- return {
246
- path: dir,
247
- kind: "pr-checkout",
248
- parentRepo,
249
- branch,
250
- orphanReason: "parent repo no longer tracks this worktree",
251
- };
252
- }
253
- const parentRepoStat = await fs.stat(parentRepo).catch(() => null);
254
- if (!parentRepoStat?.isDirectory()) {
255
- return {
256
- path: dir,
257
- kind: "pr-checkout",
258
- parentRepo,
259
- branch,
260
- orphanReason: "parent repo missing",
261
- };
262
- }
263
- return { path: dir, kind: "pr-checkout", parentRepo, branch };
264
- }
265
-
266
- async function readWorktreeBranch(headFile: string): Promise<string | undefined> {
267
- try {
268
- const head = (await fs.readFile(headFile, "utf8")).trim();
269
- const refMatch = /^ref:\s*refs\/heads\/(.+)$/.exec(head);
270
- return refMatch?.[1];
271
- } catch {
272
- return undefined;
273
- }
274
- }
9
+ export {};
275
10
 
276
- function formatEntryDetail(entry: WorktreeEntry): string {
277
- const parts: string[] = [];
278
- if (entry.kind === "pr-checkout") {
279
- const repo = entry.parentRepo ? path.basename(entry.parentRepo) : "unknown repo";
280
- const branch = entry.branch ?? "unknown branch";
281
- parts.push(`${repo} · ${branch}`);
282
- } else if (entry.kind === "task-isolation") {
283
- parts.push("task-isolation sandbox");
284
- } else if (entry.kind === "empty") {
285
- parts.push("legacy project shell");
286
- } else {
287
- parts.push("unrecognized contents");
288
- }
289
- if (entry.orphanReason) parts.push(entry.orphanReason);
290
- return parts.join(" — ");
291
- }
11
+ throw new Error(
12
+ "@gajae-code/coding-agent/cli/worktree-cli was deliberately removed: the legacy worktree cleanup API (listWorktrees/clearWorktrees) is gone. Inspect leftover managed worktrees under ~/.gjc/wt manually and use `git worktree remove` or `git worktree prune` instead.",
13
+ );
package/src/cli.ts CHANGED
@@ -9,6 +9,7 @@ import { Args, type CliConfig, Command, type CommandEntry, Flags, run } from "@g
9
9
  import { APP_NAME, formatBunRuntimeError, MIN_BUN_VERSION, VERSION } from "@gajae-code/utils/dirs";
10
10
  import { runFixtureReport } from "./cli/fixture-report";
11
11
  import { isTmuxOwnerIsolationCliArgv, runTmuxOwnerIsolationCliFromStdin } from "./gjc-runtime/tmux-owner-isolation-cli";
12
+ import { smokeTestTabWorker } from "./tools/browser/tab-worker-smoke";
12
13
 
13
14
  if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {
14
15
  process.stderr.write(
@@ -252,16 +253,15 @@ function isSubcommand(first: string | undefined): boolean {
252
253
  }
253
254
 
254
255
  /**
255
- * Smoke-test entry. Spawns the stats sync worker, pings it, exits.
256
+ * Smoke-test entry. Spawns the stats sync worker and the browser tab worker, then verifies their protocol.
256
257
  *
257
- * Purpose: catch the silent worker-load regressions that hit compiled
258
- * binaries (issues #1011 and #1027). Neither `--version` nor
259
- * `stats --summary` actually spawns a Worker on a fresh install — the
260
- * sync path early-returns when no session files exist. This probe is the
261
- * minimal end-to-end test that proves `new Worker(...)` resolves and the
262
- * bundled worker module evaluates successfully. Wired into
263
- * `scripts/install-tests/run-ci.sh` so binary / source-link / tarball
264
- * installs all exercise it on every CI run.
258
+ * Purpose: catch silent compiled-worker load regressions (issues #1011,
259
+ * #1027, and #2598). Neither `--version` nor `stats --summary` spawns both
260
+ * worker entries on a fresh install. This probe proves each bundled worker
261
+ * module resolves and evaluates; the tab-worker probe also completes its
262
+ * bootstrap/closed protocol without launching a browser. Wired into
263
+ * `scripts/install-tests/run-ci.sh` so binary / source-link / tarball installs
264
+ * exercise it on every CI run.
265
265
  */
266
266
  async function runSmokeTest(): Promise<void> {
267
267
  const { smokeTestSyncWorker } = await import("@gajae-code/stats");
@@ -278,6 +278,7 @@ async function runSmokeTest(): Promise<void> {
278
278
  if (typeof h02ScoreSequenceFuzzy !== "function" || typeof h01FindBestFuzzyMatch !== "function") {
279
279
  throw new Error("smoke-test: native fuzzy exports missing from embedded addon");
280
280
  }
281
+ await smokeTestTabWorker();
281
282
  process.stdout.write("smoke-test: ok\n");
282
283
  }
283
284
 
@@ -9,9 +9,19 @@ export default class Gc extends Command {
9
9
  prune: Flags.boolean({ description: "Remove stale records (default: report only)", default: false }),
10
10
  force: Flags.boolean({ description: "Alias for --prune (eligible records only)", default: false }),
11
11
  "dry-run": Flags.boolean({ description: "Force report-only mode", default: false }),
12
+ "repair-session-index": Flags.boolean({
13
+ description: "Quarantine a corrupt session-index suffix and retain its valid prefix",
14
+ default: false,
15
+ }),
12
16
  };
13
17
 
14
- static examples = ["gjc gc", "gjc gc --json", "gjc gc --prune", "gjc gc --prune --json"];
18
+ static examples = [
19
+ "gjc gc",
20
+ "gjc gc --json",
21
+ "gjc gc --prune",
22
+ "gjc gc --prune --json",
23
+ "gjc gc --repair-session-index --json",
24
+ ];
15
25
 
16
26
  async run(): Promise<void> {
17
27
  const result = await runGjcGcCommand(this.argv, process.cwd(), process.env);
@@ -7,6 +7,7 @@ import { Settings } from "../config/settings";
7
7
  import { applyStartupModelProfiles, createSessionManager } from "../main";
8
8
  import { initializeExtensions } from "../modes/runtime-init";
9
9
  import { Broker } from "../sdk/broker/broker";
10
+ import { completeBrokerProcess } from "../sdk/broker/internal";
10
11
  import {
11
12
  type LifecycleTranscriptEvidence,
12
13
  readSessionLifecycleLaunchRequest,
@@ -166,7 +167,7 @@ export async function openLifecycleSessionManager(
166
167
  if (request.operation === "session.resume") {
167
168
  const opened = await SessionManager.openExistingStrict(
168
169
  snapshot.identity,
169
- parsed.sessionDir,
170
+ SessionManager.managedDestination(cwd, agentDir),
170
171
  undefined,
171
172
  migrationPolicy,
172
173
  );
@@ -180,7 +181,12 @@ export async function openLifecycleSessionManager(
180
181
  throw error;
181
182
  }
182
183
  } else {
183
- const forked = await SessionManager.forkFromCaptured(snapshot, cwd, parsed.sessionDir, migrationPolicy);
184
+ const forked = await SessionManager.forkFromCaptured(
185
+ snapshot,
186
+ cwd,
187
+ SessionManager.managedDestination(cwd, agentDir),
188
+ migrationPolicy,
189
+ );
184
190
  if (forked.kind === "error")
185
191
  throw new Error("Lifecycle saved session authority changed while the session host forked it.");
186
192
  sessionManager = forked.manager;
@@ -439,9 +445,9 @@ export default class Sdk extends Command {
439
445
  });
440
446
  await broker.start();
441
447
  if (!broker.ownsDiscovery) return;
442
- const stop = () => void broker.stop().finally(() => process.exit(0));
448
+ const stop = () => void broker.stop();
443
449
  process.once("SIGTERM", stop);
444
450
  process.once("SIGINT", stop);
445
- await new Promise<void>(() => {});
451
+ await completeBrokerProcess(broker);
446
452
  }
447
453
  }
@@ -1,56 +1,13 @@
1
1
  /**
2
- * List and clean up agent-managed git worktrees under `~/.gjc/wt`.
2
+ * Tombstone for the removed legacy `worktree` CLI command module.
3
+ *
4
+ * The `gjc worktree`/`wt` command was unregistered during the
5
+ * workflow-surface narrowing and its implementation was deliberately
6
+ * removed. This module exists only to fail imports with actionable
7
+ * migration guidance.
3
8
  */
4
- import { Args, Command, Flags } from "@gajae-code/utils/cli";
5
- import { clearWorktrees, listWorktrees } from "../cli/worktree-cli";
9
+ export {};
6
10
 
7
- export default class Worktree extends Command {
8
- static description = "List or clear agent-managed git worktrees (~/.gjc/wt)";
9
-
10
- static aliases = ["wt"];
11
-
12
- static args = {
13
- // `list` (default) inspects the worktree dir; `clear` removes entries.
14
- // A positional action keeps `gjc worktree` (the no-arg form) useful.
15
- action: Args.string({
16
- description: "list (default) or clear",
17
- required: false,
18
- options: ["list", "clear"],
19
- default: "list",
20
- }),
21
- };
22
-
23
- static flags = {
24
- all: Flags.boolean({
25
- description: "Clear every entry, including live PR-checkout worktrees (clear)",
26
- default: false,
27
- }),
28
- "dry-run": Flags.boolean({
29
- char: "n",
30
- description: "Print what would be removed without touching the filesystem (clear)",
31
- default: false,
32
- }),
33
- json: Flags.boolean({ char: "j", description: "Emit machine-readable JSON", default: false }),
34
- };
35
-
36
- static examples = [
37
- "gjc worktree",
38
- "gjc worktree list --json",
39
- "gjc worktree clear",
40
- "gjc worktree clear --dry-run",
41
- "gjc worktree clear --all",
42
- ];
43
-
44
- async run(): Promise<void> {
45
- const { args, flags } = await this.parse(Worktree);
46
- if (args.action === "clear") {
47
- await clearWorktrees({
48
- all: flags.all ?? false,
49
- dryRun: flags["dry-run"] ?? false,
50
- json: flags.json ?? false,
51
- });
52
- return;
53
- }
54
- await listWorktrees({ json: flags.json ?? false });
55
- }
56
- }
11
+ throw new Error(
12
+ "@gajae-code/coding-agent/commands/worktree was deliberately removed: the `gjc worktree` command and its cleanup implementation are gone. Inspect leftover managed worktrees under ~/.gjc/wt manually and use `git worktree remove` or `git worktree prune` instead.",
13
+ );