@gajae-code/coding-agent 0.12.0 → 0.12.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 (223) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/config/settings-schema.d.ts +54 -0
  4. package/dist/types/exa/mcp-client.d.ts +10 -1
  5. package/dist/types/exec/bash-executor.d.ts +45 -12
  6. package/dist/types/extensibility/extensions/types.d.ts +16 -2
  7. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  8. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  9. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  10. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  11. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  12. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  13. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  15. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +18 -0
  16. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  17. package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
  18. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  19. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  21. package/dist/types/modes/acp/acp-agent.d.ts +2 -19
  22. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  23. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  24. package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
  25. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  26. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  27. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  28. package/dist/types/reminders/star-reminder.d.ts +3 -13
  29. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  30. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  31. package/dist/types/sdk/broker/transport.d.ts +2 -0
  32. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  33. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  34. package/dist/types/sdk/bus/config.d.ts +6 -2
  35. package/dist/types/sdk/bus/index.d.ts +29 -0
  36. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
  37. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  38. package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
  39. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  40. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  41. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  42. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  43. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  44. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  45. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  46. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  47. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  48. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  49. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  50. package/dist/types/sdk/prompt-status.d.ts +23 -3
  51. package/dist/types/sdk/session.d.ts +6 -0
  52. package/dist/types/session/agent-session.d.ts +38 -2
  53. package/dist/types/session/artifacts.d.ts +8 -0
  54. package/dist/types/session/blob-store.d.ts +57 -4
  55. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  56. package/dist/types/session/session-manager.d.ts +23 -6
  57. package/dist/types/session/session-storage.d.ts +4 -0
  58. package/dist/types/session/streaming-output.d.ts +36 -7
  59. package/dist/types/setup/credential-import.d.ts +12 -0
  60. package/dist/types/skill-state/active-state.d.ts +12 -0
  61. package/dist/types/skill-state/workflow-hud.d.ts +6 -0
  62. package/dist/types/task/executor.d.ts +3 -2
  63. package/dist/types/task/index.d.ts +3 -1
  64. package/dist/types/task/provider-retry-status.d.ts +8 -0
  65. package/dist/types/task/receipt.d.ts +7 -1
  66. package/dist/types/task/types.d.ts +23 -0
  67. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  68. package/dist/types/tools/bash-interactive.d.ts +4 -1
  69. package/dist/types/tools/bash.d.ts +3 -1
  70. package/dist/types/tools/image-gen.d.ts +4 -0
  71. package/dist/types/tools/index.d.ts +4 -0
  72. package/dist/types/tools/output-meta.d.ts +23 -2
  73. package/dist/types/tools/path-utils.d.ts +1 -0
  74. package/dist/types/tools/read.d.ts +0 -6
  75. package/dist/types/tools/review.d.ts +5 -7
  76. package/dist/types/tools/subagent.d.ts +2 -0
  77. package/dist/types/utils/gh.d.ts +13 -0
  78. package/dist/types/utils/image-resize.d.ts +1 -0
  79. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  80. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  81. package/dist/types/web/search/providers/xai.d.ts +2 -0
  82. package/package.json +10 -8
  83. package/scripts/acp-conformance-agent.ts +218 -0
  84. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  85. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  86. package/scripts/resident-memory-bench.ts +1341 -0
  87. package/scripts/run-acp-conformance.ts +285 -0
  88. package/src/async/job-manager.ts +78 -20
  89. package/src/cli/fast-help.ts +1 -1
  90. package/src/cli/plugin-cli.ts +176 -18
  91. package/src/commands/team.ts +19 -1
  92. package/src/config/file-lock.ts +11 -2
  93. package/src/config/settings-schema.ts +51 -0
  94. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  95. package/src/coordinator-mcp/server.ts +5 -12
  96. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
  97. package/src/defaults/gjc/skills/ralplan/SKILL.md +19 -16
  98. package/src/defaults/gjc/skills/team/SKILL.md +2 -0
  99. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -24
  100. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  101. package/src/defaults/gjc-defaults.ts +4 -4
  102. package/src/exa/mcp-client.ts +12 -3
  103. package/src/exec/bash-executor.ts +144 -27
  104. package/src/extensibility/extensions/runner.ts +15 -0
  105. package/src/extensibility/extensions/types.ts +18 -1
  106. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  107. package/src/extensibility/gjc-plugins/index.ts +19 -2
  108. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  109. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  110. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  111. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  112. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  113. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  114. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  115. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  116. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  117. package/src/extensibility/gjc-plugins/types.ts +141 -0
  118. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  119. package/src/gjc-runtime/launch-tmux.ts +25 -6
  120. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  121. package/src/gjc-runtime/ralplan-runtime.ts +411 -17
  122. package/src/gjc-runtime/team-launch.ts +8 -0
  123. package/src/gjc-runtime/team-runtime.ts +140 -32
  124. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
  126. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  127. package/src/gjc-runtime/ultragoal-runtime.ts +401 -611
  128. package/src/gjc-runtime/workflow-manifest.generated.json +8 -99
  129. package/src/gjc-runtime/workflow-manifest.ts +4 -68
  130. package/src/internal-urls/docs-index.generated.ts +15 -15
  131. package/src/modes/DESIGN.md +51 -0
  132. package/src/modes/acp/acp-agent.ts +298 -186
  133. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  134. package/src/modes/components/notifications-settings-editor.ts +12 -1
  135. package/src/modes/components/oauth-selector.ts +68 -30
  136. package/src/modes/components/session-observer-overlay.ts +305 -86
  137. package/src/modes/components/settings-selector.ts +56 -5
  138. package/src/modes/components/status-line/gh.ts +16 -0
  139. package/src/modes/components/tool-status-header.ts +3 -14
  140. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  141. package/src/modes/controllers/command-controller.ts +4 -0
  142. package/src/modes/controllers/extension-ui-controller.ts +7 -0
  143. package/src/modes/controllers/selector-controller.ts +45 -0
  144. package/src/modes/runtime-init.ts +3 -0
  145. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  146. package/src/prompts/system/eager-todo.md +8 -4
  147. package/src/prompts/system/system-prompt.md +11 -4
  148. package/src/prompts/tools/bash.md +5 -5
  149. package/src/prompts/tools/search-tool-bm25.md +5 -0
  150. package/src/prompts/tools/task.md +1 -1
  151. package/src/reminders/star-reminder.ts +4 -42
  152. package/src/runtime-mcp/manager.ts +19 -4
  153. package/src/sdk/acp/adapter.ts +22 -6
  154. package/src/sdk/broker/transport.ts +35 -3
  155. package/src/sdk/bus/attachment-registry.ts +5 -1
  156. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  157. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  158. package/src/sdk/bus/config.ts +18 -2
  159. package/src/sdk/bus/index.ts +661 -126
  160. package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
  161. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  162. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  163. package/src/sdk/bus/reconciliation-store.ts +95 -16
  164. package/src/sdk/bus/rich-draft.ts +1 -1
  165. package/src/sdk/bus/rich-render.ts +2 -2
  166. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  167. package/src/sdk/bus/telegram-cli.ts +33 -8
  168. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  169. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  170. package/src/sdk/bus/telegram-daemon.ts +916 -23
  171. package/src/sdk/bus/telegram-reference.ts +40 -3
  172. package/src/sdk/bus/threaded-render.ts +48 -3
  173. package/src/sdk/bus/topic-registry.ts +20 -0
  174. package/src/sdk/host/query/revision-store.ts +36 -2
  175. package/src/sdk/host/reverse-leases.ts +44 -7
  176. package/src/sdk/prompt-status.ts +16 -3
  177. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  178. package/src/sdk/session.ts +88 -9
  179. package/src/session/agent-session.ts +822 -172
  180. package/src/session/artifacts.ts +22 -0
  181. package/src/session/auth-broker-config.ts +10 -3
  182. package/src/session/blob-store.ts +915 -24
  183. package/src/session/internal/managed-session-scope.ts +44 -6
  184. package/src/session/internal/managed-session-storage.ts +48 -6
  185. package/src/session/session-manager.ts +1614 -435
  186. package/src/session/session-storage.ts +9 -1
  187. package/src/session/streaming-output.ts +238 -68
  188. package/src/setup/credential-import.ts +75 -13
  189. package/src/skill-state/active-state.ts +32 -0
  190. package/src/skill-state/workflow-hud.ts +17 -0
  191. package/src/skill-state/workflow-mutation-guard.ts +496 -2
  192. package/src/task/executor.ts +122 -39
  193. package/src/task/index.ts +23 -14
  194. package/src/task/provider-retry-status.ts +16 -0
  195. package/src/task/receipt.ts +23 -7
  196. package/src/task/render.ts +6 -2
  197. package/src/task/types.ts +108 -0
  198. package/src/tools/ask-answer-registry.ts +12 -6
  199. package/src/tools/ask.ts +86 -55
  200. package/src/tools/ast-edit.ts +1 -0
  201. package/src/tools/ast-grep.ts +1 -0
  202. package/src/tools/bash-interactive.ts +8 -3
  203. package/src/tools/bash.ts +503 -60
  204. package/src/tools/find.ts +1 -0
  205. package/src/tools/image-gen.ts +29 -5
  206. package/src/tools/index.ts +4 -0
  207. package/src/tools/output-meta.ts +108 -14
  208. package/src/tools/path-utils.ts +3 -2
  209. package/src/tools/read.ts +18 -0
  210. package/src/tools/review.ts +5 -7
  211. package/src/tools/search.ts +1 -0
  212. package/src/tools/subagent-render.ts +6 -2
  213. package/src/tools/subagent.ts +6 -0
  214. package/src/tools/telegram-send.ts +6 -3
  215. package/src/utils/gh.ts +39 -0
  216. package/src/utils/image-resize.ts +3 -0
  217. package/src/web/search/providers/anthropic.ts +4 -4
  218. package/src/web/search/providers/kimi.ts +15 -5
  219. package/src/web/search/providers/searxng.ts +47 -4
  220. package/src/web/search/providers/xai.ts +7 -2
  221. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  222. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  223. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
