@gajae-code/coding-agent 0.12.11 → 0.12.13

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 (140) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/types/cli/marketplace-hint.d.ts +28 -0
  3. package/dist/types/cli/notify-cli.d.ts +42 -1
  4. package/dist/types/cli/update-cli.d.ts +9 -3
  5. package/dist/types/cli.d.ts +10 -0
  6. package/dist/types/commands/notify.d.ts +6 -0
  7. package/dist/types/config/file-lock-gc.d.ts +13 -1
  8. package/dist/types/config/file-lock.d.ts +3 -0
  9. package/dist/types/config/model-registry.d.ts +6 -0
  10. package/dist/types/config/models-config-schema.d.ts +99 -0
  11. package/dist/types/coordinator/contract.d.ts +1 -1
  12. package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
  13. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  14. package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
  15. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
  16. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  17. package/dist/types/gjc-runtime/deep-interview-state.d.ts +14 -0
  18. package/dist/types/gjc-runtime/gc-runtime.d.ts +11 -0
  19. package/dist/types/main.d.ts +6 -2
  20. package/dist/types/modes/acp/acp-agent.d.ts +24 -1
  21. package/dist/types/modes/components/session-selector.d.ts +1 -1
  22. package/dist/types/modes/utils/host-status.d.ts +23 -0
  23. package/dist/types/runtime-mcp/types.d.ts +2 -0
  24. package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
  25. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
  26. package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
  27. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
  28. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
  29. package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
  30. package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
  31. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
  32. package/dist/types/sdk/bus/html-format.d.ts +10 -0
  33. package/dist/types/sdk/bus/notification-service.d.ts +10 -0
  34. package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
  35. package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
  36. package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
  37. package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
  38. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +28 -7
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -17
  41. package/dist/types/sdk/bus/telegram-reference.d.ts +20 -1
  42. package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
  43. package/dist/types/sdk/host/host.d.ts +52 -0
  44. package/dist/types/sdk/lifecycle-session.d.ts +6 -0
  45. package/dist/types/sdk/session-activation.d.ts +41 -0
  46. package/dist/types/sdk/session.d.ts +18 -0
  47. package/dist/types/sdk/startup-capability.d.ts +8 -1
  48. package/dist/types/session/agent-session.d.ts +7 -2
  49. package/dist/types/session/artifacts.d.ts +2 -2
  50. package/dist/types/session/session-manager.d.ts +40 -6
  51. package/dist/types/tools/index.d.ts +15 -0
  52. package/dist/types/utils/npm-registry.d.ts +71 -0
  53. package/examples/extensions/README.md +38 -4
  54. package/examples/extensions/ooo-bridge.ts +15 -0
  55. package/package.json +8 -8
  56. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  57. package/src/async/job-manager.ts +9 -2
  58. package/src/cli/fast-help.ts +1 -0
  59. package/src/cli/marketplace-hint.ts +39 -0
  60. package/src/cli/notify-cli.ts +226 -2
  61. package/src/cli/plugin-cli.ts +10 -0
  62. package/src/cli/update-cli.ts +90 -23
  63. package/src/cli.ts +23 -0
  64. package/src/commands/notify.ts +27 -3
  65. package/src/commands/sdk.ts +1 -0
  66. package/src/config/file-lock-gc.ts +65 -26
  67. package/src/config/file-lock.ts +61 -16
  68. package/src/config/model-profiles.ts +46 -30
  69. package/src/config/model-registry.ts +17 -5
  70. package/src/config/models-config-schema.ts +9 -4
  71. package/src/coordinator/contract.ts +1 -0
  72. package/src/coordinator-mcp/question-state.ts +2 -0
  73. package/src/coordinator-mcp/server.ts +358 -7
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  75. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  76. package/src/extensibility/extensions/index.ts +1 -0
  77. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
  78. package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
  79. package/src/extensibility/extensions/runner.ts +15 -2
  80. package/src/extensibility/extensions/types.ts +2 -0
  81. package/src/gjc-runtime/deep-interview-recorder.ts +9 -5
  82. package/src/gjc-runtime/deep-interview-state.ts +27 -3
  83. package/src/gjc-runtime/gc-render.ts +8 -1
  84. package/src/gjc-runtime/gc-runtime.ts +15 -1
  85. package/src/gjc-runtime/launch-tmux.ts +53 -1
  86. package/src/internal-urls/docs-index.generated.ts +14 -13
  87. package/src/main.ts +214 -70
  88. package/src/modes/DESIGN.md +8 -5
  89. package/src/modes/acp/acp-agent.ts +123 -7
  90. package/src/modes/components/footer.ts +10 -18
  91. package/src/modes/components/model-selector.ts +52 -5
  92. package/src/modes/components/session-selector.ts +26 -3
  93. package/src/modes/components/status-line/gh.ts +23 -2
  94. package/src/modes/controllers/event-controller.ts +3 -0
  95. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  96. package/src/modes/controllers/input-controller.ts +11 -0
  97. package/src/modes/controllers/selector-controller.ts +8 -2
  98. package/src/modes/interactive-mode.ts +7 -1
  99. package/src/modes/print-mode.ts +10 -0
  100. package/src/modes/utils/host-status.ts +38 -0
  101. package/src/prompts/system/project-prompt.md +19 -0
  102. package/src/runtime-mcp/types.ts +2 -0
  103. package/src/sdk/broker/lifecycle.ts +60 -3
  104. package/src/sdk/bus/chat-daemon-cli.ts +42 -0
  105. package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
  106. package/src/sdk/bus/chat-daemon-control.ts +16 -3
  107. package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
  108. package/src/sdk/bus/control-plane-frames.ts +51 -0
  109. package/src/sdk/bus/conversation-store.ts +29 -1
  110. package/src/sdk/bus/existing-thread-readiness.ts +155 -0
  111. package/src/sdk/bus/html-format.ts +19 -3
  112. package/src/sdk/bus/index.ts +91 -12
  113. package/src/sdk/bus/notification-service.ts +28 -1
  114. package/src/sdk/bus/rich-render.ts +3 -2
  115. package/src/sdk/bus/slack-daemon.ts +99 -4
  116. package/src/sdk/bus/slack-live-provider.ts +32 -2
  117. package/src/sdk/bus/slack-provider.ts +11 -0
  118. package/src/sdk/bus/slack-thread-binding.ts +641 -0
  119. package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
  120. package/src/sdk/bus/telegram-daemon-contract.ts +28 -7
  121. package/src/sdk/bus/telegram-daemon.ts +2007 -509
  122. package/src/sdk/bus/telegram-reference.ts +114 -15
  123. package/src/sdk/bus/topic-registry.ts +674 -181
  124. package/src/sdk/host/host.ts +180 -1
  125. package/src/sdk/lifecycle-session.ts +8 -2
  126. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  127. package/src/sdk/session-activation.ts +127 -0
  128. package/src/sdk/session.ts +144 -29
  129. package/src/sdk/startup-capability.ts +10 -1
  130. package/src/session/agent-session.ts +220 -13
  131. package/src/session/artifacts.ts +56 -13
  132. package/src/session/internal/managed-session-storage.ts +228 -7
  133. package/src/session/session-manager.ts +225 -60
  134. package/src/setup/provider-presets.json +3 -3
  135. package/src/task/index.ts +92 -44
  136. package/src/thinking-metadata.ts +2 -1
  137. package/src/tools/ask.ts +10 -0
  138. package/src/tools/index.ts +15 -0
  139. package/src/tools/todo-write.ts +4 -4
  140. package/src/utils/npm-registry.ts +660 -0
