@gajae-code/coding-agent 0.15.6 → 0.16.1

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 (270) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/types/async/job-manager.d.ts +32 -1
  3. package/dist/types/browser-backend.d.ts +12 -0
  4. package/dist/types/cli/args.d.ts +2 -0
  5. package/dist/types/cli/root-flags.d.ts +14 -0
  6. package/dist/types/cli/update-cli.d.ts +14 -0
  7. package/dist/types/cli.d.ts +7 -0
  8. package/dist/types/commands/launch.d.ts +10 -0
  9. package/dist/types/commands/sdk.d.ts +2 -0
  10. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  11. package/dist/types/config/billing-path.d.ts +25 -0
  12. package/dist/types/config/config-schema-version.d.ts +13 -1
  13. package/dist/types/config/file-lock.d.ts +5 -0
  14. package/dist/types/config/model-registry.d.ts +22 -0
  15. package/dist/types/config/settings-schema.d.ts +27 -8
  16. package/dist/types/config/settings.d.ts +2 -0
  17. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  18. package/dist/types/crash/record-loader.d.ts +15 -0
  19. package/dist/types/extensibility/extensions/types.d.ts +8 -3
  20. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  21. package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
  22. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  23. package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
  24. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  26. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  27. package/dist/types/main.d.ts +1 -0
  28. package/dist/types/master-mode/context.d.ts +21 -0
  29. package/dist/types/master-mode/first-request.d.ts +34 -0
  30. package/dist/types/master-mode/snapshot.d.ts +32 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +21 -21
  32. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  33. package/dist/types/modes/components/model-selector.d.ts +4 -0
  34. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  35. package/dist/types/modes/components/session-selector.d.ts +1 -1
  36. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  37. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  38. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  39. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  40. package/dist/types/modes/interactive-mode.d.ts +16 -2
  41. package/dist/types/modes/print-mode.d.ts +17 -0
  42. package/dist/types/modes/theme/theme.d.ts +2 -1
  43. package/dist/types/modes/types.d.ts +2 -0
  44. package/dist/types/modes/ui-language.d.ts +7 -1
  45. package/dist/types/sdk/broker/broker.d.ts +79 -2
  46. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  47. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  48. package/dist/types/sdk/broker/index.d.ts +3 -0
  49. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
  50. package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
  51. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  52. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  53. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  54. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  55. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  56. package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
  57. package/dist/types/sdk/broker/state-version.d.ts +21 -1
  58. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  59. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  60. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
  61. package/dist/types/sdk/bus/index.d.ts +9 -1
  62. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
  63. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
  64. package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
  65. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  67. package/dist/types/sdk/cli/index.d.ts +1 -0
  68. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  69. package/dist/types/sdk/cli/rows.d.ts +12 -5
  70. package/dist/types/sdk/cli/session-cli.d.ts +20 -3
  71. package/dist/types/sdk/client/client.d.ts +3 -2
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/events.d.ts +1 -0
  77. package/dist/types/sdk/host/host.d.ts +10 -0
  78. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  79. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  80. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  81. package/dist/types/sdk/host/session-runtime.d.ts +70 -17
  82. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  83. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  84. package/dist/types/sdk/lifecycle-session.d.ts +3 -1
  85. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  86. package/dist/types/sdk/receipt-state.d.ts +2 -0
  87. package/dist/types/sdk/router/session-router.d.ts +6 -0
  88. package/dist/types/sdk/session.d.ts +16 -0
  89. package/dist/types/sdk/startup-capability.d.ts +2 -1
  90. package/dist/types/sdk/turn-result.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +52 -7
  92. package/dist/types/session/credential-store-errors.d.ts +1 -0
  93. package/dist/types/session/fold-coordinator.d.ts +29 -2
  94. package/dist/types/session/session-manager.d.ts +79 -2
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/tools/ask.d.ts +1 -0
  98. package/dist/types/tools/bash.d.ts +16 -0
  99. package/dist/types/tools/browser/launch.d.ts +10 -0
  100. package/dist/types/tools/browser/registry.d.ts +8 -0
  101. package/dist/types/tools/gh.d.ts +8 -0
  102. package/dist/types/tools/index.d.ts +10 -10
  103. package/dist/types/tools/job.d.ts +3 -1
  104. package/dist/types/utils/external-editor.d.ts +7 -0
  105. package/examples/extensions/README.md +1 -1
  106. package/examples/extensions/pirate.ts +7 -4
  107. package/package.json +7 -7
  108. package/scripts/generate-sdk-operation-inventory.ts +13 -1
  109. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  110. package/src/async/job-manager.ts +107 -9
  111. package/src/browser-backend.ts +29 -0
  112. package/src/cli/accounts-cli.ts +9 -5
  113. package/src/cli/args.ts +17 -0
  114. package/src/cli/fast-help.ts +1 -0
  115. package/src/cli/plugin-cli.ts +29 -0
  116. package/src/cli/root-flags.ts +5 -0
  117. package/src/cli/update-cli.ts +140 -23
  118. package/src/commands/launch.ts +86 -18
  119. package/src/commands/sdk.ts +135 -28
  120. package/src/config/autorouting-tier-map.ts +12 -0
  121. package/src/config/billing-path.ts +42 -0
  122. package/src/config/config-schema-version.ts +18 -1
  123. package/src/config/file-lock.ts +158 -83
  124. package/src/config/model-preset-registry.ts +178 -23
  125. package/src/config/model-profiles.ts +5 -5
  126. package/src/config/model-registry.ts +291 -42
  127. package/src/config/model-resolver.ts +5 -0
  128. package/src/config/settings-schema.ts +27 -8
  129. package/src/config/settings.ts +51 -15
  130. package/src/coordinator-mcp/projection-scan.ts +27 -6
  131. package/src/coordinator-mcp/question-state.ts +9 -11
  132. package/src/coordinator-mcp/server.ts +115 -21
  133. package/src/crash/index-store.ts +2 -0
  134. package/src/crash/record-loader.ts +10 -0
  135. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  136. package/src/extensibility/extensions/runner.ts +27 -0
  137. package/src/extensibility/extensions/types.ts +8 -3
  138. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  139. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  140. package/src/extensibility/skills.ts +22 -12
  141. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  142. package/src/gjc-runtime/launch-worktree.ts +285 -4
  143. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  144. package/src/gjc-runtime/session-state-lock.ts +213 -14
  145. package/src/gjc-runtime/session-state-sidecar.ts +938 -39
  146. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  147. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
  148. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  149. package/src/hooks/skill-state.ts +6 -0
  150. package/src/internal-urls/docs-index.generated.ts +15 -15
  151. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  152. package/src/main.ts +61 -3
  153. package/src/master-mode/context.ts +50 -0
  154. package/src/master-mode/first-request.ts +65 -0
  155. package/src/master-mode/snapshot.ts +205 -0
  156. package/src/modes/acp/acp-agent.ts +610 -161
  157. package/src/modes/components/footer.ts +4 -2
  158. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  159. package/src/modes/components/model-selector.ts +76 -8
  160. package/src/modes/components/oauth-selector.ts +6 -1
  161. package/src/modes/components/session-selector.ts +23 -15
  162. package/src/modes/components/sessions-dashboard.ts +8 -5
  163. package/src/modes/components/settings-selector.ts +1 -0
  164. package/src/modes/components/status-line/model-name.ts +53 -0
  165. package/src/modes/components/status-line/priority-row.ts +107 -0
  166. package/src/modes/components/status-line/segments.ts +23 -9
  167. package/src/modes/components/tool-status-header.ts +151 -15
  168. package/src/modes/controllers/command-controller.ts +1 -1
  169. package/src/modes/controllers/event-controller.ts +30 -4
  170. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  171. package/src/modes/controllers/input-controller.ts +30 -2
  172. package/src/modes/controllers/selector-controller.ts +200 -98
  173. package/src/modes/interactive-mode.ts +36 -12
  174. package/src/modes/print-mode.ts +64 -0
  175. package/src/modes/prompt-action-autocomplete.ts +30 -4
  176. package/src/modes/runtime-init.ts +12 -9
  177. package/src/modes/theme/theme.ts +5 -0
  178. package/src/modes/types.ts +2 -0
  179. package/src/modes/ui-language.ts +49 -2
  180. package/src/modes/utils/ui-helpers.ts +22 -3
  181. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  182. package/src/prompts/system/master-mode.md +28 -0
  183. package/src/prompts/tools/ask.md +1 -1
  184. package/src/prompts/tools/bash.md +1 -0
  185. package/src/prompts/tools/job.md +1 -1
  186. package/src/sdk/acp/adapter.ts +5 -0
  187. package/src/sdk/broker/broker.ts +1646 -27
  188. package/src/sdk/broker/endpoint-authority.ts +86 -0
  189. package/src/sdk/broker/ensure.ts +81 -3
  190. package/src/sdk/broker/index.ts +3 -0
  191. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  192. package/src/sdk/broker/lifecycle.ts +440 -75
  193. package/src/sdk/broker/master-capability.ts +232 -0
  194. package/src/sdk/broker/session-index.ts +121 -17
  195. package/src/sdk/broker/session-scope.ts +272 -0
  196. package/src/sdk/broker/spawn-authority.ts +1031 -0
  197. package/src/sdk/broker/spawn-substrate.ts +591 -0
  198. package/src/sdk/broker/startup-budget.ts +144 -2
  199. package/src/sdk/broker/state-version.ts +43 -14
  200. package/src/sdk/broker/transport.ts +1 -0
  201. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  202. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  203. package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
  204. package/src/sdk/bus/discord-daemon.ts +50 -10
  205. package/src/sdk/bus/index.ts +84 -7
  206. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  207. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  208. package/src/sdk/bus/notification-service.ts +22 -5
  209. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  210. package/src/sdk/bus/reconciliation-store.ts +55 -31
  211. package/src/sdk/bus/slack-conversation.ts +1 -1
  212. package/src/sdk/bus/slack-daemon.ts +72 -38
  213. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  214. package/src/sdk/cli/index.ts +1 -0
  215. package/src/sdk/cli/master-cli.ts +259 -0
  216. package/src/sdk/cli/rows.ts +41 -15
  217. package/src/sdk/cli/session-cli.ts +297 -26
  218. package/src/sdk/client/client.ts +14 -8
  219. package/src/sdk/host/control/dispatch.ts +101 -40
  220. package/src/sdk/host/control/index.ts +2 -0
  221. package/src/sdk/host/control/operations.ts +2 -0
  222. package/src/sdk/host/control/runtime-gate.ts +203 -11
  223. package/src/sdk/host/events.ts +1 -0
  224. package/src/sdk/host/host.ts +29 -2
  225. package/src/sdk/host/query/cursor.ts +52 -7
  226. package/src/sdk/host/query/handlers.ts +33 -12
  227. package/src/sdk/host/query/revision-store.ts +12 -0
  228. package/src/sdk/host/session-runtime.test.ts +673 -14
  229. package/src/sdk/host/session-runtime.ts +674 -103
  230. package/src/sdk/host/surface-policy.ts +3 -3
  231. package/src/sdk/lifecycle/broker-client.ts +28 -0
  232. package/src/sdk/lifecycle/service.ts +279 -20
  233. package/src/sdk/lifecycle-session.ts +17 -4
  234. package/src/sdk/prompt-deadline-manager.ts +2 -1
  235. package/src/sdk/protocol/operation-inventory.generated.json +105 -3
  236. package/src/sdk/protocol/operation-registry.ts +14 -4
  237. package/src/sdk/receipt-state.ts +10 -0
  238. package/src/sdk/router/session-router.ts +219 -118
  239. package/src/sdk/session.ts +416 -195
  240. package/src/sdk/startup-capability.ts +1 -0
  241. package/src/sdk/turn-result.ts +22 -0
  242. package/src/session/agent-session.ts +1628 -318
  243. package/src/session/blob-store.ts +15 -5
  244. package/src/session/credential-store-errors.ts +2 -0
  245. package/src/session/fold-coordinator.ts +40 -3
  246. package/src/session/internal/managed-session-storage.ts +153 -78
  247. package/src/session/session-manager.ts +850 -97
  248. package/src/session/yield-queue.ts +24 -10
  249. package/src/setup/frictionless-onboarding.ts +2 -0
  250. package/src/slash-commands/builtin-registry.ts +41 -0
  251. package/src/task/executor.ts +9 -4
  252. package/src/telemetry/events.ts +1138 -12
  253. package/src/tools/ask-contract.test.ts +64 -0
  254. package/src/tools/ask-contract.ts +36 -1
  255. package/src/tools/ask.ts +10 -8
  256. package/src/tools/bash-interactive.ts +11 -2
  257. package/src/tools/bash.ts +412 -75
  258. package/src/tools/browser/launch.ts +37 -0
  259. package/src/tools/browser/registry.ts +99 -9
  260. package/src/tools/cron.ts +6 -4
  261. package/src/tools/descriptors.test.ts +15 -0
  262. package/src/tools/descriptors.ts +5 -1
  263. package/src/tools/gh.ts +104 -48
  264. package/src/tools/github-cache.ts +2 -2
  265. package/src/tools/index.ts +10 -6
  266. package/src/tools/job.ts +4 -1
  267. package/src/tools/monitor.ts +12 -9
  268. package/src/tools/resolve.ts +30 -10
  269. package/src/tools/tool-catalog.generated.ts +2 -2
  270. package/src/utils/external-editor.ts +72 -1