+ import type { Dirent } from "node:fs";
2
3
  import * as fs from "node:fs/promises";
3
4
  import * as path from "node:path";
4
5
  import { getConfigRootDir } from "@gajae-code/utils";
@@ -22,7 +23,7 @@ import {
22
23
  RepositoryBindingError,
23
24
  } from "./repository-binding";
24
25
  import { GJC_RALPLAN_ARTIFACT_ENV, isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
25
- import { gjcRoot, modeStatePath, sessionPlansDir } from "./session-layout";
26
+ import { gjcRoot, modeStatePath, sessionIdFromDirName, sessionPlansDir } from "./session-layout";
26
27
  import { resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
27
28
  import { migrateWorkflowState } from "./state-migrations";
28
29
  import { runNativeStateCommand } from "./state-runtime";
@@ -33,6 +34,7 @@ import {
33
34
  writeArtifact,
34
35
  writeWorkflowEnvelopeAtomic,
35
36
  } from "./state-writer";
37
+ import { probeGjcTeamAvailability } from "./team-runtime";
36
38
  import { assertSafePathComponent, CommandError, flagValue, hasFlag } from "./workflow-cli-common";
37
39
  import { getSkillManifest } from "./workflow-manifest";
38
40
 
@@ -74,6 +76,16 @@ export const PLANNING_STUCK_MARKER = "PLANNING-STUCK";
74
76
  export const RALPLAN_DEFAULT_MAX_REVIEW_PASSES_PER_LANE = 1;
75
77
  /** Inclusive upper bound for `gjc.ralplan.maxReviewPassesPerLane` settings overrides. */
76
78
  export const RALPLAN_MAX_REVIEW_PASSES_PER_LANE_LIMIT = 10;
79
+ export type RalplanAutoHandoffTarget = "off" | "ultragoal" | "team";
80
+
81
+ export interface RalplanAutoHandoffResolution {
82
+ configuredTarget: RalplanAutoHandoffTarget;
83
+ effectiveTarget: RalplanAutoHandoffTarget;
84
+ degradationReason: string | null;
85
+ source: string;
86
+ }
87
+
88
+ const RALPLAN_AUTO_HANDOFF_TARGETS = new Set<RalplanAutoHandoffTarget>(["off", "ultragoal", "team"]);
77
89
 
78
90
  const RALPLAN_ITERATION_OPENER_STAGES = new Set<RalplanStage>(["planner", "revision"]);
79
91
 
@@ -391,6 +403,115 @@ export async function resolveRalplanMaxIterations(cwd: string): Promise<{ maxIte
391
403
  if (user !== null) return { maxIterations: user, source: userPath };
392
404
  return { maxIterations: RALPLAN_DEFAULT_MAX_ITERATIONS, source: "default" };
393
405
  }
406
+ function parseRalplanAutoHandoffTarget(value: unknown): RalplanAutoHandoffTarget | undefined {
407
+ return typeof value === "string" && RALPLAN_AUTO_HANDOFF_TARGETS.has(value as RalplanAutoHandoffTarget)
408
+ ? (value as RalplanAutoHandoffTarget)
409
+ : undefined;
410
+ }
411
+
412
+ type RalplanAutoHandoffOptions = {
413
+ planningStuck?: boolean;
414
+ teamAvailabilityProbe?: () => { available: true } | { available: false; reason: string };
415
+ };
416
+
417
+ type RalplanAutoHandoffSetting =
418
+ | { kind: "absent" }
419
+ | { kind: "valid"; value: RalplanAutoHandoffTarget }
420
+ | { kind: "invalid"; reason: string };
421
+
422
+ function parsePresentRalplanAutoHandoff(value: unknown): RalplanAutoHandoffSetting {
423
+ const target = parseRalplanAutoHandoffTarget(value);
424
+ return target === undefined
425
+ ? {
426
+ kind: "invalid",
427
+ reason: "expected gjc.ralplan.autoHandoff to be one of off, ultragoal, team",
428
+ }
429
+ : { kind: "valid", value: target };
430
+ }
431
+
432
+ function parseRalplanAutoHandoffSettings(parsed: unknown): RalplanAutoHandoffSetting {
433
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { kind: "absent" };
434
+ const settings = parsed as Record<string, unknown>;
435
+ if (Object.hasOwn(settings, "gjc.ralplan.autoHandoff")) {
436
+ return parsePresentRalplanAutoHandoff(settings["gjc.ralplan.autoHandoff"]);
437
+ }
438
+ const gjc = settings.gjc;
439
+ if (!gjc || typeof gjc !== "object" || Array.isArray(gjc)) return { kind: "absent" };
440
+ const ralplan = (gjc as Record<string, unknown>).ralplan;
441
+ if (!ralplan || typeof ralplan !== "object" || Array.isArray(ralplan)) return { kind: "absent" };
442
+ const ralplanSettings = ralplan as Record<string, unknown>;
443
+ if (!Object.hasOwn(ralplanSettings, "autoHandoff")) return { kind: "absent" };
444
+ return parsePresentRalplanAutoHandoff(ralplanSettings.autoHandoff);
445
+ }
446
+
447
+ async function readSettingsAutoHandoff(settingsPath: string): Promise<RalplanAutoHandoffSetting> {
448
+ let raw: string;
449
+ try {
450
+ raw = await Bun.file(settingsPath).text();
451
+ } catch (error) {
452
+ if (getErrorCode(error) === "ENOENT") return { kind: "absent" };
453
+ return {
454
+ kind: "invalid",
455
+ reason: `unable to read settings: ${error instanceof Error ? error.message : String(error)}`,
456
+ };
457
+ }
458
+ let parsed: unknown;
459
+ try {
460
+ parsed = JSON.parse(raw);
461
+ } catch (error) {
462
+ return {
463
+ kind: "invalid",
464
+ reason: `malformed JSON: ${error instanceof Error ? error.message : String(error)}`,
465
+ };
466
+ }
467
+ return parseRalplanAutoHandoffSettings(parsed);
468
+ }
469
+
470
+ export async function resolveRalplanAutoHandoff(
471
+ cwd: string,
472
+ options: RalplanAutoHandoffOptions = {},
473
+ ): Promise<RalplanAutoHandoffResolution> {
474
+ const projectPath = path.join(gjcRoot(cwd), "settings.json");
475
+ const project = await readSettingsAutoHandoff(projectPath);
476
+ if (project.kind === "invalid") {
477
+ throw new RalplanCommandError(2, `invalid ralplan settings at ${projectPath}: ${project.reason}`);
478
+ }
479
+ if (project.kind === "valid") {
480
+ return resolveRalplanAutoHandoffTarget(project.value, projectPath, options);
481
+ }
482
+ const userPath = path.join(getConfigRootDir(), "settings.json");
483
+ const user = await readSettingsAutoHandoff(userPath);
484
+ if (user.kind === "invalid") {
485
+ throw new RalplanCommandError(2, `invalid ralplan settings at ${userPath}: ${user.reason}`);
486
+ }
487
+ return resolveRalplanAutoHandoffTarget(
488
+ user.kind === "valid" ? user.value : "off",
489
+ user.kind === "valid" ? userPath : "default",
490
+ options,
491
+ );
492
+ }
493
+
494
+ function resolveRalplanAutoHandoffTarget(
495
+ configuredTarget: RalplanAutoHandoffTarget,
496
+ source: string,
497
+ options: RalplanAutoHandoffOptions,
498
+ ): RalplanAutoHandoffResolution {
499
+ if (options.planningStuck) {
500
+ return { configuredTarget, effectiveTarget: "off", degradationReason: "planning_stuck", source };
501
+ }
502
+ if (configuredTarget !== "team")
503
+ return { configuredTarget, effectiveTarget: configuredTarget, degradationReason: null, source };
504
+
505
+ const availability = (options.teamAvailabilityProbe ?? probeGjcTeamAvailability)();
506
+ return availability.available
507
+ ? { configuredTarget, effectiveTarget: "team", degradationReason: null, source }
508
+ : {
509
+ configuredTarget,
510
+ effectiveTarget: "off",
511
+ degradationReason: `team_unavailable:${availability.reason}`,
512
+ source,
513
+ };
514
+ }
394
515
 
395
516
  function parseMaxReviewPassesPerLaneValue(value: unknown): number | null {
396
517
  return parseBoundedPositiveInteger(value, RALPLAN_MAX_REVIEW_PASSES_PER_LANE_LIMIT);
@@ -763,6 +884,8 @@ async function persistActiveRunId(cwd: string, sessionId: string, runId: string,
763
884
  for (const key of Object.keys(existing)) {
764
885
  if (key.startsWith("last_review_verdict")) delete existing[key];
765
886
  }
887
+ delete existing.planning_stuck;
888
+ delete existing.auto_handoff;
766
889
  }
767
890
  if (
768
891
  existing.run_id === runId &&
@@ -1094,6 +1217,194 @@ async function applyLaneVerdictUpdate(
1094
1217
  { cwd },
1095
1218
  );
1096
1219
  }
1220
+ function ralplanPlanningStuckIndexKey(entry: unknown): string | undefined {
1221
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return undefined;
1222
+ const record = entry as Record<string, unknown>;
1223
+ return record.planning_stuck === true ? "planning_stuck" : undefined;
1224
+ }
1225
+
1226
+ async function recordRalplanPlanningStuck(
1227
+ cwd: string,
1228
+ sessionId: string,
1229
+ runId: string,
1230
+ reason: string,
1231
+ ): Promise<void> {
1232
+ const runDir = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId);
1233
+ await appendJsonlIdempotent(
1234
+ path.join(runDir, "index.jsonl"),
1235
+ {
1236
+ event: "planning_stuck",
1237
+ planning_stuck: true,
1238
+ marker: PLANNING_STUCK_MARKER,
1239
+ reason,
1240
+ created_at: new Date().toISOString(),
1241
+ },
1242
+ {
1243
+ cwd,
1244
+ audit: {
1245
+ category: "ledger",
1246
+ verb: "append",
1247
+ owner: "gjc-runtime",
1248
+ skill: "ralplan",
1249
+ sessionId,
1250
+ },
1251
+ key: ralplanPlanningStuckIndexKey,
1252
+ },
1253
+ );
1254
+ const statePath = ralplanStatePath(cwd, sessionId);
1255
+ await withWorkflowStateLock(
1256
+ statePath,
1257
+ async () => {
1258
+ const existingRead = await readExistingStateForMutation(statePath);
1259
+ if (existingRead.kind === "corrupt") return;
1260
+ let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
1261
+ if (existing.run_id !== runId) return;
1262
+ existing.planning_stuck = { marker: PLANNING_STUCK_MARKER, reason };
1263
+ existing = migrateWorkflowState(existing, "ralplan").state;
1264
+ existing.updated_at = new Date().toISOString();
1265
+ await writeWorkflowEnvelopeAtomic(statePath, existing, {
1266
+ cwd,
1267
+ lockHeld: true,
1268
+ receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan planning-stuck", sessionId },
1269
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
1270
+ });
1271
+ },
1272
+ { cwd },
1273
+ );
1274
+ }
1275
+
1276
+ async function readRalplanFinalAdmission(
1277
+ cwd: string,
1278
+ sessionId: string,
1279
+ runId: string,
1280
+ ): Promise<RalplanAutoHandoffResolution | undefined> {
1281
+ const index = await loadRalplanIndexForCap(cwd, sessionId, runId);
1282
+ if (index.rawText === undefined) return undefined;
1283
+ let finalAdmission: RalplanAutoHandoffResolution | undefined;
1284
+ for (const line of index.rawText.split(/\r?\n/)) {
1285
+ try {
1286
+ const row = JSON.parse(line) as Record<string, unknown>;
1287
+ if (row.stage === "final" && typeof row.path === "string" && typeof row.sha256 === "string") {
1288
+ finalAdmission = parseRalplanFinalAdmission(row.auto_handoff) ?? unavailableRalplanFinalAdmission();
1289
+ }
1290
+ } catch {
1291
+ // The artifact dedupe guard handles malformed ledger records separately.
1292
+ }
1293
+ }
1294
+ return finalAdmission;
1295
+ }
1296
+
1297
+ async function readRalplanPlanningStuck(cwd: string, sessionId: string, runId: string): Promise<boolean> {
1298
+ const index = await loadRalplanIndexForCap(cwd, sessionId, runId);
1299
+ if (index.rawText === undefined) return index.indexPresent;
1300
+ if (index.indexPresent && index.rawLineCount > 0 && index.parseableLines === 0) return true;
1301
+ for (const line of index.rawText.split(/\r?\n/)) {
1302
+ if (!line.trim()) continue;
1303
+ try {
1304
+ const row = JSON.parse(line) as Record<string, unknown>;
1305
+ if (row.planning_stuck === true) return true;
1306
+ } catch {
1307
+ return true;
1308
+ }
1309
+ }
1310
+ return false;
1311
+ }
1312
+
1313
+ async function persistRalplanFinalAdmission(
1314
+ cwd: string,
1315
+ sessionId: string,
1316
+ runId: string,
1317
+ admission: RalplanAutoHandoffResolution,
1318
+ ): Promise<void> {
1319
+ const statePath = ralplanStatePath(cwd, sessionId);
1320
+ await withWorkflowStateLock(
1321
+ statePath,
1322
+ async () => {
1323
+ const existingRead = await readExistingStateForMutation(statePath);
1324
+ if (existingRead.kind === "corrupt") {
1325
+ throw new RalplanCommandError(
1326
+ 2,
1327
+ `existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to record final admission`,
1328
+ );
1329
+ }
1330
+ let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
1331
+ if (existing.run_id !== runId) return;
1332
+ existing.auto_handoff = admission;
1333
+ existing = migrateWorkflowState(existing, "ralplan").state;
1334
+ existing.updated_at = new Date().toISOString();
1335
+ await writeWorkflowEnvelopeAtomic(statePath, existing, {
1336
+ cwd,
1337
+ lockHeld: true,
1338
+ receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan final-admission", sessionId },
1339
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
1340
+ });
1341
+ },
1342
+ { cwd },
1343
+ );
1344
+ }
1345
+
1346
+ async function findExistingRalplanRunOwners(cwd: string, runId: string): Promise<string[]> {
1347
+ let entries: Dirent<string>[];
1348
+ try {
1349
+ entries = await fs.readdir(gjcRoot(cwd), { withFileTypes: true });
1350
+ } catch (error) {
1351
+ const err = error as NodeJS.ErrnoException;
1352
+ if (err.code === "ENOENT" || err.code === "ENOTDIR") return [];
1353
+ throw error;
1354
+ }
1355
+
1356
+ const owners = await Promise.all(
1357
+ entries.map(async entry => {
1358
+ if (!entry.isDirectory()) return undefined;
1359
+ const sessionId = sessionIdFromDirName(entry.name);
1360
+ if (!sessionId) return undefined;
1361
+
1362
+ const stateRead = await readExistingStateForMutation(ralplanStatePath(cwd, sessionId));
1363
+ const stateOwnsRun =
1364
+ stateRead.kind === "valid" &&
1365
+ typeof stateRead.value.run_id === "string" &&
1366
+ stateRead.value.run_id.trim() === runId;
1367
+ if (stateOwnsRun) return sessionId;
1368
+
1369
+ try {
1370
+ const stat = await fs.stat(path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId));
1371
+ return stat.isDirectory() ? sessionId : undefined;
1372
+ } catch (error) {
1373
+ const err = error as NodeJS.ErrnoException;
1374
+ if (err.code === "ENOENT" || err.code === "ENOTDIR") return undefined;
1375
+ throw error;
1376
+ }
1377
+ }),
1378
+ );
1379
+ return owners.filter((owner): owner is string => owner !== undefined).sort();
1380
+ }
1381
+
1382
+ async function resolveArtifactSessionId(args: readonly string[], cwd: string, explicitRunId: string | undefined) {
1383
+ const flagSessionId = flagValue(args, "--session-id");
1384
+ const currentSession = resolveGjcSessionForWrite(cwd, {
1385
+ flagValue: flagSessionId,
1386
+ envSessionId: process.env.GJC_SESSION_ID,
1387
+ });
1388
+ if (!explicitRunId) return currentSession.gjcSessionId;
1389
+
1390
+ const owners = await findExistingRalplanRunOwners(cwd, explicitRunId);
1391
+ if (owners.length === 0) return currentSession.gjcSessionId;
1392
+ if (owners.length > 1) {
1393
+ throw new RalplanCommandError(
1394
+ 2,
1395
+ `ralplan run ${explicitRunId} has multiple owner sessions (${owners.join(", ")}); repair the fragmented run before writing`,
1396
+ );
1397
+ }
1398
+
1399
+ const ownerSessionId = owners[0]!;
1400
+ if (flagSessionId !== undefined && currentSession.gjcSessionId !== ownerSessionId) {
1401
+ throw new RalplanCommandError(
1402
+ 2,
1403
+ `ralplan run ${explicitRunId} is owned by session ${ownerSessionId}, not ${currentSession.gjcSessionId}`,
1404
+ );
1405
+ }
1406
+ return ownerSessionId;
1407
+ }
1097
1408
 
1098
1409
  async function resolveArtifactArgs(args: readonly string[], cwd: string): Promise<ResolvedArtifactArgs> {
1099
1410
  const stage = flagValue(args, "--stage");
@@ -1115,20 +1426,18 @@ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promis
1115
1426
  throw new RalplanCommandError(2, `--artifact-env must be ${GJC_RALPLAN_ARTIFACT_ENV}`);
1116
1427
  }
1117
1428
 
1118
- const session = resolveGjcSessionForWrite(cwd, {
1119
- flagValue: flagValue(args, "--session-id"),
1120
- envSessionId: process.env.GJC_SESSION_ID,
1121
- });
1122
- const sessionId = session.gjcSessionId;
1429
+ const explicitRunId = flagValue(args, "--run-id")?.trim();
1430
+ if (explicitRunId) assertSafePathComponent(explicitRunId, "run-id");
1431
+
1432
+ const sessionId = await resolveArtifactSessionId(args, cwd, explicitRunId);
1123
1433
  assertSafePathComponent(sessionId, "session-id");
1124
1434
  const sessionIdRaw = sessionId;
1125
1435
 
1126
1436
  // Precedence for run_id:
1127
1437
  // 1. explicit --run-id flag
1128
- // 2. existing run_id field in .gjc/state[/sessions/<id>]/ralplan-state.json
1129
- // 3. explicit --session-id flag (use as run id)
1438
+ // 2. existing run_id field in the resolved owner session's ralplan state
1439
+ // 3. resolved owner session id
1130
1440
  // 4. freshly generated default run id
1131
- const explicitRunId = flagValue(args, "--run-id")?.trim();
1132
1441
  const runId = explicitRunId || (await readActiveRunId(cwd, sessionId)) || sessionIdRaw || defaultRunId();
1133
1442
  assertSafePathComponent(runId, "run-id");
1134
1443
 
@@ -1170,6 +1479,7 @@ async function persistArtifact(
1170
1479
  cwd: string,
1171
1480
  content: string,
1172
1481
  sha256: string,
1482
+ finalAdmission?: RalplanAutoHandoffResolution,
1173
1483
  ): Promise<PersistedArtifact> {
1174
1484
  const runDir = path.join(sessionPlansDir(cwd, resolved.sessionId), "ralplan", resolved.runId);
1175
1485
 
@@ -1193,6 +1503,7 @@ async function persistArtifact(
1193
1503
  path: filePath,
1194
1504
  created_at: createdAt,
1195
1505
  sha256,
1506
+ ...(finalAdmission ? { auto_handoff: finalAdmission } : {}),
1196
1507
  };
1197
1508
  await appendJsonlIdempotent(path.join(runDir, "index.jsonl"), indexEntry, {
1198
1509
  cwd,
@@ -1237,6 +1548,35 @@ interface ExistingStageArtifact {
1237
1548
  path: string;
1238
1549
  sha256: string;
1239
1550
  createdAt: string;
1551
+ autoHandoff?: RalplanAutoHandoffResolution;
1552
+ }
1553
+
1554
+ function parseRalplanFinalAdmission(value: unknown): RalplanAutoHandoffResolution | undefined {
1555
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
1556
+ const admission = value as Record<string, unknown>;
1557
+ if (
1558
+ !RALPLAN_AUTO_HANDOFF_TARGETS.has(admission.configuredTarget as RalplanAutoHandoffTarget) ||
1559
+ !RALPLAN_AUTO_HANDOFF_TARGETS.has(admission.effectiveTarget as RalplanAutoHandoffTarget) ||
1560
+ (typeof admission.degradationReason !== "string" && admission.degradationReason !== null) ||
1561
+ typeof admission.source !== "string"
1562
+ ) {
1563
+ return undefined;
1564
+ }
1565
+ return {
1566
+ configuredTarget: admission.configuredTarget as RalplanAutoHandoffTarget,
1567
+ effectiveTarget: admission.effectiveTarget as RalplanAutoHandoffTarget,
1568
+ degradationReason: typeof admission.degradationReason === "string" ? admission.degradationReason : null,
1569
+ source: admission.source,
1570
+ };
1571
+ }
1572
+
1573
+ function unavailableRalplanFinalAdmission(): RalplanAutoHandoffResolution {
1574
+ return {
1575
+ configuredTarget: "off",
1576
+ effectiveTarget: "off",
1577
+ degradationReason: "admission_unavailable",
1578
+ source: "ledger",
1579
+ };
1240
1580
  }
1241
1581
 
1242
1582
  /**
@@ -1269,6 +1609,7 @@ function findExistingStageArtifact(
1269
1609
  path: record.path,
1270
1610
  sha256: record.sha256,
1271
1611
  createdAt: typeof record.created_at === "string" ? record.created_at : "",
1612
+ ...(stage === "final" ? { autoHandoff: parseRalplanFinalAdmission(record.auto_handoff) } : {}),
1272
1613
  };
1273
1614
  }
1274
1615
  return match;
@@ -1354,6 +1695,7 @@ async function repairMissingStageArtifactLedger(
1354
1695
  cwd: string,
1355
1696
  resolved: Pick<ResolvedArtifactArgs, "sessionId" | "runId" | "stage" | "stageN">,
1356
1697
  onDisk: OnDiskStageArtifact,
1698
+ finalAdmission?: RalplanAutoHandoffResolution,
1357
1699
  ): Promise<ExistingStageArtifact> {
1358
1700
  const createdAt = new Date().toISOString();
1359
1701
  const indexEntry = {
@@ -1362,6 +1704,7 @@ async function repairMissingStageArtifactLedger(
1362
1704
  path: onDisk.path,
1363
1705
  created_at: createdAt,
1364
1706
  sha256: onDisk.sha256,
1707
+ ...(finalAdmission ? { auto_handoff: finalAdmission } : {}),
1365
1708
  };
1366
1709
  const result = await appendJsonlIdempotent(
1367
1710
  path.join(sessionPlansDir(cwd, resolved.sessionId), "ralplan", resolved.runId, "index.jsonl"),
@@ -1386,10 +1729,18 @@ async function repairMissingStageArtifactLedger(
1386
1729
  path: record.path,
1387
1730
  sha256: record.sha256,
1388
1731
  createdAt: typeof record.created_at === "string" ? record.created_at : createdAt,
1732
+ ...(resolved.stage === "final"
1733
+ ? { autoHandoff: parseRalplanFinalAdmission(record.auto_handoff) ?? unavailableRalplanFinalAdmission() }
1734
+ : {}),
1389
1735
  };
1390
1736
  }
1391
1737
  }
1392
- return { path: onDisk.path, sha256: onDisk.sha256, createdAt };
1738
+ return {
1739
+ path: onDisk.path,
1740
+ sha256: onDisk.sha256,
1741
+ createdAt,
1742
+ ...(resolved.stage === "final" ? { autoHandoff: finalAdmission ?? unavailableRalplanFinalAdmission() } : {}),
1743
+ };
1393
1744
  }
1394
1745
 
1395
1746
  /**
@@ -1463,12 +1814,18 @@ async function buildRalplanHud(options: {
1463
1814
  let architectPasses: number | undefined;
1464
1815
  let criticPasses: number | undefined;
1465
1816
  let verdict: string | undefined;
1817
+ let autoHandoff: RalplanAutoHandoffResolution | undefined;
1818
+ let planningStuck = false;
1466
1819
  if (options.runId && options.sessionId) {
1467
- const [rows, lastReviewVerdict] = await Promise.all([
1820
+ const [rows, lastReviewVerdict, persistedAutoHandoff, persistedPlanningStuck] = await Promise.all([
1468
1821
  readRalplanIndexRows(options.cwd, options.sessionId, options.runId),
1469
1822
  readRalplanLastReviewVerdict(options.cwd, options.sessionId),
1823
+ readRalplanFinalAdmission(options.cwd, options.sessionId, options.runId),
1824
+ readRalplanPlanningStuck(options.cwd, options.sessionId, options.runId),
1470
1825
  ]);
1471
1826
  verdict = lastReviewVerdict;
1827
+ autoHandoff = persistedAutoHandoff;
1828
+ planningStuck = persistedPlanningStuck;
1472
1829
  if (rows.length > 0) {
1473
1830
  const summary = summarizeRalplanIndex(rows);
1474
1831
  iterationFromIndex = summary.iteration;
@@ -1486,6 +1843,8 @@ async function buildRalplanHud(options: {
1486
1843
  criticPasses,
1487
1844
  reviewPassBudget: options.reviewPassBudget,
1488
1845
  verdict,
1846
+ autoHandoff,
1847
+ planningStuck,
1489
1848
  pendingApproval: options.pendingApproval,
1490
1849
  latestSummary: options.latestSummary,
1491
1850
  updatedAt: new Date().toISOString(),
@@ -1534,7 +1893,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
1534
1893
  );
1535
1894
  }
1536
1895
  if (resolved.stage === "final") await ensureFinalPendingApproval(cwd, resolved, existingArtifact);
1537
- return buildDeduplicatedResult(resolved, existingArtifact, sha256, cwd, repositoryBinding);
1896
+ return await buildDeduplicatedResult(resolved, existingArtifact, sha256, cwd, repositoryBinding);
1538
1897
  }
1539
1898
 
1540
1899
  // `persistArtifact` writes the artifact before its ledger row. If a process crashed
@@ -1549,7 +1908,12 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
1549
1908
  );
1550
1909
  }
1551
1910
  if (resolved.stage === "final") await ensureFinalPendingApproval(cwd, resolved, onDiskArtifact);
1552
- const repairedArtifact = await repairMissingStageArtifactLedger(cwd, resolved, onDiskArtifact);
1911
+ const repairedArtifact = await repairMissingStageArtifactLedger(
1912
+ cwd,
1913
+ resolved,
1914
+ onDiskArtifact,
1915
+ resolved.stage === "final" ? unavailableRalplanFinalAdmission() : undefined,
1916
+ );
1553
1917
  let appliedPersistedRoleState: PersistedRoleStateUpdate | undefined;
1554
1918
  if (
1555
1919
  persistedRoleState &&
@@ -1561,7 +1925,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
1561
1925
  if (laneVerdict && (await applyLaneVerdictUpdate(cwd, resolved.sessionId, laneVerdict, resolved.runId))) {
1562
1926
  appliedLaneVerdict = laneVerdict;
1563
1927
  }
1564
- return buildDeduplicatedResult(
1928
+ return await buildDeduplicatedResult(
1565
1929
  resolved,
1566
1930
  repairedArtifact,
1567
1931
  sha256,
@@ -1595,6 +1959,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
1595
1959
  iterationFloor: onDiskOpeners,
1596
1960
  });
1597
1961
  if (!capDecision.allowed) {
1962
+ await recordRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId, capDecision.reason);
1598
1963
  return buildPlanningStuckResult({
1599
1964
  json: resolved.json,
1600
1965
  stage: resolved.stage,
@@ -1611,6 +1976,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
1611
1976
  onDiskLaneCounts: onDiskLaneArtifacts,
1612
1977
  });
1613
1978
  if (!laneBudgetDecision.allowed) {
1979
+ await recordRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId, laneBudgetDecision.reason);
1614
1980
  return buildLaneBudgetStuckResult({
1615
1981
  json: resolved.json,
1616
1982
  stage: resolved.stage,
@@ -1621,15 +1987,27 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
1621
1987
  });
1622
1988
  }
1623
1989
 
1990
+ let autoHandoff: RalplanAutoHandoffResolution | undefined;
1991
+ if (resolved.stage === "final") {
1992
+ // Resolve and validate the configured admission before any final artifact or
1993
+ // state write. The ledger row below is the durable receipt for deduplicated
1994
+ // retries; state is only a current-session projection.
1995
+ autoHandoff = await resolveRalplanAutoHandoff(cwd, {
1996
+ planningStuck: await readRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId),
1997
+ });
1998
+ }
1624
1999
  // Keep run-state `current_phase` coherent with the stage being persisted.
1625
2000
  await persistActiveRunId(cwd, resolved.sessionId, resolved.runId, resolved.stage);
1626
- const persisted = await persistArtifact(resolved, cwd, content, sha256);
2001
+ const persisted = await persistArtifact(resolved, cwd, content, sha256, autoHandoff);
1627
2002
  if (persistedRoleState) {
1628
2003
  await applyPersistedRoleStateUpdate(cwd, resolved.sessionId, persistedRoleState);
1629
2004
  }
1630
2005
  if (laneVerdict) {
1631
2006
  await applyLaneVerdictUpdate(cwd, resolved.sessionId, laneVerdict);
1632
2007
  }
2008
+ if (autoHandoff) {
2009
+ await persistRalplanFinalAdmission(cwd, resolved.sessionId, resolved.runId, autoHandoff);
2010
+ }
1633
2011
  await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "ralplan-runtime", path: persisted.path });
1634
2012
  await syncRalplanHud({
1635
2013
  cwd,
@@ -1651,6 +2029,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
1651
2029
  : undefined;
1652
2030
 
1653
2031
  const payload: Record<string, unknown> = {
2032
+ session_id: resolved.sessionId,
1654
2033
  run_id: persisted.runId,
1655
2034
  path: persisted.path,
1656
2035
  stage: persisted.stage,
@@ -1663,6 +2042,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
1663
2042
  if (persistedRoleState) payload[`${persistedRoleState.role}_state`] = persistedRoleStatePayload(persistedRoleState);
1664
2043
  if (reviewBudgetWarning) payload.review_budget_warning = reviewBudgetWarning;
1665
2044
  if (laneVerdict) payload.lane_verdict = { lane: laneVerdict.lane, verdict: laneVerdict.verdict };
2045
+ if (autoHandoff) payload.auto_handoff = autoHandoff;
1666
2046
 
1667
2047
  const stdout = resolved.json
1668
2048
  ? `${JSON.stringify(payload, null, 2)}\n`
@@ -1675,7 +2055,14 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
1675
2055
  * do not rewrite artifacts, append rows, or churn run state; a crash-gap repair may
1676
2056
  * complete riding persisted role/lane metadata before returning this receipt.
1677
2057
  */
1678
- function buildDeduplicatedResult(
2058
+ function applyRalplanPlanningStuckOverride(
2059
+ admission: RalplanAutoHandoffResolution,
2060
+ planningStuck: boolean,
2061
+ ): RalplanAutoHandoffResolution {
2062
+ return planningStuck ? { ...admission, effectiveTarget: "off", degradationReason: "planning_stuck" } : admission;
2063
+ }
2064
+
2065
+ async function buildDeduplicatedResult(
1679
2066
  resolved: ResolvedArtifactArgs,
1680
2067
  existing: ExistingStageArtifact,
1681
2068
  sha256: string,
@@ -1683,8 +2070,9 @@ function buildDeduplicatedResult(
1683
2070
  repositoryBinding: RepositoryBinding,
1684
2071
  laneVerdict?: LaneVerdictUpdate,
1685
2072
  persistedRoleState?: PersistedRoleStateUpdate,
1686
- ): RalplanCommandResult {
2073
+ ): Promise<RalplanCommandResult> {
1687
2074
  const payload: Record<string, unknown> = {
2075
+ session_id: resolved.sessionId,
1688
2076
  run_id: resolved.runId,
1689
2077
  path: existing.path,
1690
2078
  stage: resolved.stage,
@@ -1705,6 +2093,11 @@ function buildDeduplicatedResult(
1705
2093
  resolved.runId,
1706
2094
  "pending-approval.md",
1707
2095
  );
2096
+ const planningStuck = await readRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId);
2097
+ payload.auto_handoff = applyRalplanPlanningStuckOverride(
2098
+ existing.autoHandoff ?? unavailableRalplanFinalAdmission(),
2099
+ planningStuck,
2100
+ );
1708
2101
  }
1709
2102
  const stdout = resolved.json
1710
2103
  ? `${JSON.stringify(payload, null, 2)}\n`
@@ -1851,6 +2244,7 @@ async function handleConsensusHandoff(args: readonly string[], cwd: string): Pro
1851
2244
  });
1852
2245
 
1853
2246
  const summary = {
2247
+ session_id: resolved.sessionId,
1854
2248
  skill: "ralplan",
1855
2249
  mode,
1856
2250
  state_path: statePath,
@@ -36,6 +36,7 @@ export function buildWorkerCommand(
36
36
  worker: GjcTeamWorker,
37
37
  platform: NodeJS.Platform = process.platform,
38
38
  promptOverride?: string,
39
+ env: NodeJS.ProcessEnv = process.env,
39
40
  ): string {
40
41
  const quote = platform === "win32" ? powershellQuote : shellQuote;
41
42
  const envAssignment = (key: string, value: string): string =>
@@ -74,6 +75,13 @@ export function buildWorkerCommand(
74
75
  "GJC_TEAM_WORKER_MEMORY_GUARD_PATH",
75
76
  workerMemoryGuardLedgerPath(path.join(config.state_root, config.team_name), worker.id),
76
77
  ),
78
+ // The worker derives its heartbeat cadence from the same window the leader
79
+ // enforces. tmux panes do not inherit the launching shell's environment, so
80
+ // without this a tightened window would leave workers publishing on the
81
+ // default cadence and reported stale while they are working.
82
+ ...(env.GJC_TEAM_HEARTBEAT_STALE_MS?.trim()
83
+ ? [envAssignment("GJC_TEAM_HEARTBEAT_STALE_MS", env.GJC_TEAM_HEARTBEAT_STALE_MS.trim())]
84
+ : []),
77
85
  ];
78
86
  const joined = envLines.join(" ");
79
87
  const clearInheritedSession = config.gjc_session_id