@@ -18,6 +18,11 @@ import { type EnsureBrokerSettings, ensureBroker } from "../sdk/broker/ensure";
18
18
  import { UnsupportedStateVersionError } from "../sdk/broker/state-version";
19
19
  import { SdkClient, SdkClientError } from "../sdk/client/client";
20
20
  import { readSdkBrokerDiscovery } from "../sdk/client/discovery";
21
+ import {
22
+ type ActivatedPreparedSession,
23
+ requestPreparedSessionActivation,
24
+ SessionActivationError,
25
+ } from "../sdk/session-activation";
21
26
  import {
22
27
  type CoordinatorModelProfileLoader,
23
28
  loadCoordinatorModelProfiles,
@@ -222,6 +227,8 @@ interface TurnRecord {
222
227
 
223
228
  type CoordinatorSessionStateValue =
224
229
  | "booting"
230
+ /** Live and endpoint-addressable, but withholding readiness until activation. */
231
+ | "prepared"
225
232
  | "ready_for_input"
226
233
  | "running"
227
234
  | "needs_user_input"
@@ -290,6 +297,8 @@ interface CoordinatorEventInput {
290
297
  metadata?: Record<string, string | number | boolean | null>;
291
298
  }
292
299
 
300
+ const UNOBSERVED_COMPENSATION_CODE = "broker_compensation_unobserved";
301
+
293
302
  const MISSING_FINAL_RESPONSE_ADVISORY = "completion_missing_final_response";
294
303
  const PROMPT_ACK_TIMEOUT_REASON = "runtime_prompt_ack_timeout";
295
304
  const DEFAULT_RUNTIME_PROMPT_ACK_TIMEOUT_MS = 10_000;
@@ -364,12 +373,18 @@ function toolSchema(name: CoordinatorToolName): {
364
373
  if (name === "gjc_coordinator_start_session") {
365
374
  return {
366
375
  name,
367
- description: "Start a broker-managed GJC session through canonical SDK lifecycle control.",
376
+ description:
377
+ "Start a broker-managed GJC session through canonical SDK lifecycle control. Set prepare_existing_thread to hold the session at prepared (endpoint-addressable, readiness withheld) so an existing chat thread can be bound before activation.",
368
378
  inputSchema: {
369
379
  type: "object",
370
380
  properties: {
371
381
  cwd,
372
382
  prompt: { type: "string" },
383
+ prepare_existing_thread: {
384
+ type: "boolean",
385
+ description:
386
+ "Create the session prepared instead of ready: no readiness is published and no initial prompt is accepted until gjc_coordinator_activate_session proves activation.",
387
+ },
373
388
  mpreset,
374
389
  idempotency_key: idempotencyKey,
375
390
  allow_mutation: allowMutation,
@@ -378,6 +393,22 @@ function toolSchema(name: CoordinatorToolName): {
378
393
  },
379
394
  };
380
395
  }
396
+ if (name === "gjc_coordinator_activate_session") {
397
+ return {
398
+ name,
399
+ description:
400
+ "Activate a prepared session so it publishes the readiness it withheld. Requires the session's own proof at the exact endpoint generation, so it fails closed while no existing-thread binding has been applied.",
401
+ inputSchema: {
402
+ type: "object",
403
+ properties: {
404
+ session_id: sessionId,
405
+ idempotency_key: idempotencyKey,
406
+ allow_mutation: allowMutation,
407
+ },
408
+ required: ["session_id", "idempotency_key", "allow_mutation"],
409
+ },
410
+ };
411
+ }
381
412
  if (name === "gjc_coordinator_stop_session") {
382
413
  return {
383
414
  name,
@@ -853,6 +884,38 @@ function boundedPublicResponse(response: Record<string, unknown>): Record<string
853
884
  return asRecord(value) ?? { ok: false, error: { code: "unavailable", message: "Invalid coordinator response." } };
854
885
  }
855
886
 
887
+ /**
888
+ * `activation_outcome_unknown` states only that the activation's outcome could
889
+ * not be observed: the session may already have published the readiness the
890
+ * request asked for. It is the one activation answer that proves nothing, so it
891
+ * is returned to the caller without being sealed as a settled receipt.
892
+ */
893
+ function isUnknownActivationOutcome(response: Record<string, unknown>): boolean {
894
+ if (response.ok !== false) return false;
895
+ return asRecord(response.error)?.code === "activation_outcome_unknown";
896
+ }
897
+
898
+ /**
899
+ * Which durable states may reach the activation proof, and which of them is
900
+ * already settled.
901
+ *
902
+ * Durable state is a record of what was proved, never a proof on its own. Only
903
+ * a `prepared` session (readiness still withheld) and a `ready_for_input` one
904
+ * (readiness already proved once) are activatable, and both are answered by the
905
+ * live session at the exact endpoint, generation, incarnation, and binding.
906
+ * Every other observed state — stale, booting, running, needs_user_input,
907
+ * completed, errored, unknown, or a missing state file — is not activatable and
908
+ * fails closed before any frame is sent.
909
+ */
910
+ function classifyCoordinatorActivation(
911
+ state: CoordinatorSessionState | null,
912
+ ): { activatable: true; settled: boolean; observed: string } | { activatable: false; observed: string } {
913
+ const observed = state?.state ?? "unknown";
914
+ if (observed === "prepared") return { activatable: true, settled: false, observed };
915
+ if (observed === "ready_for_input") return { activatable: true, settled: true, observed };
916
+ return { activatable: false, observed };
917
+ }
918
+
856
919
  interface RuntimePromptAcknowledgement {
857
920
  accepted: true;
858
921
  command_id: string;
@@ -2509,6 +2572,16 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2509
2572
  return normalizeRuntimePromptAcknowledgement(result);
2510
2573
  }
2511
2574
 
2575
+ /**
2576
+ * The outcome of a failed compensating close is unobserved, not decided: the
2577
+ * session may still be running. Sealing it under the idempotency key would
2578
+ * answer that uncertainty forever, so the key stays open for a real retry.
2579
+ */
2580
+ function isUnobservedCompensation(response: Record<string, unknown>): boolean {
2581
+ const error = asRecord(response.error);
2582
+ return error?.code === UNOBSERVED_COMPENSATION_CODE;
2583
+ }
2584
+
2512
2585
  function sdkError(error: unknown): Record<string, unknown> {
2513
2586
  if (error instanceof SdkClientError) return { ok: false, error: { code: error.code, message: error.message } };
2514
2587
  return {
@@ -2528,12 +2601,26 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2528
2601
  return path.join(namespaceDir, "idempotency", `${keyDigest}.json`);
2529
2602
  }
2530
2603
 
2604
+ /**
2605
+ * Run one mutation under its idempotency key, then seal its response as the
2606
+ * key's terminal replay.
2607
+ *
2608
+ * `isNonterminal` is the one exception. Sealing is correct for a decided
2609
+ * outcome, and wrong for a response that states only that the outcome could
2610
+ * not be observed: the key would answer that uncertainty forever, even once
2611
+ * the remote effect settled. A tool may declare such a response nonterminal,
2612
+ * which returns it to this caller while the receipt stays `in_progress`, so
2613
+ * an exact same-key retry re-runs the observation under the same request
2614
+ * digest. The default declares nothing nonterminal, so every other tool keeps
2615
+ * its existing caching, conflict, and replay behaviour unchanged.
2616
+ */
2531
2617
  async function withToolIdempotency(
2532
2618
  tool: string,
2533
2619
  idempotencyKey: string,
2534
2620
  canonicalArgs: Record<string, unknown>,
2535
2621
  operation: () => Promise<Record<string, unknown>>,
2536
2622
  recoverInProgress = false,
2623
+ isNonterminal: (response: Record<string, unknown>) => boolean = () => false,
2537
2624
  ): Promise<Record<string, unknown>> {
2538
2625
  const keyDigest = createHash("sha256").update(idempotencyKey).digest("hex");
2539
2626
  const requestDigest = createHash("sha256")
@@ -2584,6 +2671,9 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2584
2671
  };
2585
2672
  if (existing.state === "in_progress") {
2586
2673
  const response = boundedPublicResponse(await operation().catch(error => sdkError(error)));
2674
+ // The receipt keeps its original key and request digests, so a
2675
+ // reused key still conflicts and a later settled answer still seals.
2676
+ if (isNonterminal(response)) return response;
2587
2677
  await writeCoordinatorIdempotencyFile(file, {
2588
2678
  ...existing,
2589
2679
  state: "completed",
@@ -2610,6 +2700,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2610
2700
  };
2611
2701
  await writeCoordinatorIdempotencyFile(file, started);
2612
2702
  const response = boundedPublicResponse(await operation().catch(error => sdkError(error)));
2703
+ if (isNonterminal(response)) return response;
2613
2704
  await writeCoordinatorIdempotencyFile(file, {
2614
2705
  ...started,
2615
2706
  state: "completed",
@@ -2803,6 +2894,75 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2803
2894
  return binding.endpoint;
2804
2895
  }
2805
2896
 
2897
+ /**
2898
+ * The same incarnation-bound authority `resolveSessionEndpoint` requires,
2899
+ * plus the exact endpoint generation the activation frame has to name. A
2900
+ * session whose endpoint rolled or whose workspace binding drifted is refused
2901
+ * here, before any activation is attempted.
2902
+ */
2903
+ async function resolveSessionActivationTarget(
2904
+ session: Record<string, unknown>,
2905
+ idempotencyKey?: string,
2906
+ ): Promise<{ endpoint: { url: string; token: string }; endpointGeneration: number }> {
2907
+ const sessionId = optionalString(session.session_id) ?? optionalString(session.sessionId);
2908
+ const cwd = optionalString(session.cwd);
2909
+ const persistedWorkspace = optionalString(session.broker_workspace);
2910
+ const persistedGeneration =
2911
+ typeof session.endpoint_generation === "number" &&
2912
+ Number.isSafeInteger(session.endpoint_generation) &&
2913
+ session.endpoint_generation > 0
2914
+ ? session.endpoint_generation
2915
+ : null;
2916
+ const persistedIncarnation = optionalString(session.endpoint_incarnation);
2917
+ if (!sessionId || !cwd || !persistedWorkspace || persistedGeneration === null || !persistedIncarnation)
2918
+ throw new SdkClientError("not_found", "Coordinator session has no incarnation-bound broker identity.");
2919
+ const workspace = await canonicalBrokerWorkspace(cwd);
2920
+ if (!sameCanonicalPath(workspace, persistedWorkspace, platform))
2921
+ throw new SdkClientError("endpoint_stale", "Coordinator session workspace binding is stale.");
2922
+ const binding = await exactBrokerSessionBinding(sessionId, workspace, idempotencyKey);
2923
+ if (binding.endpointGeneration !== persistedGeneration || binding.endpointIncarnation !== persistedIncarnation)
2924
+ throw new SdkClientError("endpoint_stale", "Coordinator session endpoint incarnation is stale.");
2925
+ return { endpoint: binding.endpoint, endpointGeneration: binding.endpointGeneration };
2926
+ }
2927
+
2928
+ /**
2929
+ * Ask a prepared session to publish its withheld readiness.
2930
+ *
2931
+ * The Coordinator never writes a chat mapping and never fakes a readiness
2932
+ * signal: it proves exact endpoint authority, then delegates to the same
2933
+ * activation exchange the `gjc notify activate-thread` CLI uses. The session's
2934
+ * own activation gate remains the authority on whether a binding exists.
2935
+ */
2936
+ async function activatePreparedCoordinatorSession(
2937
+ session: Record<string, unknown>,
2938
+ sessionId: string,
2939
+ idempotencyKey: string,
2940
+ ): Promise<ActivatedPreparedSession> {
2941
+ const target = await resolveSessionActivationTarget(session, idempotencyKey);
2942
+ let client: SdkClient;
2943
+ try {
2944
+ client = await (services.connectSdk ?? ((url, token) => SdkClient.connect(url, token)))(
2945
+ target.endpoint.url,
2946
+ target.endpoint.token,
2947
+ );
2948
+ } catch {
2949
+ // Nothing was sent, so no activation can have been applied.
2950
+ throw new SessionActivationError("activation_unavailable", "The session endpoint could not be reached.");
2951
+ }
2952
+ try {
2953
+ return await requestPreparedSessionActivation(
2954
+ {
2955
+ request: async frame => (await client.request(frame)) as Record<string, unknown>,
2956
+ close: async () => await client.close(),
2957
+ },
2958
+ sessionId,
2959
+ target.endpointGeneration,
2960
+ );
2961
+ } finally {
2962
+ await client.close().catch(() => undefined);
2963
+ }
2964
+ }
2965
+
2806
2966
  async function listSessions(cwd?: string): Promise<Array<Record<string, unknown>>> {
2807
2967
  const roots = cwd ? [cwd] : config.allowedRoots;
2808
2968
  const listings = await Promise.all(
@@ -3606,6 +3766,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
3606
3766
  return response;
3607
3767
  },
3608
3768
  true,
3769
+ isUnobservedCompensation,
3609
3770
  );
3610
3771
  }
3611
3772
  if (name === "gjc_coordinator_read_status") {
@@ -4026,10 +4187,41 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4026
4187
  };
4027
4188
  }
4028
4189
  const prompt = typeof args.prompt === "string" && args.prompt.length > 0 ? args.prompt : null;
4190
+ /**
4191
+ * A prepared session is deliberately not ready for input: its readiness
4192
+ * is withheld until an operator-supplied thread is bound and activation
4193
+ * is proven. Accepting an initial prompt here would either be silently
4194
+ * dropped or delivered to a session no consumer has been told is live,
4195
+ * so it is refused before any broker mutation or idempotency record.
4196
+ */
4197
+ // Runtime dispatch hands `params.arguments` through unvalidated, so a
4198
+ // client sending the string "true" would coerce to false here and start
4199
+ // an ordinary ready session that accepts the prompt — the opposite of
4200
+ // what the schema promises. Reject a non-boolean before any mutation.
4201
+ const requestedPrepare = (args as Record<string, unknown>).prepare_existing_thread;
4202
+ if (requestedPrepare !== undefined && typeof requestedPrepare !== "boolean")
4203
+ return {
4204
+ ok: false,
4205
+ error: {
4206
+ code: "invalid_input",
4207
+ message: `prepare_existing_thread must be a boolean; received ${typeof requestedPrepare}.`,
4208
+ },
4209
+ };
4210
+ const preparesExistingThread = requestedPrepare === true;
4211
+ if (preparesExistingThread && prompt)
4212
+ return {
4213
+ ok: false,
4214
+ error: {
4215
+ code: "invalid_input",
4216
+ message:
4217
+ "prepare_existing_thread cannot carry an initial prompt; activate the session first, then send_prompt.",
4218
+ },
4219
+ };
4029
4220
  const canonicalArgs = {
4030
4221
  cwd,
4031
4222
  mpreset: mpresetResolution.mpreset,
4032
4223
  ...(prompt ? { prompt } : {}),
4224
+ ...(preparesExistingThread ? { prepare_existing_thread: true } : {}),
4033
4225
  allow_mutation: true,
4034
4226
  };
4035
4227
  return await withToolIdempotency(
@@ -4056,10 +4248,45 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4056
4248
  cwd,
4057
4249
  target: coordinatorLifecycleTarget(config.sessionCommand, cwd),
4058
4250
  ...(mpresetResolution.mpreset ? { modelPreset: mpresetResolution.mpreset } : {}),
4251
+ ...(preparesExistingThread ? { readiness: "deferred" } : {}),
4059
4252
  },
4060
4253
  idempotencyKey,
4061
4254
  ),
4062
4255
  );
4256
+ /**
4257
+ * Preparation is only real when the broker proves it. A create that
4258
+ * silently published readiness would leave a live session whose root
4259
+ * is already claimed, so the session is closed rather than reported
4260
+ * as prepared.
4261
+ */
4262
+ if (preparesExistingThread && created.readiness !== "prepared") {
4263
+ const unpreparedId = optionalString(created.sessionId ?? created.session_id);
4264
+ let compensated = true;
4265
+ if (unpreparedId) {
4266
+ compensated = await brokerSession(
4267
+ cwd,
4268
+ "session.close",
4269
+ { sessionId: unpreparedId },
4270
+ `${idempotencyKey}:unprepared-close`,
4271
+ ).then(
4272
+ () => true,
4273
+ () => false,
4274
+ );
4275
+ }
4276
+ // A swallowed compensation leaves a live, untracked session while
4277
+ // idempotency seals the failure, so exact retries only replay the
4278
+ // cached error and never reach the session again. Name the session
4279
+ // and mark the outcome unobserved so the key is not sealed.
4280
+ if (!compensated)
4281
+ throw new SdkClientError(
4282
+ UNOBSERVED_COMPENSATION_CODE,
4283
+ `SDK broker did not prepare the requested session, and closing the unprepared session ${unpreparedId} failed; it may still be running.`,
4284
+ );
4285
+ throw new SdkClientError(
4286
+ "broker_request_unavailable",
4287
+ "SDK broker did not prepare the requested session.",
4288
+ );
4289
+ }
4063
4290
  sessionId = safeExternalId("session", created.sessionId ?? created.session_id);
4064
4291
  const sessionCwd = await canonicalBrokerWorkspace(optionalString(created.cwd) ?? cwd);
4065
4292
  const binding = await exactBrokerSessionBinding(sessionId, sessionCwd, idempotencyKey);
@@ -4076,7 +4303,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4076
4303
  kind: "start",
4077
4304
  session: canonicalCreationSnapshot(session),
4078
4305
  remote_create_key: creation.request.remote_create_key,
4079
- initial_state: prompt ? "running" : "ready_for_input",
4306
+ initial_state: prompt ? "running" : preparesExistingThread ? "prepared" : "ready_for_input",
4080
4307
  initial_prompt: prompt
4081
4308
  ? { text: prompt, caller_key_digest: createHash("sha256").update(idempotencyKey).digest("hex") }
4082
4309
  : null,
@@ -4117,14 +4344,18 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4117
4344
  await advanceCreationReceipt(questionPaths, creation.keyDigest, "completed", response);
4118
4345
  return response;
4119
4346
  }
4120
- const sessionState = await writeSessionState(namespaceDir, sessionId, "ready_for_input", {
4121
- live: null,
4122
- reason: null,
4123
- });
4347
+ const sessionState = await writeSessionState(
4348
+ namespaceDir,
4349
+ sessionId,
4350
+ preparesExistingThread ? "prepared" : "ready_for_input",
4351
+ { live: null, reason: null },
4352
+ );
4124
4353
  await appendCoordinatorEvent(namespaceDir, {
4125
4354
  kind: "session.started",
4126
4355
  sessionId,
4127
- summary: `Session ${sessionId} started through SDK lifecycle control`,
4356
+ summary: preparesExistingThread
4357
+ ? `Session ${sessionId} prepared through SDK lifecycle control`
4358
+ : `Session ${sessionId} started through SDK lifecycle control`,
4128
4359
  payloadRef: path.relative(namespaceDir, sessionFile(sessionId)),
4129
4360
  });
4130
4361
  const response = {
@@ -4132,12 +4363,113 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4132
4363
  session: publicCoordinatorSession(session),
4133
4364
  session_state: publicCoordinatorSessionState(sessionState),
4134
4365
  lifecycle,
4366
+ ...(preparesExistingThread ? { session_id: sessionId, state: "prepared" as const } : {}),
4135
4367
  };
4136
4368
  await advanceCreationReceipt(questionPaths, creation.keyDigest, "projected", response);
4137
4369
  await advanceCreationReceipt(questionPaths, creation.keyDigest, "completed", response);
4138
4370
  return response;
4139
4371
  },
4140
4372
  true,
4373
+ isUnobservedCompensation,
4374
+ );
4375
+ }
4376
+ if (name === "gjc_coordinator_activate_session") {
4377
+ requireCoordinatorMutation(config, "sessions", args);
4378
+ const idempotencyKey = requiredIdempotencyKey(args);
4379
+ const sessionId = safeExternalId("session", args.session_id);
4380
+ return await withToolIdempotency(
4381
+ name,
4382
+ idempotencyKey,
4383
+ { session_id: sessionId, allow_mutation: true },
4384
+ async () =>
4385
+ await withSessionTransition(sessionId, async () => {
4386
+ const currentSession = asRecord(await readJsonFile(sessionFile(sessionId)));
4387
+ if (!currentSession)
4388
+ return {
4389
+ ok: false,
4390
+ error: { code: "not_found", message: `Coordinator session not found: ${sessionId}` },
4391
+ };
4392
+ const before = await readSessionState(namespaceDir, sessionId);
4393
+ /**
4394
+ * A settled activation is not reported from durable state alone: a
4395
+ * session that went stale, errored, completed, or never recorded a
4396
+ * state cannot be answered `already`, and even a recorded
4397
+ * `ready_for_input` has to be re-proved against the live session
4398
+ * below before it may be.
4399
+ */
4400
+ const eligibility = classifyCoordinatorActivation(before);
4401
+ if (!eligibility.activatable)
4402
+ return {
4403
+ ok: false,
4404
+ session_id: sessionId,
4405
+ state: eligibility.observed,
4406
+ session_state: publicCoordinatorSessionState(before),
4407
+ error: {
4408
+ code: "session_not_activatable",
4409
+ message: `Coordinator session is not activatable in state ${eligibility.observed}.`,
4410
+ },
4411
+ };
4412
+ let activated: ActivatedPreparedSession;
4413
+ try {
4414
+ activated = await activatePreparedCoordinatorSession(currentSession, sessionId, idempotencyKey);
4415
+ } catch (error) {
4416
+ if (!(error instanceof SessionActivationError)) throw error;
4417
+ return {
4418
+ ok: false,
4419
+ session_id: sessionId,
4420
+ state: before?.state ?? "unknown",
4421
+ session_state: publicCoordinatorSessionState(before),
4422
+ error: { code: error.code, message: error.message },
4423
+ };
4424
+ }
4425
+ /**
4426
+ * An already-ready session transitions nothing: the answer above is
4427
+ * the live session's own, proved at the exact endpoint generation
4428
+ * this call resolved, so durable state is neither rewritten nor
4429
+ * given a second readiness event.
4430
+ */
4431
+ if (eligibility.settled)
4432
+ return {
4433
+ ok: true,
4434
+ session_id: sessionId,
4435
+ status: activated.status,
4436
+ state: "ready_for_input" as const,
4437
+ endpoint_generation: activated.endpointGeneration,
4438
+ session_state: publicCoordinatorSessionState(before),
4439
+ };
4440
+ // Only a proven `activated`/`already` moves durable state to ready.
4441
+ const sessionState = await writeSessionState(namespaceDir, sessionId, "ready_for_input", {
4442
+ live: true,
4443
+ reason: null,
4444
+ });
4445
+ await appendCoordinatorEvent(namespaceDir, {
4446
+ kind: "session.started",
4447
+ sessionId,
4448
+ summary: `Session ${sessionId} activated its withheld readiness`,
4449
+ payloadRef: path.relative(namespaceDir, sessionFile(sessionId)),
4450
+ metadata: {
4451
+ status: activated.status,
4452
+ endpoint_generation: activated.endpointGeneration,
4453
+ },
4454
+ });
4455
+ return {
4456
+ ok: true,
4457
+ session_id: sessionId,
4458
+ status: activated.status,
4459
+ state: "ready_for_input" as const,
4460
+ endpoint_generation: activated.endpointGeneration,
4461
+ session_state: publicCoordinatorSessionState(sessionState),
4462
+ };
4463
+ }),
4464
+ /**
4465
+ * A crash between writing the receipt and settling it leaves an
4466
+ * in-progress activation. Recovering it is safe because every retry
4467
+ * re-proves the workspace, generation, and incarnation before it
4468
+ * sends anything, and the session answers a repeated activation
4469
+ * `already` rather than publishing readiness twice.
4470
+ */
4471
+ true,
4472
+ isUnknownActivationOutcome,
4141
4473
  );
4142
4474
  }
4143
4475
  if (name === "gjc_coordinator_send_prompt") {
@@ -4166,6 +4498,25 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4166
4498
  error: { code: "not_found", message: `Coordinator session not found: ${sessionId}` },
4167
4499
  };
4168
4500
  }
4501
+ /**
4502
+ * A prepared session is not ready for input. Its readiness is still
4503
+ * withheld, so a prompt here would be delivered to a session no
4504
+ * consumer has been told is live, and (for an existing-thread
4505
+ * preparation) before its root binding could be applied.
4506
+ */
4507
+ const preparedState = await readSessionState(namespaceDir, sessionId);
4508
+ if (preparedState?.state === "prepared") {
4509
+ return {
4510
+ ok: false,
4511
+ session_id: sessionId,
4512
+ state: "prepared" as const,
4513
+ error: {
4514
+ code: "session_not_activated",
4515
+ message: `Session ${sessionId} is prepared; activate it before sending a prompt.`,
4516
+ },
4517
+ session_state: publicCoordinatorSessionState(preparedState),
4518
+ };
4519
+ }
4169
4520
  const previousActiveTurn = await readActiveTurn(namespaceDir, sessionId);
4170
4521
  if (previousActiveTurn && args.queue !== true && args.force !== true) {
4171
4522
  return {
@@ -122,7 +122,7 @@ Deep Interview threshold: <resolvedThresholdPercent> (source: <resolvedThreshold
122
122
  - Substitute `<resolvedThreshold>`, `<resolvedThresholdPercent>`, and `<resolvedThresholdSource>` throughout the remaining instructions before continuing.
123
123
  - Include `threshold_source` in the first `gjc deep-interview write` payload and preserve it on later state updates; do not edit `.gjc/_session-{sessionid}/state` files directly unless an explicit force override is active.
124
124
  - Include both threshold and source in the final spec metadata.
125
- - Read any `language` object from active deep-interview state and carry `language.instruction` forward mechanically. If absent, default to English unless `{{ARGUMENTS}}` makes another user/session language obvious or the user explicitly requests another language. Do not add language-specific special cases.
125
+ - Read any `language` object from active deep-interview state and carry `language.instruction` forward mechanically. If absent, default to English unless the appended `User:` request makes another user/session language obvious or the user explicitly requests another language. Do not add language-specific special cases.
126
126
 
127
127
  ## Phase 0.5: Suitability Gate
128
128
 
@@ -308,6 +308,8 @@ One generation freezes the change set and reviews it exactly once:
308
308
  - Native/desktop/tui surfaces require a structurally valid screenshot, PTY capture with terminal control codes, or app-automation transcript.
309
309
  - API/package surfaces require a real artifact file or typed receipt whose artifact `kind` contains one of `api`, `package`, `consumer`, `black-box`, or `test-report`; examples: `api-package-test-report`, `package-consumer-report`, `black-box-api-receipt`. Algorithm/math surfaces require a real artifact file or typed receipt whose artifact `kind` contains one of `property`, `boundary`, `edge`, `adversarial`, `failure`, `math`, `algorithm`, or `test-report`; examples: `property-test-report`, `algorithm-boundary-report`. Bare `inlineEvidence` text alone is not sufficient for any surface.
310
310
  - The mandatory **computer-use** red-team suite (`kill-switch-bypass`, `suspended-enforcement`, `permission-revoked`, …) is conditional, not universal: require it only when computer/desktop control is genuinely part of the product surface being dogfooded. For every other product type, prove the change through the matching live surface instead — browser-use automation for web/GUI, bash/CLI live invocation or argv replay for CLI, and real artifacts or typed receipts for API/package/algorithm/math. Editing docs, prompts, or skills that merely mention computer-use does not by itself make the computer-use suite applicable; pick the red-team surface that matches what the change actually ships.
311
+ - **The runtime decides applicability from the change set, and it fails closed.** Judgement about "what the change actually ships" does not override it, so check the paths before assuming the suite is skippable. `gjc ultragoal checkpoint --status complete` requires the suite whenever the computed change set touches computer source (`crates/pi-natives/src/computer/**`), the computer tool (`packages/coding-agent/src/tools/computer.ts`, `packages/coding-agent/src/tools/computer/**`), or a **shared behavior registry** — `packages/coding-agent/src/config/settings-schema.ts`, `packages/coding-agent/src/tools/index.ts`, `packages/coding-agent/src/tools/renderers.ts`. The registries are deliberately unconditional: they mix computer and non-computer entries, and a path-only or uninspectable change cannot prove computer controls were untouched, so *any* edit to them demands the suite even when the diff contains nothing computer-related. The suite is also required whenever change-set capture was incomplete. Generated bindings (`packages/natives/native/index.{d.ts,js}`), prompt/skill/doc files, and every other path do not trigger it on their own.
312
+ - Practical consequence: a change that is not about computer-use at all — say a new settings key in `settings-schema.ts` — will still be gated on the seven mandatory cases. Do **not** fabricate them to get past the gate, and do not weaken the gate. Either supply a genuine suite, or treat it as a blocker and escalate to the operator (`gjc ultragoal record-critic-gate-override` exists for an authorized override).
311
313
  7. The executor QA/red-team lane must report a matrix using `executorQa.contractCoverage`, `executorQa.surfaceEvidence`, `executorQa.adversarialCases`, and `executorQa.artifactRefs`. Not-applicable rows are allowed only in `contractCoverage` and `surfaceEvidence`; each `status: "not_applicable"` row requires `contractRef` plus `reason`. `adversarialCases` rows cannot be not-applicable.
312
314
  8. **Join before repairing.** Fold all three lane verdicts and the final code review into the strict gate under `iteration.reviewCohort` (`reviewGeneration`, `sourceHash`, `joined: true`, and the three `lanes`). No lane may checkpoint on its own, and no fix work starts until the findings are joined. Clean means `architectReview.architectureStatus`, `architectReview.productStatus`, and `architectReview.codeStatus` are all `"CLEAR"`, `architectReview.recommendation` is `"APPROVE"`, executor QA statuses are `"passed"`, iteration is `"passed"` with `fullRerun: true`, the cohort is joined with every lane clean and hash-bound, every evidence field is non-empty, every required matrix row is present, and every blockers array is empty. `COMMENT`, `WATCH`, `REQUEST CHANGES`, `BLOCK`, missing evidence, missing or shallow matrix rows, plan/code mismatches, or non-empty blockers are non-clean.
313
315
  9. If the joined findings contain any blocker, do **not** checkpoint `complete` and do **not** call `goal({"op":"complete"})`. Record **one consolidated blocker batch** for all findings from the whole cohort instead of one story per lane:
@@ -9,6 +9,7 @@ export {
9
9
  loadExtensionFromFactory,
10
10
  loadExtensions,
11
11
  } from "./loader";
12
+ export * from "./ouroboros-ooo-bridge";
12
13
  export * from "./prefix-command-bridge";
13
14
  export * from "./runner";
14
15
  // Type guards