@@ -375,8 +375,10 @@ function buildSingleResource({
375
375
 
376
376
  function formatFileLine(idx: number, file: PrDiffFile, repo: string, prNumber: number): string {
377
377
  const stats = file.changeType === "binary" ? "(binary)" : `+${file.additions} -${file.deletions}`;
378
- const rename = file.oldPath ? ` (renamed from ${file.oldPath})` : "";
379
- return `${idx}. ${file.path} ${stats} [${file.changeType}]${rename}\n pr://${repo}/${prNumber}/diff/${idx}`;
378
+ const escapedNote = file.pathEscaped ? " (escaped non-UTF-8 name)" : "";
379
+ const renameNote = file.oldPathEscaped ? " (escaped non-UTF-8 name)" : "";
380
+ const rename = file.oldPath ? ` (renamed from ${file.oldPath}${renameNote})` : "";
381
+ return `${idx}. ${file.path}${escapedNote} ${stats} [${file.changeType}]${rename}\n pr://${repo}/${prNumber}/diff/${idx}`;
380
382
  }
381
383
 
382
384
  async function fetchAndRenderPrDiff(
package/src/main.ts CHANGED
@@ -39,7 +39,9 @@ import { BUNDLED_GROK_BUILD_EXTENSION_ID, getBundledGrokBuildExtensionFactory }
39
39
  import { initializeWithSettings } from "./discovery";
40
40
  import { exportFromFile } from "./export/html";
41
41
  import type { ExtensionUIContext } from "./extensibility/extensions/types";
42
+ import { releaseLaunchWorktreeReservationAfterRegistration } from "./gjc-runtime/launch-worktree-reservation";
42
43
  import { persistCoordinatorRuntimeInputReady } from "./gjc-runtime/session-state-sidecar";
44
+ import { assertMasterLaunchArgs, assertMasterLaunchDisposition, createMasterModeContext } from "./master-mode/context";
43
45
  import type { AcpStartupOptions } from "./modes/acp/startup-options";
44
46
  import type { SessionSelectionResult } from "./modes/components/session-selector";
45
47
  import type { InteractiveMode } from "./modes/interactive-mode";
@@ -56,7 +58,7 @@ import {
56
58
  discoverAuthStorage,
57
59
  } from "./sdk";
58
60
  import { processIncarnation } from "./sdk/broker/process-incarnation";
59
- import { SessionIndex } from "./sdk/broker/session-index";
61
+ import { newMasterAttestationEpoch, resolveSessionLocator, SessionIndex } from "./sdk/broker/session-index";
60
62
  import type { AgentSession } from "./session/agent-session";
61
63
  import { SessionMigrationBusyError } from "./session/internal/session-open-errors";
62
64
  import {
@@ -451,6 +453,7 @@ type StartupModelProfileArgs = {
451
453
  startupModel?: CreateAgentSessionOptions["model"];
452
454
  startupThinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
453
455
  preferCachedModels?: boolean;
456
+ preferCachedDefaultProfile?: boolean;
454
457
  };
455
458
 
456
459
  async function applyStartupModelProfilesWithPolicy(
@@ -482,7 +485,9 @@ async function applyStartupModelProfilesWithPolicy(
482
485
  // deferred `--model <pattern>` path resolved inside createAgentSession.
483
486
  const explicitModel = args.parsedArgs.model ? (args.startupModel ?? args.session.model) : undefined;
484
487
  const defaultProfile = args.settings.get("modelProfile.default");
485
- const preferCachedProfiles = args.preferCachedModels === true && args.parsedArgs.mpreset !== undefined;
488
+ const preferCachedProfiles =
489
+ (args.preferCachedModels === true && args.parsedArgs.mpreset !== undefined) ||
490
+ (args.preferCachedDefaultProfile === true && defaultProfile !== undefined);
486
491
  const applyConfiguredProfiles = async (): Promise<boolean> => {
487
492
  let applied = true;
488
493
  if (defaultProfile) {
@@ -752,6 +757,9 @@ export async function runInteractiveMode(
752
757
  if (
753
758
  shouldOfferAutomaticOnboarding({
754
759
  normalInteractive: true,
760
+ automation:
761
+ /^(?:1|true|yes|on)$/i.test(process.env.CI ?? "") ||
762
+ /^(?:1|true|yes|on)$/i.test(process.env.GJC_AUTOMATION ?? ""),
755
763
  initialMessage,
756
764
  initialMessages,
757
765
  initialImages,
@@ -1366,6 +1374,7 @@ export async function runRootCommand(
1366
1374
  deps: RunRootCommandDependencies = {},
1367
1375
  ): Promise<void> {
1368
1376
  const parsedArgs = parsed;
1377
+ assertMasterLaunchArgs(parsedArgs);
1369
1378
  let initialThemeInitialized = false;
1370
1379
  let autoChdirApplied = false;
1371
1380
  let bareResumeSessionManager: SessionManager | undefined;
@@ -1553,6 +1562,22 @@ export async function runRootCommand(
1553
1562
  print: Boolean(parsedArgs.print),
1554
1563
  mode: parsedArgs.mode,
1555
1564
  });
1565
+ // Master admission runs unconditionally and BEFORE any session state is
1566
+ // opened or created. Nesting it under nonInteractiveError skipped it for a
1567
+ // non-TTY launch carrying prepared input, which resolves to autoPrint with no
1568
+ // error, so `--master <prompt>` proceeded as an ordinary auto-print run.
1569
+ try {
1570
+ assertMasterLaunchDisposition({
1571
+ master: parsedArgs.master,
1572
+ isInteractive: disposition.isInteractive,
1573
+ autoPrint: disposition.autoPrint,
1574
+ nonInteractiveError: disposition.nonInteractiveError,
1575
+ });
1576
+ } catch (error) {
1577
+ process.stderr.write(`${chalk.red(error instanceof Error ? error.message : "Invalid master launch")}\n`);
1578
+ if (!deps.suppressProcessExit) process.exitCode = 1;
1579
+ return;
1580
+ }
1556
1581
  if (disposition.nonInteractiveError) {
1557
1582
  process.stderr.write(`${chalk.red(disposition.nonInteractiveError)}\n`);
1558
1583
  process.exit(1);
@@ -1676,6 +1701,14 @@ export async function runRootCommand(
1676
1701
  }
1677
1702
  }
1678
1703
  }
1704
+ const masterModeContext =
1705
+ parsedArgs.master === true && sessionManager
1706
+ ? createMasterModeContext(
1707
+ parsedArgs.masterScope ?? "repo",
1708
+ sessionManager.getSessionId(),
1709
+ newMasterAttestationEpoch(),
1710
+ )
1711
+ : undefined;
1679
1712
 
1680
1713
  const { options: sessionOptions } = await logger.time(
1681
1714
  "buildSessionOptions",
@@ -1742,6 +1775,7 @@ export async function runRootCommand(
1742
1775
  sessionOptions.notificationHostModeSupported = isInteractive;
1743
1776
  sessionOptions.sdkHostModeSupported = isInteractive;
1744
1777
  sessionOptions.settings = settingsInstance;
1778
+ sessionOptions.masterModeContext = masterModeContext;
1745
1779
  if (isInteractive && sessionOptions.mcpConfigPath) {
1746
1780
  sessionOptions.deferMcpConfigStartup = true;
1747
1781
  }
@@ -1826,7 +1860,7 @@ export async function runRootCommand(
1826
1860
  const directSessionId = process.env.GJC_LIFECYCLE_REQUEST_ID ? undefined : sessionManager?.getSessionId();
1827
1861
  if (directSessionId) {
1828
1862
  const sessionIndex = new SessionIndex(settingsInstance.getAgentDir());
1829
- const locator = { repo: sessionManager?.getCwd() ?? cwd, stateRoot: settingsInstance.getAgentDir() };
1863
+ const locator = await resolveSessionLocator(sessionManager?.getCwd() ?? cwd, settingsInstance.getAgentDir());
1830
1864
  // A pid is reusable, so the broker's teardown fence needs this session's OS
1831
1865
  // start incarnation recorded alongside the pid it publishes.
1832
1866
  const directSessionIncarnation = processIncarnation(process.pid);
@@ -1837,7 +1871,31 @@ export async function runRootCommand(
1837
1871
  endpointGeneration: 0,
1838
1872
  pid: process.pid,
1839
1873
  ...(directSessionIncarnation ? { processIncarnation: directSessionIncarnation } : {}),
1874
+ ...(masterModeContext && directSessionIncarnation
1875
+ ? {
1876
+ masterRole: {
1877
+ version: 2,
1878
+ ownerSessionId: masterModeContext.ownerSessionId,
1879
+ launchPid: process.pid,
1880
+ launchProcessIncarnation: directSessionIncarnation,
1881
+ role: "master" as const,
1882
+ attestationEpoch: masterModeContext.attestationEpoch,
1883
+ },
1884
+ }
1885
+ : {}),
1840
1886
  });
1887
+ if (locator.worktreeRoot !== null) {
1888
+ try {
1889
+ await releaseLaunchWorktreeReservationAfterRegistration(
1890
+ settingsInstance.getAgentDir(),
1891
+ locator.worktreeRoot,
1892
+ );
1893
+ } catch (error) {
1894
+ logger.warn("Failed to release launch worktree reservation after host registration", {
1895
+ error: error instanceof Error ? error.message : String(error),
1896
+ });
1897
+ }
1898
+ }
1841
1899
  postmortem.register("direct-session-index", async () => {
1842
1900
  await sessionIndex.append({
1843
1901
  type: "host_unregistered",
@@ -0,0 +1,50 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import type { Args } from "../cli/args";
3
+
4
+ export type MasterScope = "repo" | "pwd" | "global";
5
+
6
+ /**
7
+ * Process-local master authority. Its capability accessor is deliberately the
8
+ * only way to reach the raw capability; this context is never serializable.
9
+ */
10
+ export interface MasterModeContext {
11
+ readonly scope: MasterScope;
12
+ readonly ownerSessionId: string;
13
+ readonly attestationEpoch: string;
14
+ getCapability(): string;
15
+ }
16
+
17
+ export function createMasterModeContext(
18
+ scope: MasterScope,
19
+ ownerSessionId: string,
20
+ attestationEpoch: string,
21
+ ): MasterModeContext {
22
+ const capability = randomBytes(32).toString("base64url");
23
+ return { scope, ownerSessionId, attestationEpoch, getCapability: () => capability };
24
+ }
25
+
26
+ /** Reject routes that cannot establish an interactive, durable master session. */
27
+ export function assertMasterLaunchArgs(parsed: Args): void {
28
+ if (parsed.master !== true) return;
29
+ if (
30
+ parsed.print === true ||
31
+ parsed.mode !== undefined ||
32
+ parsed.export !== undefined ||
33
+ parsed.listModels !== undefined ||
34
+ parsed.noSession === true
35
+ ) {
36
+ throw new Error("--master requires an interactive durable TUI launch");
37
+ }
38
+ }
39
+
40
+ export function assertMasterLaunchDisposition(input: {
41
+ master: boolean | undefined;
42
+ isInteractive: boolean;
43
+ autoPrint: boolean;
44
+ nonInteractiveError?: string;
45
+ }): void {
46
+ if (input.master !== true) return;
47
+ if (!input.isInteractive || input.autoPrint || input.nonInteractiveError !== undefined) {
48
+ throw new Error("--master requires an interactive TTY launch");
49
+ }
50
+ }
@@ -0,0 +1,65 @@
1
+ import { canonicalSessionCwd, sessionWorktreeRoot } from "../sdk/broker/session-index";
2
+ import type { MasterScope } from "./context";
3
+ import { collectMasterPeerSnapshot, type MasterPeerSnapshotLifecycle, renderMasterPeerSnapshot } from "./snapshot";
4
+
5
+ export const MASTER_PEER_SNAPSHOT_CUSTOM_TYPE = "master-peer-snapshot";
6
+
7
+ /** The injected message shape consumed by the agent-session contributor seam. */
8
+ export interface MasterPeerSnapshotMessage {
9
+ customType: string;
10
+ content: string;
11
+ display: false;
12
+ attribution: "agent";
13
+ }
14
+
15
+ export interface MasterPeerSnapshotContributorInput {
16
+ readonly lifecycle: MasterPeerSnapshotLifecycle;
17
+ readonly ownerSessionId: string;
18
+ /** Current transcript/session identity; differs from ownerSessionId after a branch or switch. */
19
+ readonly getSessionId?: () => string;
20
+ readonly scope: MasterScope;
21
+ /** Live cwd accessor; resolved at injection time, not at registration. */
22
+ readonly getCwd: () => string;
23
+ /**
24
+ * Whether a prior injection was persisted to the session branch. A persisted
25
+ * block proves a first request was accepted, so later turns never duplicate
26
+ * it; a pre-accept cancellation persists nothing and the next attempt
27
+ * re-collects a fresh snapshot.
28
+ */
29
+ readonly hasPersistedInjection: () => boolean;
30
+ readonly onError?: (error: unknown) => void;
31
+ }
32
+
33
+ /**
34
+ * Builds the one-shot before-agent-start contributor that injects the scoped,
35
+ * no-probe master peer snapshot immediately before the first accepted provider
36
+ * request. Idle masters never collect: the snapshot is gathered only when a
37
+ * prompt actually starts.
38
+ */
39
+ export function createMasterPeerSnapshotContributor(
40
+ input: MasterPeerSnapshotContributorInput,
41
+ ): () => Promise<MasterPeerSnapshotMessage | undefined> {
42
+ return async () => {
43
+ try {
44
+ if (input.hasPersistedInjection()) return undefined;
45
+ const anchorCwd = await canonicalSessionCwd(input.getCwd());
46
+ const snapshot = await collectMasterPeerSnapshot({
47
+ lifecycle: input.lifecycle,
48
+ actor: { id: input.ownerSessionId, namespace: "sdk:master-snapshot" },
49
+ ownerSessionId: input.ownerSessionId,
50
+ ...(input.getSessionId === undefined ? {} : { currentSessionId: input.getSessionId() }),
51
+ scope: input.scope,
52
+ requestAnchor: { cwd: anchorCwd, worktreeRoot: await sessionWorktreeRoot(anchorCwd) },
53
+ });
54
+ return {
55
+ customType: MASTER_PEER_SNAPSHOT_CUSTOM_TYPE,
56
+ content: renderMasterPeerSnapshot(snapshot),
57
+ display: false,
58
+ attribution: "agent",
59
+ };
60
+ } catch (error) {
61
+ input.onError?.(error);
62
+ return undefined;
63
+ }
64
+ };
65
+ }
@@ -0,0 +1,205 @@
1
+ import {
2
+ type ResolvedScopeV1,
3
+ resolveScopeRequest,
4
+ type ScopeNameV1,
5
+ type ScopeRequestV1,
6
+ type SdkSearchResultV1,
7
+ type SdkSearchRowV1,
8
+ sdkSearchResultV1,
9
+ } from "../sdk/broker/session-scope";
10
+ import type { SessionLifecycleActor, SessionListOutcome, SessionListRequest } from "../sdk/lifecycle/service";
11
+
12
+ export type MasterPeerSnapshot = {
13
+ readonly status: SdkSearchResultV1["status"];
14
+ readonly scope: ResolvedScopeV1;
15
+ readonly observedAt: string;
16
+ readonly indexSeq?: number;
17
+ readonly rows: readonly SdkSearchRowV1[];
18
+ readonly truncated?: boolean;
19
+ };
20
+
21
+ const MASTER_PEER_SNAPSHOT_MAX_ROWS = 256;
22
+ const MASTER_PEER_SNAPSHOT_MAX_BYTES = 64 * 1024;
23
+
24
+ /** The scoped Broker surface needed for the one-time master peer snapshot. */
25
+ export interface MasterPeerSnapshotLifecycle {
26
+ list(request: Omit<SessionListRequest, "operation">): Promise<SessionListOutcome>;
27
+ }
28
+
29
+ export interface CollectMasterPeerSnapshotInput {
30
+ readonly lifecycle: MasterPeerSnapshotLifecycle;
31
+ readonly actor: SessionLifecycleActor;
32
+ readonly ownerSessionId: string;
33
+ readonly currentSessionId?: string;
34
+ readonly scope: ScopeNameV1;
35
+ readonly requestAnchor: ScopeRequestV1["requestAnchor"];
36
+ readonly timeoutMs?: number;
37
+ }
38
+
39
+ function compareText(left: string, right: string): number {
40
+ return left < right ? -1 : left > right ? 1 : 0;
41
+ }
42
+
43
+ /**
44
+ * Orders by session ID, then the complete canonical locator as a deterministic
45
+ * tie-breaker. The Broker's `live` value is index evidence, not a fresh probe.
46
+ */
47
+ function compareRows(left: SdkSearchRowV1, right: SdkSearchRowV1): number {
48
+ const id = compareText(left.id, right.id);
49
+ if (id !== 0) return id;
50
+ const cwd = compareText(left.locator.cwd, right.locator.cwd);
51
+ if (cwd !== 0) return cwd;
52
+ const worktreeRoot = compareText(left.locator.worktreeRoot ?? "", right.locator.worktreeRoot ?? "");
53
+ if (worktreeRoot !== 0) return worktreeRoot;
54
+ return compareText(left.locator.stateRoot, right.locator.stateRoot);
55
+ }
56
+
57
+ function unavailableSnapshot(scope: ResolvedScopeV1): MasterPeerSnapshot {
58
+ return {
59
+ status: "unavailable",
60
+ scope,
61
+ observedAt: new Date().toISOString(),
62
+ rows: [],
63
+ };
64
+ }
65
+
66
+ function matchesResolvedScope(left: ResolvedScopeV1, right: ResolvedScopeV1): boolean {
67
+ return JSON.stringify(left) === JSON.stringify(right);
68
+ }
69
+
70
+ /** Retains a row prefix only when its final escaped prompt block fits the budget. */
71
+ function boundSnapshotRows(
72
+ snapshot: Omit<MasterPeerSnapshot, "rows" | "truncated">,
73
+ rows: readonly SdkSearchRowV1[],
74
+ ): {
75
+ rows: readonly SdkSearchRowV1[];
76
+ truncated: boolean;
77
+ } {
78
+ const retained: SdkSearchRowV1[] = [];
79
+ for (const [index, row] of rows.entries()) {
80
+ if (retained.length >= MASTER_PEER_SNAPSHOT_MAX_ROWS) return { rows: retained, truncated: true };
81
+
82
+ retained.push(row);
83
+ const willTruncate = index < rows.length - 1;
84
+ const candidate: MasterPeerSnapshot = {
85
+ ...snapshot,
86
+ rows: retained,
87
+ ...(willTruncate ? { truncated: true } : {}),
88
+ };
89
+ if (Buffer.byteLength(renderMasterPeerSnapshot(candidate), "utf8") > MASTER_PEER_SNAPSHOT_MAX_BYTES) {
90
+ retained.pop();
91
+ return { rows: retained, truncated: true };
92
+ }
93
+ if (willTruncate && retained.length === MASTER_PEER_SNAPSHOT_MAX_ROWS) return { rows: retained, truncated: true };
94
+ }
95
+ return { rows: retained, truncated: false };
96
+ }
97
+
98
+ /**
99
+ * Collects the first-request peer snapshot through scoped Broker session.list.
100
+ * It deliberately has no Router or endpoint-probe dependency.
101
+ */
102
+ export async function collectMasterPeerSnapshot(input: CollectMasterPeerSnapshotInput): Promise<MasterPeerSnapshot> {
103
+ const request: ScopeRequestV1 = {
104
+ version: 1,
105
+ requested: input.scope,
106
+ requestAnchor: input.requestAnchor,
107
+ };
108
+ const resolvedScope = await resolveScopeRequest(request);
109
+ const outcome = await input.lifecycle.list({
110
+ actor: input.actor,
111
+ capability: "session.list",
112
+ target: { scope: request },
113
+ ...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
114
+ });
115
+ const result = sdkSearchResultV1(outcome.result);
116
+ if (!result || !matchesResolvedScope(result.scope, resolvedScope)) return unavailableSnapshot(resolvedScope);
117
+ const snapshot: Omit<MasterPeerSnapshot, "rows" | "truncated"> = {
118
+ status: result.status,
119
+ scope: result.scope,
120
+ observedAt: result.observedAt,
121
+ ...(result.indexSeq === undefined ? {} : { indexSeq: result.indexSeq }),
122
+ };
123
+ const rows = result.rows
124
+ .filter(row => row.id !== (input.currentSessionId ?? input.ownerSessionId))
125
+ .sort(compareRows);
126
+ const bounded = boundSnapshotRows(snapshot, rows);
127
+ return {
128
+ ...snapshot,
129
+ rows: bounded.rows,
130
+ ...(bounded.truncated ? { truncated: true } : {}),
131
+ };
132
+ }
133
+
134
+ /** Escapes untrusted metadata before it is placed in fixed prompt framing. */
135
+ export function escapeMasterPeerSnapshotText(input: string): string {
136
+ let firstEscapable = -1;
137
+ for (let index = 0; index < input.length; index++) {
138
+ const code = input.charCodeAt(index);
139
+ if (code === 38 || code === 60 || code === 62 || code === 96) {
140
+ firstEscapable = index;
141
+ break;
142
+ }
143
+ }
144
+ if (firstEscapable === -1) return input;
145
+
146
+ let output = input.slice(0, firstEscapable);
147
+ for (let index = firstEscapable; index < input.length; index++) {
148
+ const character = input[index];
149
+ if (character === "&") output += "&amp;";
150
+ else if (character === "<") output += "&lt;";
151
+ else if (character === ">") output += "&gt;";
152
+ else if (character === "`") output += "&#96;";
153
+ else output += character;
154
+ }
155
+ return output;
156
+ }
157
+
158
+ function escapeOptionalText(input: string | null): string | null {
159
+ return input === null ? null : escapeMasterPeerSnapshotText(input);
160
+ }
161
+
162
+ function renderedScope(scope: ResolvedScopeV1): ResolvedScopeV1 {
163
+ const resolved =
164
+ scope.resolved === null
165
+ ? null
166
+ : scope.resolved.kind === "repo"
167
+ ? { kind: "repo" as const, worktreeRoot: escapeMasterPeerSnapshotText(scope.resolved.worktreeRoot) }
168
+ : scope.resolved.kind === "pwd"
169
+ ? { kind: "pwd" as const, cwd: escapeMasterPeerSnapshotText(scope.resolved.cwd) }
170
+ : { kind: "global" as const, visibility: scope.resolved.visibility };
171
+ return {
172
+ version: scope.version,
173
+ requested: escapeMasterPeerSnapshotText(scope.requested) as ScopeNameV1,
174
+ requestAnchor: {
175
+ cwd: escapeMasterPeerSnapshotText(scope.requestAnchor.cwd),
176
+ worktreeRoot: escapeOptionalText(scope.requestAnchor.worktreeRoot),
177
+ },
178
+ resolved,
179
+ resolution: escapeMasterPeerSnapshotText(scope.resolution) as ResolvedScopeV1["resolution"],
180
+ };
181
+ }
182
+
183
+ function renderedRows(rows: readonly SdkSearchRowV1[]): readonly SdkSearchRowV1[] {
184
+ return rows.map(row => ({
185
+ id: escapeMasterPeerSnapshotText(row.id),
186
+ locator: {
187
+ cwd: escapeMasterPeerSnapshotText(row.locator.cwd),
188
+ worktreeRoot: escapeOptionalText(row.locator.worktreeRoot),
189
+ stateRoot: escapeMasterPeerSnapshotText(row.locator.stateRoot),
190
+ },
191
+ live: row.live,
192
+ }));
193
+ }
194
+
195
+ /** Renders the fixed, escaped prompt block for one master peer observation. */
196
+ export function renderMasterPeerSnapshot(snapshot: MasterPeerSnapshot): string {
197
+ const content = {
198
+ scope: renderedScope(snapshot.scope),
199
+ observedAt: escapeMasterPeerSnapshotText(snapshot.observedAt),
200
+ ...(snapshot.indexSeq === undefined ? {} : { indexSeq: snapshot.indexSeq }),
201
+ ...(snapshot.truncated === undefined ? {} : { truncated: snapshot.truncated }),
202
+ rows: renderedRows(snapshot.rows),
203
+ };
204
+ return `<gjc-master-peer-snapshot>\nstatus: ${escapeMasterPeerSnapshotText(snapshot.status)}\n${JSON.stringify(content)}\n</gjc-master-peer-snapshot>`;
205
+ }