@gajae-code/coding-agent 0.18.7 → 0.18.8

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 (279) hide show
  1. package/CHANGELOG.md +279 -1
  2. package/dist/types/async/job-manager.d.ts +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/capability/skill.d.ts +2 -0
  5. package/dist/types/capability/types.d.ts +2 -0
  6. package/dist/types/cli/public-command-errors.d.ts +3 -1
  7. package/dist/types/cli/setup-cli.d.ts +1 -1
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  10. package/dist/types/config/file-lock.d.ts +1 -0
  11. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  12. package/dist/types/config/model-profile-activation.d.ts +11 -3
  13. package/dist/types/config/model-registry.d.ts +4 -9
  14. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  15. package/dist/types/cursor-delete-path.d.ts +1 -0
  16. package/dist/types/cursor-mcp-name.d.ts +8 -0
  17. package/dist/types/customization/inventory.d.ts +2 -0
  18. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  21. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  22. package/dist/types/discovery/helpers.d.ts +6 -1
  23. package/dist/types/export/html/css-color.d.ts +2 -0
  24. package/dist/types/export/html/template.generated.d.ts +1 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  26. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  27. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  28. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  29. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  30. package/dist/types/extensibility/skill-management.d.ts +3 -1
  31. package/dist/types/extensibility/skills.d.ts +2 -0
  32. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  33. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  34. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  36. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  37. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  38. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  39. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  40. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  41. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  42. package/dist/types/lsp/workspace-path.d.ts +1 -0
  43. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  44. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  45. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  46. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  47. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  48. package/dist/types/modes/interactive-mode.d.ts +1 -0
  49. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  50. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  51. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  52. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  53. package/dist/types/progress/progress-contract.d.ts +154 -0
  54. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  55. package/dist/types/progress/project-progress.d.ts +141 -0
  56. package/dist/types/progress/render-progress.d.ts +28 -0
  57. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  58. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  59. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  60. package/dist/types/runtime-mcp/types.d.ts +6 -0
  61. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  62. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  63. package/dist/types/sdk/broker/broker.d.ts +18 -1
  64. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  65. package/dist/types/sdk/bus/index.d.ts +12 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  67. package/dist/types/sdk/client/client.d.ts +2 -0
  68. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  69. package/dist/types/sdk/index.d.ts +1 -0
  70. package/dist/types/sdk/router/session-router.d.ts +2 -0
  71. package/dist/types/sdk/session-list.d.ts +1 -1
  72. package/dist/types/sdk/session.d.ts +8 -0
  73. package/dist/types/session/agent-session.d.ts +38 -9
  74. package/dist/types/session/artifacts.d.ts +2 -2
  75. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  76. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  77. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  78. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  79. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  80. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  81. package/dist/types/session/session-manager.d.ts +31 -5
  82. package/dist/types/session/session-retirement.d.ts +31 -7
  83. package/dist/types/session/session-storage.d.ts +62 -2
  84. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  85. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  86. package/dist/types/setup/credential-import.d.ts +6 -4
  87. package/dist/types/skill-state/active-state.d.ts +38 -0
  88. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  89. package/dist/types/task/executor.d.ts +3 -0
  90. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  91. package/dist/types/tools/index.d.ts +3 -0
  92. package/dist/types/tools/python.d.ts +9 -1
  93. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  94. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  95. package/dist/types/tools/todo-write.d.ts +6 -0
  96. package/dist/types/utils/tools-checksum.d.ts +7 -0
  97. package/package.json +7 -7
  98. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  99. package/scripts/verify-managed-empty-stop.ts +28 -0
  100. package/src/async/job-manager.ts +228 -122
  101. package/src/capability/index.ts +17 -0
  102. package/src/capability/mcp.ts +2 -0
  103. package/src/capability/skill.ts +2 -0
  104. package/src/capability/types.ts +2 -0
  105. package/src/cli/auth-broker-cli.ts +1 -1
  106. package/src/cli/customize-doctor.ts +15 -2
  107. package/src/cli/public-command-errors.ts +21 -1
  108. package/src/cli/setup-cli.ts +4 -4
  109. package/src/cli/skills-cli.ts +5 -0
  110. package/src/commands/deep-interview.ts +47 -8
  111. package/src/commands/sdk.ts +28 -8
  112. package/src/config/autorouting-tier-map.ts +117 -7
  113. package/src/config/file-lock.ts +307 -68
  114. package/src/config/mcp-schema.json +4 -0
  115. package/src/config/model-bindings-applier.ts +20 -0
  116. package/src/config/model-profile-activation.ts +240 -46
  117. package/src/config/model-registry.ts +47 -153
  118. package/src/config/skill-settings-defaults.ts +7 -0
  119. package/src/coordinator-mcp/policy.ts +3 -2
  120. package/src/cursor-delete-path.ts +10 -0
  121. package/src/cursor-mcp-name.ts +15 -0
  122. package/src/cursor.ts +13 -2
  123. package/src/customization/inventory.ts +3 -0
  124. package/src/debug/report-bundle-parent.ts +22 -0
  125. package/src/debug/report-bundle.ts +3 -0
  126. package/src/debug/system-info.ts +13 -1
  127. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  128. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  129. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  130. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  131. package/src/defaults/gjc-defaults.test.ts +18 -1
  132. package/src/defaults/gjc-defaults.ts +3 -0
  133. package/src/defaults/gjc-skills.generated.ts +58 -36
  134. package/src/discovery/agents.ts +6 -1
  135. package/src/discovery/builtin.ts +10 -0
  136. package/src/discovery/helpers.ts +159 -20
  137. package/src/discovery/mcp-json.ts +6 -0
  138. package/src/discovery/ssh.ts +1 -1
  139. package/src/discovery/substitute-plugin-root.ts +3 -1
  140. package/src/edit/file-read-cache.ts +1 -1
  141. package/src/eval/py/executor.ts +380 -120
  142. package/src/export/html/css-color.ts +9 -0
  143. package/src/export/html/index.ts +5 -4
  144. package/src/export/html/template.generated.ts +1 -1
  145. package/src/extensibility/extensions/runner.ts +4 -0
  146. package/src/extensibility/extensions/types.ts +7 -0
  147. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  148. package/src/extensibility/plugins/manager.ts +3 -2
  149. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  150. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  151. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  152. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  153. package/src/extensibility/skill-management.ts +6 -2
  154. package/src/extensibility/skills.ts +6 -1
  155. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  156. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  157. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  158. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  159. package/src/gjc-runtime/gc-runtime.ts +206 -9
  160. package/src/gjc-runtime/launch-worktree.ts +14 -4
  161. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  162. package/src/gjc-runtime/state-argv.ts +2 -0
  163. package/src/gjc-runtime/state-migrations.ts +91 -29
  164. package/src/gjc-runtime/state-runtime.ts +3716 -135
  165. package/src/gjc-runtime/state-writer.ts +83 -8
  166. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  167. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  168. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  169. package/src/harness-control-plane/operate.ts +12 -4
  170. package/src/harness-control-plane/owner.ts +5 -1
  171. package/src/hashline/parser.ts +48 -9
  172. package/src/hashline/recovery.ts +13 -4
  173. package/src/internal-urls/docs-index.generated.ts +18 -17
  174. package/src/internal-urls/skill-path.ts +19 -0
  175. package/src/internal-urls/skill-protocol.ts +2 -0
  176. package/src/lsp/edits.ts +6 -0
  177. package/src/lsp/workspace-path.ts +51 -0
  178. package/src/main.ts +195 -31
  179. package/src/modes/acp/acp-agent.ts +88 -4
  180. package/src/modes/components/custom-editor.ts +14 -0
  181. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  182. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  183. package/src/modes/components/extensions/state-manager.ts +15 -5
  184. package/src/modes/components/model-selector.ts +1 -4
  185. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  186. package/src/modes/components/status-line/segments.ts +4 -0
  187. package/src/modes/controllers/event-controller.ts +11 -4
  188. package/src/modes/controllers/input-controller.ts +74 -29
  189. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  190. package/src/modes/controllers/selector-controller.ts +28 -15
  191. package/src/modes/interactive-mode.ts +4 -0
  192. package/src/modes/runtime-init.ts +4 -0
  193. package/src/modes/utils/abort-message.ts +3 -1
  194. package/src/modes/utils/interrupt-ownership.ts +12 -0
  195. package/src/modes/utils/keybinding-matchers.ts +4 -0
  196. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  197. package/src/progress/collect-project-progress.ts +153 -0
  198. package/src/progress/progress-contract.ts +172 -0
  199. package/src/progress/progress-snapshot.ts +140 -0
  200. package/src/progress/project-progress.ts +446 -0
  201. package/src/progress/render-progress.ts +209 -0
  202. package/src/prompts/system/master-mode.md +1 -0
  203. package/src/prompts/tools/skill-discovery.md +1 -1
  204. package/src/runtime-mcp/config-writer.ts +10 -6
  205. package/src/runtime-mcp/config.ts +1 -0
  206. package/src/runtime-mcp/oauth-flow.ts +5 -0
  207. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  208. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  209. package/src/runtime-mcp/smithery-registry.ts +22 -3
  210. package/src/runtime-mcp/transports/http.ts +2 -2
  211. package/src/runtime-mcp/types.ts +6 -0
  212. package/src/sdk/acp/adapter.ts +93 -8
  213. package/src/sdk/broker/broker-exit.ts +52 -23
  214. package/src/sdk/broker/broker.ts +75 -6
  215. package/src/sdk/broker/ensure.ts +2 -0
  216. package/src/sdk/broker/lifecycle.ts +711 -66
  217. package/src/sdk/broker/session-index.ts +191 -81
  218. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  219. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  220. package/src/sdk/bus/index.ts +238 -43
  221. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  222. package/src/sdk/cli/session-cli.ts +17 -3
  223. package/src/sdk/client/client.ts +28 -8
  224. package/src/sdk/host/query/handlers.ts +9 -1
  225. package/src/sdk/host/session-runtime.test.ts +2647 -294
  226. package/src/sdk/host/session-runtime.ts +426 -109
  227. package/src/sdk/host/surface-policy.ts +1 -0
  228. package/src/sdk/index.ts +8 -0
  229. package/src/sdk/lifecycle-session.ts +3 -1
  230. package/src/sdk/prompt-deadline-manager.ts +15 -0
  231. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  232. package/src/sdk/protocol/operation-registry.ts +9 -2
  233. package/src/sdk/router/session-router.ts +75 -12
  234. package/src/sdk/session-list.ts +1 -0
  235. package/src/sdk/session.ts +67 -38
  236. package/src/session/agent-session.ts +623 -224
  237. package/src/session/artifacts.ts +174 -70
  238. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  239. package/src/session/internal/managed-session-scope.ts +3457 -462
  240. package/src/session/internal/managed-session-storage.ts +376 -61
  241. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  242. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  243. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  244. package/src/session/session-manager.ts +939 -133
  245. package/src/session/session-retirement.ts +573 -14
  246. package/src/session/session-storage.ts +417 -14
  247. package/src/session/task-artifact-owner-codec.ts +71 -8
  248. package/src/session/task-artifact-owner-retirement.ts +21 -19
  249. package/src/setup/credential-auto-import.ts +12 -3
  250. package/src/setup/credential-import.ts +176 -5
  251. package/src/setup/paseo/announce.ts +6 -1
  252. package/src/skill-state/active-state.ts +132 -2
  253. package/src/slash-commands/builtin-registry.ts +39 -9
  254. package/src/slash-commands/helpers/progress-report.ts +23 -0
  255. package/src/stt/setup.ts +3 -5
  256. package/src/task/commands.ts +3 -1
  257. package/src/task/executor.ts +4 -0
  258. package/src/task/index.ts +6 -0
  259. package/src/tools/ask.ts +205 -6
  260. package/src/tools/ast-edit.ts +4 -0
  261. package/src/tools/bash-allowed-prefixes.ts +15 -0
  262. package/src/tools/bash-skill-urls.ts +56 -3
  263. package/src/tools/bash.ts +48 -79
  264. package/src/tools/descriptors.ts +5 -1
  265. package/src/tools/eval.ts +56 -36
  266. package/src/tools/image-gen.ts +1 -1
  267. package/src/tools/index.ts +3 -0
  268. package/src/tools/python.ts +187 -66
  269. package/src/tools/recipe/index.ts +9 -0
  270. package/src/tools/recipe/recipe-guard.ts +19 -0
  271. package/src/tools/restricted-cwd.ts +10 -0
  272. package/src/tools/skill-discovery.ts +5 -0
  273. package/src/tools/skill.ts +28 -1
  274. package/src/tools/todo-write.ts +10 -1
  275. package/src/tools/tool-catalog.generated.ts +1 -1
  276. package/src/tools/write.ts +1 -0
  277. package/src/utils/tools-checksum.ts +34 -0
  278. package/src/utils/tools-manager.ts +24 -6
  279. package/src/web/search/providers/codex.ts +3 -2
@@ -1,4 +1,4 @@
1
- import { createHash, randomBytes } from "node:crypto";
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
  import type { Dirent } from "node:fs";
3
3
  import * as fssync from "node:fs";
4
4
  import * as fs from "node:fs/promises";
@@ -36,10 +36,14 @@ import { resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session
36
36
  import { migrateWorkflowState } from "./state-migrations";
37
37
  import { runNativeStateCommand } from "./state-runtime";
38
38
  import {
39
- appendJsonlIdempotent,
39
+ type AppendJsonlIdempotentOptions,
40
+ type AppendJsonlIdempotentResult,
41
+ appendJsonl,
42
+ detectWorkflowEnvelopeIntegrityMismatch,
40
43
  readExistingStateForMutation,
41
44
  withWorkflowStateLock,
42
45
  writeArtifact,
46
+ writeTextAtomic,
43
47
  writeWorkflowEnvelopeAtomic,
44
48
  } from "./state-writer";
45
49
  import { assertSafePathComponent, CommandError, flagValue, hasFlag } from "./workflow-cli-common";
@@ -347,6 +351,53 @@ export async function countRalplanOnDiskLaneArtifacts(
347
351
  * signals so callers can fail closed when the ledger is empty/malformed while
348
352
  * opener artifacts already exist on disk.
349
353
  */
354
+ async function readRalplanIndexBounded(indexPath: string): Promise<string | undefined> {
355
+ let lexical: fssync.BigIntStats;
356
+ try {
357
+ lexical = await fs.lstat(indexPath, { bigint: true });
358
+ } catch (error) {
359
+ if (getErrorCode(error) === "ENOENT") return undefined;
360
+ throw error;
361
+ }
362
+ if (lexical.isSymbolicLink() || !lexical.isFile() || lexical.size > BigInt(RALPLAN_MAX_INDEX_RECOVERY_BYTES))
363
+ throw new RalplanCommandError(2, "ralplan index exceeds its bounded recovery limit");
364
+ const flags = fssync.constants.O_RDONLY | (process.platform === "win32" ? 0 : (fssync.constants.O_NOFOLLOW ?? 0));
365
+ let handle: fs.FileHandle | undefined;
366
+ try {
367
+ handle = await fs.open(indexPath, flags);
368
+ const opened = await handle.stat({ bigint: true });
369
+ const same = (left: fssync.BigIntStats, right: fssync.BigIntStats) =>
370
+ left.dev === right.dev &&
371
+ left.ino === right.ino &&
372
+ left.mode === right.mode &&
373
+ left.size === right.size &&
374
+ left.mtimeNs === right.mtimeNs &&
375
+ left.ctimeNs === right.ctimeNs &&
376
+ left.nlink === right.nlink;
377
+ if (!opened.isFile() || !same(lexical, opened))
378
+ throw new RalplanCommandError(2, "ralplan index identity changed");
379
+ const buffer = Buffer.alloc(Number(opened.size));
380
+ let offset = 0;
381
+ while (offset < buffer.length) {
382
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
383
+ if (bytesRead === 0) throw new RalplanCommandError(2, "ralplan index changed during bounded read");
384
+ offset += bytesRead;
385
+ }
386
+ const afterDescriptor = await handle.stat({ bigint: true });
387
+ const afterPath = await fs.lstat(indexPath, { bigint: true });
388
+ if (
389
+ afterPath.isSymbolicLink() ||
390
+ !afterPath.isFile() ||
391
+ !same(opened, afterDescriptor) ||
392
+ !same(opened, afterPath)
393
+ )
394
+ throw new RalplanCommandError(2, "ralplan index changed during bounded read");
395
+ return buffer.toString("utf8");
396
+ } finally {
397
+ await handle?.close();
398
+ }
399
+ }
400
+
350
401
  export async function loadRalplanIndexForCap(
351
402
  cwd: string,
352
403
  sessionId: string,
@@ -360,7 +411,8 @@ export async function loadRalplanIndexForCap(
360
411
  }> {
361
412
  const indexPath = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId, "index.jsonl");
362
413
  try {
363
- const text = await fs.readFile(indexPath, "utf8");
414
+ const text = await readRalplanIndexBounded(indexPath);
415
+ if (text === undefined) return { rows: [], indexPresent: false, parseableLines: 0, rawLineCount: 0 };
364
416
  const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0);
365
417
  const rows: RalplanIndexRow[] = [];
366
418
  for (const line of lines) {
@@ -1010,6 +1062,13 @@ async function persistActiveRunId(cwd: string, sessionId: string, runId: string,
1010
1062
  }
1011
1063
  delete existing.planning_stuck;
1012
1064
  delete existing.auto_handoff;
1065
+ // Initial run establishment retains the incoming handoff; only a replacement run severs it.
1066
+ if (existing.run_id !== undefined) {
1067
+ delete existing.handoff_from;
1068
+ delete existing.handoff_at;
1069
+ delete existing.upstream_handoff_at;
1070
+ }
1071
+ delete existing.final_admission_phase_transition;
1013
1072
  }
1014
1073
  if (
1015
1074
  existing.run_id === runId &&
@@ -1354,7 +1413,7 @@ async function recordRalplanPlanningStuck(
1354
1413
  reason: string,
1355
1414
  ): Promise<void> {
1356
1415
  const runDir = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId);
1357
- await appendJsonlIdempotent(
1416
+ await appendRalplanIndexIdempotent(
1358
1417
  path.join(runDir, "index.jsonl"),
1359
1418
  {
1360
1419
  event: "planning_stuck",
@@ -1439,6 +1498,7 @@ async function persistRalplanFinalAdmission(
1439
1498
  sessionId: string,
1440
1499
  runId: string,
1441
1500
  admission: RalplanAutoHandoffResolution,
1501
+ publication: { id: string; sha256: string },
1442
1502
  ): Promise<void> {
1443
1503
  const statePath = ralplanStatePath(cwd, sessionId);
1444
1504
  await withWorkflowStateLock(
@@ -1453,7 +1513,64 @@ async function persistRalplanFinalAdmission(
1453
1513
  }
1454
1514
  let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
1455
1515
  if (existing.run_id !== runId) return;
1516
+ const phase = typeof existing.current_phase === "string" ? existing.current_phase.trim() : "";
1517
+ if (getSkillManifest("ralplan").phaseLock.includes(phase) && (phase !== "final" || existing.active === false))
1518
+ return;
1519
+ const pending =
1520
+ existing.final_publication_pending &&
1521
+ typeof existing.final_publication_pending === "object" &&
1522
+ !Array.isArray(existing.final_publication_pending)
1523
+ ? (existing.final_publication_pending as Record<string, unknown>)
1524
+ : undefined;
1525
+ if (
1526
+ pending?.publication_id !== publication.id ||
1527
+ pending.final_sha256 !== publication.sha256 ||
1528
+ pending.run_id !== runId
1529
+ )
1530
+ throw new RalplanCommandError(2, "final publication was superseded by a concurrent writer");
1531
+ const index = await loadRalplanIndexForCap(cwd, sessionId, runId);
1532
+ let lastFinalSha: string | undefined;
1533
+ for (const line of index.rawText?.split(/\r?\n/) ?? []) {
1534
+ if (!line.trim()) continue;
1535
+ try {
1536
+ const row = JSON.parse(line) as Record<string, unknown>;
1537
+ if (row.stage === "final" && typeof row.sha256 === "string") lastFinalSha = row.sha256;
1538
+ } catch {
1539
+ throw new RalplanCommandError(2, "final publication index is malformed");
1540
+ }
1541
+ }
1542
+ if (lastFinalSha !== publication.sha256)
1543
+ throw new RalplanCommandError(2, "final publication index does not match the active writer");
1544
+ const currentAdmission = parseRalplanFinalAdmission(existing.auto_handoff);
1545
+ const receipt =
1546
+ existing.receipt && typeof existing.receipt === "object" && !Array.isArray(existing.receipt)
1547
+ ? (existing.receipt as Record<string, unknown>)
1548
+ : undefined;
1549
+ const checksum =
1550
+ receipt?.content_sha256 &&
1551
+ typeof receipt.content_sha256 === "object" &&
1552
+ !Array.isArray(receipt.content_sha256)
1553
+ ? (receipt.content_sha256 as Record<string, unknown>)
1554
+ : undefined;
1555
+ if (
1556
+ currentAdmission &&
1557
+ existing.final_publication_pending === undefined &&
1558
+ JSON.stringify(currentAdmission) === JSON.stringify(admission) &&
1559
+ receipt?.skill === "ralplan" &&
1560
+ receipt.owner === "gjc-runtime" &&
1561
+ receipt.command === "gjc ralplan final-admission" &&
1562
+ checksum?.algorithm === "sha256" &&
1563
+ typeof checksum.value === "string" &&
1564
+ /^[0-9a-f]{64}$/.test(checksum.value) &&
1565
+ checksum.covered_path === path.resolve(statePath) &&
1566
+ typeof checksum.computed_at === "string" &&
1567
+ checksum.computed_at.trim() !== "" &&
1568
+ !(await detectWorkflowEnvelopeIntegrityMismatch(statePath))
1569
+ ) {
1570
+ return;
1571
+ }
1456
1572
  existing.auto_handoff = admission;
1573
+ delete existing.final_publication_pending;
1457
1574
  existing = migrateWorkflowState(existing, "ralplan").state;
1458
1575
  existing.updated_at = new Date().toISOString();
1459
1576
  await writeWorkflowEnvelopeAtomic(statePath, existing, {
@@ -1467,6 +1584,87 @@ async function persistRalplanFinalAdmission(
1467
1584
  );
1468
1585
  }
1469
1586
 
1587
+ async function markRalplanFinalPublicationPending(
1588
+ cwd: string,
1589
+ sessionId: string,
1590
+ runId: string,
1591
+ publication: { id: string; sha256: string },
1592
+ recovery = false,
1593
+ ): Promise<boolean> {
1594
+ const statePath = ralplanStatePath(cwd, sessionId);
1595
+ return await withWorkflowStateLock(
1596
+ statePath,
1597
+ async () => {
1598
+ const existingRead = await readExistingStateForMutation(statePath);
1599
+ if (existingRead.kind === "corrupt")
1600
+ throw new RalplanCommandError(2, `existing ralplan state is corrupt or tampered (${existingRead.error})`);
1601
+ let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
1602
+ const phase = typeof existing.current_phase === "string" ? existing.current_phase.trim() : "";
1603
+ // Active final is publication/approval, not a terminal lifecycle despite its stage phase lock.
1604
+ const phaseLocked =
1605
+ getSkillManifest("ralplan").phaseLock.includes(phase) && (phase !== "final" || existing.active === false);
1606
+ if (recovery) {
1607
+ // A receipt lookup may repair its current projection, never select a historical run.
1608
+ if (phaseLocked || (existingRead.kind === "valid" && existing.run_id !== runId)) return false;
1609
+ const pending = existing.final_publication_pending;
1610
+ if (
1611
+ pending !== undefined &&
1612
+ (!pending ||
1613
+ typeof pending !== "object" ||
1614
+ Array.isArray(pending) ||
1615
+ (pending as Record<string, unknown>).run_id !== runId ||
1616
+ (pending as Record<string, unknown>).final_sha256 !== publication.sha256)
1617
+ )
1618
+ return false;
1619
+ const index = await loadRalplanIndexForCap(cwd, sessionId, runId);
1620
+ let lastFinalSha: unknown;
1621
+ for (const line of index.rawText?.split(/\r?\n/) ?? []) {
1622
+ if (!line.trim()) continue;
1623
+ const row = JSON.parse(line) as Record<string, unknown>;
1624
+ if (row.stage === "final") lastFinalSha = row.sha256;
1625
+ }
1626
+ if (lastFinalSha !== publication.sha256) return false;
1627
+ } else if (existing.run_id === runId && phaseLocked) {
1628
+ throw new RalplanCommandError(2, "cannot publish a new final after the Ralplan run reached a locked phase");
1629
+ }
1630
+ if (existing.run_id !== runId) {
1631
+ delete existing.planning_stuck;
1632
+ delete existing.auto_handoff;
1633
+ // A first final may establish the run directly after a planning handoff.
1634
+ if (existing.run_id !== undefined) {
1635
+ delete existing.handoff_from;
1636
+ delete existing.handoff_at;
1637
+ delete existing.upstream_handoff_at;
1638
+ }
1639
+ delete existing.final_admission_phase_transition;
1640
+ }
1641
+ existing.skill = "ralplan";
1642
+ existing.version = WORKFLOW_STATE_VERSION;
1643
+ existing.session_id = sessionId;
1644
+ existing.run_id = runId;
1645
+ existing.active = true;
1646
+ existing.current_phase = "final";
1647
+ const startedAt = new Date().toISOString();
1648
+ existing.updated_at = startedAt;
1649
+ existing.final_publication_pending = {
1650
+ run_id: runId,
1651
+ publication_id: publication.id,
1652
+ final_sha256: publication.sha256,
1653
+ started_at: startedAt,
1654
+ };
1655
+ existing = migrateWorkflowState(existing, "ralplan").state;
1656
+ await writeWorkflowEnvelopeAtomic(statePath, existing, {
1657
+ cwd,
1658
+ lockHeld: true,
1659
+ receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan final-pending", sessionId },
1660
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
1661
+ });
1662
+ return true;
1663
+ },
1664
+ { cwd },
1665
+ );
1666
+ }
1667
+
1470
1668
  async function findExistingRalplanRunOwners(cwd: string, runId: string): Promise<string[]> {
1471
1669
  let entries: Dirent<string>[];
1472
1670
  try {
@@ -1592,6 +1790,10 @@ interface PersistedArtifact {
1592
1790
  pendingApprovalPath?: string;
1593
1791
  }
1594
1792
 
1793
+ /** Keep the writer-side ralplan ledger within the bound enforced by state-runtime. */
1794
+ const RALPLAN_MAX_INDEX_BYTES = 1024 * 1024;
1795
+ const RALPLAN_MAX_INDEX_RECOVERY_BYTES = 8 * 1024 * 1024;
1796
+
1595
1797
  /**
1596
1798
  * Content-addressed identity for an `index.jsonl` row: a repeated `--write` of the
1597
1799
  * same `(stage, stage_n)` at identical content (same sha256) is the #638 duplicate
@@ -1605,6 +1807,93 @@ function ralplanIndexKey(entry: unknown): string | undefined {
1605
1807
  return `${stage}\u0000${stage_n}\u0000${sha256}`;
1606
1808
  }
1607
1809
 
1810
+ function serializeRalplanIndexEntries(entries: readonly unknown[]): string {
1811
+ return entries.map(entry => `${JSON.stringify(entry)}\n`).join("");
1812
+ }
1813
+
1814
+ /**
1815
+ * Compact the append-only ralplan ledger without dropping the current final receipt.
1816
+ * The newest row and newest planning-stuck marker are also retained so a write can
1817
+ * never report success while silently losing the receipt it just persisted.
1818
+ */
1819
+ function compactRalplanIndexEntries(entries: readonly unknown[]): unknown[] {
1820
+ let latestFinal = -1;
1821
+ let latestPlanningStuck = -1;
1822
+ for (const [index, entry] of entries.entries()) {
1823
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
1824
+ const record = entry as Record<string, unknown>;
1825
+ if (record.stage === "final") latestFinal = index;
1826
+ if (record.planning_stuck === true) latestPlanningStuck = index;
1827
+ }
1828
+ }
1829
+ const retained = entries.map((entry, index) => ({
1830
+ entry,
1831
+ bytes: Buffer.byteLength(`${JSON.stringify(entry)}\n`, "utf8"),
1832
+ protected: index === entries.length - 1 || index === latestFinal || index === latestPlanningStuck,
1833
+ }));
1834
+ let totalBytes = retained.reduce((total, item) => total + item.bytes, 0);
1835
+ while (totalBytes > RALPLAN_MAX_INDEX_BYTES) {
1836
+ const removable = retained.findIndex(item => !item.protected);
1837
+ if (removable < 0) {
1838
+ throw new RalplanCommandError(2, "ralplan index live receipt set exceeds the 1 MiB bound");
1839
+ }
1840
+ totalBytes -= retained[removable]!.bytes;
1841
+ retained.splice(removable, 1);
1842
+ }
1843
+ return retained.map(item => item.entry);
1844
+ }
1845
+
1846
+ /** Append a ralplan index row under the ledger lock, compacting before the write. */
1847
+ async function appendRalplanIndexIdempotent(
1848
+ targetPath: string,
1849
+ entry: unknown,
1850
+ options: AppendJsonlIdempotentOptions,
1851
+ ): Promise<AppendJsonlIdempotentResult> {
1852
+ const { key, equals, ...writeOptions } = options;
1853
+ const resolvedTargetPath = path.resolve(options.cwd ?? process.cwd(), targetPath);
1854
+ return await withWorkflowStateLock(
1855
+ targetPath,
1856
+ async () => {
1857
+ const raw = (await readRalplanIndexBounded(resolvedTargetPath)) ?? "";
1858
+ const existing: unknown[] = [];
1859
+ let malformed = false;
1860
+ for (const line of raw.split(/\r?\n/)) {
1861
+ const trimmed = line.trim();
1862
+ if (!trimmed) continue;
1863
+ try {
1864
+ existing.push(JSON.parse(trimmed));
1865
+ } catch {
1866
+ malformed = true;
1867
+ }
1868
+ }
1869
+ const candidateKey = key ? key(entry) : undefined;
1870
+ const duplicate = equals
1871
+ ? existing.find(item => equals(entry, item))
1872
+ : candidateKey === undefined
1873
+ ? undefined
1874
+ : existing.find(item => key?.(item) === candidateKey);
1875
+ const candidateLine = `${JSON.stringify(entry)}\n`;
1876
+ if (Buffer.byteLength(raw, "utf8") + Buffer.byteLength(candidateLine, "utf8") <= RALPLAN_MAX_INDEX_BYTES) {
1877
+ if (duplicate !== undefined) return { path: resolvedTargetPath, appended: false, duplicate };
1878
+ await appendJsonl(targetPath, entry, writeOptions);
1879
+ return { path: resolvedTargetPath, appended: true };
1880
+ }
1881
+ if (malformed) throw new RalplanCommandError(2, "oversized ralplan index contains malformed JSON");
1882
+ const next = duplicate === undefined ? [...existing, entry] : existing;
1883
+ const compacted = compactRalplanIndexEntries(next);
1884
+ const serialized = serializeRalplanIndexEntries(compacted);
1885
+ const existingSerialized = serializeRalplanIndexEntries(existing);
1886
+ if (serialized !== existingSerialized) {
1887
+ await writeTextAtomic(targetPath, serialized, writeOptions);
1888
+ }
1889
+ return duplicate === undefined
1890
+ ? { path: resolvedTargetPath, appended: true }
1891
+ : { path: resolvedTargetPath, appended: false, duplicate };
1892
+ },
1893
+ options,
1894
+ );
1895
+ }
1896
+
1608
1897
  async function persistArtifact(
1609
1898
  resolved: ResolvedArtifactArgs,
1610
1899
  cwd: string,
@@ -1636,7 +1925,7 @@ async function persistArtifact(
1636
1925
  sha256,
1637
1926
  ...(finalAdmission ? { auto_handoff: finalAdmission } : {}),
1638
1927
  };
1639
- await appendJsonlIdempotent(path.join(runDir, "index.jsonl"), indexEntry, {
1928
+ await appendRalplanIndexIdempotent(path.join(runDir, "index.jsonl"), indexEntry, {
1640
1929
  cwd,
1641
1930
  audit: {
1642
1931
  category: "ledger",
@@ -1837,7 +2126,7 @@ async function repairMissingStageArtifactLedger(
1837
2126
  sha256: onDisk.sha256,
1838
2127
  ...(finalAdmission ? { auto_handoff: finalAdmission } : {}),
1839
2128
  };
1840
- const result = await appendJsonlIdempotent(
2129
+ const result = await appendRalplanIndexIdempotent(
1841
2130
  path.join(sessionPlansDir(cwd, resolved.sessionId), "ralplan", resolved.runId, "index.jsonl"),
1842
2131
  indexEntry,
1843
2132
  {
@@ -2034,6 +2323,7 @@ async function handleArtifactWrite(
2034
2323
  args: readonly string[],
2035
2324
  cwd: string,
2036
2325
  agentDir?: string,
2326
+ finalPublicationLockHeld = false,
2037
2327
  ): Promise<RalplanCommandResult> {
2038
2328
  // #4693: explicit --worktree-root binds every persistence root to the selected
2039
2329
  // canonical worktree; the invocation cwd survives only for --artifact input
@@ -2044,6 +2334,17 @@ async function handleArtifactWrite(
2044
2334
  const resolved = await resolveArtifactArgs(args, persistCwd, cwd, {
2045
2335
  confineArtifactRoot: target.explicit ? cwd : undefined,
2046
2336
  });
2337
+ if (resolved.stage === "final" && !finalPublicationLockHeld) {
2338
+ const publicationLockPath = path.join(
2339
+ sessionPlansDir(persistCwd, resolved.sessionId),
2340
+ "ralplan",
2341
+ resolved.runId,
2342
+ "final-publication",
2343
+ );
2344
+ return await withWorkflowStateLock(publicationLockPath, () => handleArtifactWrite(args, cwd, agentDir, true), {
2345
+ cwd: persistCwd,
2346
+ });
2347
+ }
2047
2348
  const persistedRoleState = parsePersistedRoleStateArgs(args, resolved.stage);
2048
2349
  const laneVerdict = parseLaneVerdictArgs(args, resolved.stage, resolved.stageN);
2049
2350
  // Fail closed before stage persistence / path writes when cwd drifted to a sibling repo.
@@ -2093,7 +2394,55 @@ async function handleArtifactWrite(
2093
2394
  `refusing to overwrite ralplan ${resolved.stage} stage ${resolved.stageN} at ${existingArtifact.path}: an artifact with different content already exists (existing sha256=${existingArtifact.sha256}, new sha256=${sha256}). Use a new --stage_n to record another pass.`,
2094
2395
  );
2095
2396
  }
2096
- if (resolved.stage === "final") await ensureFinalPendingApproval(persistCwd, resolved, existingArtifact);
2397
+ if (resolved.stage === "final") {
2398
+ await ensureFinalPendingApproval(persistCwd, resolved, existingArtifact);
2399
+ await appendRalplanIndexIdempotent(
2400
+ path.join(sessionPlansDir(persistCwd, resolved.sessionId), "ralplan", resolved.runId, "index.jsonl"),
2401
+ {
2402
+ stage: "final",
2403
+ stage_n: resolved.stageN,
2404
+ path: existingArtifact.path,
2405
+ created_at: existingArtifact.createdAt,
2406
+ sha256: existingArtifact.sha256,
2407
+ ...(existingArtifact.autoHandoff ? { auto_handoff: existingArtifact.autoHandoff } : {}),
2408
+ },
2409
+ {
2410
+ cwd: persistCwd,
2411
+ key: ralplanIndexKey,
2412
+ audit: {
2413
+ category: "ledger",
2414
+ verb: "append",
2415
+ owner: "gjc-runtime",
2416
+ skill: "ralplan",
2417
+ sessionId: resolved.sessionId,
2418
+ },
2419
+ },
2420
+ );
2421
+ // The artifact and its authenticated ledger receipt are durable before the
2422
+ // current-session admission projection. Repair that projection after a
2423
+ // crash in the gap, before returning the deduplicated receipt.
2424
+ if (existingArtifact.autoHandoff) {
2425
+ const planningStuck = await readRalplanPlanningStuck(persistCwd, resolved.sessionId, resolved.runId);
2426
+ const publication = { id: randomUUID(), sha256: existingArtifact.sha256 };
2427
+ if (
2428
+ await markRalplanFinalPublicationPending(
2429
+ persistCwd,
2430
+ resolved.sessionId,
2431
+ resolved.runId,
2432
+ publication,
2433
+ true,
2434
+ )
2435
+ ) {
2436
+ await persistRalplanFinalAdmission(
2437
+ persistCwd,
2438
+ resolved.sessionId,
2439
+ resolved.runId,
2440
+ applyRalplanPlanningStuckOverride(existingArtifact.autoHandoff, planningStuck),
2441
+ publication,
2442
+ );
2443
+ }
2444
+ }
2445
+ }
2097
2446
  return await buildDeduplicatedResult(resolved, existingArtifact, sha256, persistCwd, repositoryBinding);
2098
2447
  }
2099
2448
 
@@ -2115,6 +2464,26 @@ async function handleArtifactWrite(
2115
2464
  onDiskArtifact,
2116
2465
  resolved.stage === "final" ? unavailableRalplanFinalAdmission() : undefined,
2117
2466
  );
2467
+ if (resolved.stage === "final") {
2468
+ const recoveryPublication = { id: randomUUID(), sha256 };
2469
+ if (
2470
+ await markRalplanFinalPublicationPending(
2471
+ persistCwd,
2472
+ resolved.sessionId,
2473
+ resolved.runId,
2474
+ recoveryPublication,
2475
+ true,
2476
+ )
2477
+ ) {
2478
+ await persistRalplanFinalAdmission(
2479
+ persistCwd,
2480
+ resolved.sessionId,
2481
+ resolved.runId,
2482
+ unavailableRalplanFinalAdmission(),
2483
+ recoveryPublication,
2484
+ );
2485
+ }
2486
+ }
2118
2487
  let appliedPersistedRoleState: PersistedRoleStateUpdate | undefined;
2119
2488
  if (
2120
2489
  persistedRoleState &&
@@ -2189,6 +2558,7 @@ async function handleArtifactWrite(
2189
2558
  }
2190
2559
 
2191
2560
  let autoHandoff: RalplanAutoHandoffResolution | undefined;
2561
+ let finalPublication: { id: string; sha256: string } | undefined;
2192
2562
  if (resolved.stage === "final") {
2193
2563
  // Resolve and validate the configured admission before any final artifact or
2194
2564
  // state write. The ledger row below is the durable receipt for deduplicated
@@ -2197,6 +2567,8 @@ async function handleArtifactWrite(
2197
2567
  agentDir,
2198
2568
  planningStuck: await readRalplanPlanningStuck(persistCwd, resolved.sessionId, resolved.runId),
2199
2569
  });
2570
+ finalPublication = { id: randomUUID(), sha256 };
2571
+ await markRalplanFinalPublicationPending(persistCwd, resolved.sessionId, resolved.runId, finalPublication);
2200
2572
  }
2201
2573
  // Keep run-state `current_phase` coherent with the stage being persisted.
2202
2574
  await persistActiveRunId(persistCwd, resolved.sessionId, resolved.runId, resolved.stage);
@@ -2208,7 +2580,13 @@ async function handleArtifactWrite(
2208
2580
  await applyLaneVerdictUpdate(persistCwd, resolved.sessionId, laneVerdict);
2209
2581
  }
2210
2582
  if (autoHandoff) {
2211
- await persistRalplanFinalAdmission(persistCwd, resolved.sessionId, resolved.runId, autoHandoff);
2583
+ await persistRalplanFinalAdmission(
2584
+ persistCwd,
2585
+ resolved.sessionId,
2586
+ resolved.runId,
2587
+ autoHandoff,
2588
+ finalPublication!,
2589
+ );
2212
2590
  }
2213
2591
  await writeSessionActivityMarker(persistCwd, resolved.sessionId, {
2214
2592
  writer: "ralplan-runtime",
@@ -2385,51 +2763,68 @@ async function seedRalplanState(
2385
2763
  explicitTarget = false,
2386
2764
  ): Promise<{ statePath: string; runId: string; repositoryBinding: RepositoryBinding }> {
2387
2765
  const statePath = ralplanStatePath(cwd, resolved.sessionId);
2388
- // Reuse an existing run id when present so a re-invocation of `gjc ralplan "task"` doesn't
2389
- // orphan in-progress artifacts under a fresh run id.
2390
- const existingRunId = await readActiveRunId(cwd, resolved.sessionId);
2391
- const runId = existingRunId ?? resolved.sessionId ?? defaultRunId();
2392
- assertSafePathComponent(runId, "run-id");
2393
- const now = new Date().toISOString();
2394
- // When an active seed already carries authority, re-entry must match it (fail closed).
2395
- // Otherwise stamp the current cwd as the durable binding for this run.
2396
- const repositoryBinding = existingRunId
2397
- ? await enforceRalplanRepositoryBinding(cwd, resolved.sessionId, { exactWorktreeRoot: explicitTarget })
2398
- : publicRepositoryBinding(await captureRepositoryBinding(cwd, { displayPath: cwd }));
2399
- const payload: Record<string, unknown> = {
2400
- active: true,
2401
- current_phase: "planner",
2402
- skill: "ralplan",
2403
- version: WORKFLOW_STATE_VERSION,
2404
- mode: resolved.deliberate ? "deliberate" : "short",
2405
- interactive: resolved.interactive,
2406
- task: resolved.task,
2407
- run_id: runId,
2408
- updated_at: now,
2409
- repository_binding: repositoryBinding,
2410
- };
2411
- if (resolved.architectKind) payload.architect_kind = resolved.architectKind;
2412
- if (resolved.criticKind) payload.critic_kind = resolved.criticKind;
2413
- if (resolved.sessionId) payload.session_id = resolved.sessionId;
2414
- await writeWorkflowEnvelopeAtomic(statePath, payload, {
2415
- cwd,
2416
- receipt: {
2417
- cwd,
2418
- skill: "ralplan",
2419
- owner: "gjc-runtime",
2420
- command: "gjc ralplan seed",
2421
- sessionId: resolved.sessionId,
2422
- },
2423
- audit: {
2424
- category: "state",
2425
- verb: "write",
2426
- owner: "gjc-runtime",
2427
- skill: "ralplan",
2428
- sessionId: resolved.sessionId,
2766
+ return withWorkflowStateLock(
2767
+ statePath,
2768
+ async () => {
2769
+ const existingStateRead = await readExistingStateForMutation(statePath);
2770
+ if (existingStateRead.kind === "corrupt")
2771
+ throw new RalplanCommandError(
2772
+ 2,
2773
+ `existing ralplan state is corrupt or tampered (${existingStateRead.error}); refusing to overwrite ${statePath}`,
2774
+ );
2775
+ const existingRunId =
2776
+ existingStateRead.kind === "valid" && typeof existingStateRead.value.run_id === "string"
2777
+ ? existingStateRead.value.run_id.trim() || undefined
2778
+ : undefined;
2779
+ const runId = existingRunId ?? resolved.sessionId ?? defaultRunId();
2780
+ assertSafePathComponent(runId, "run-id");
2781
+ const now = new Date().toISOString();
2782
+ const repositoryBinding = existingRunId
2783
+ ? await enforceRalplanRepositoryBinding(cwd, resolved.sessionId, { exactWorktreeRoot: explicitTarget })
2784
+ : publicRepositoryBinding(await captureRepositoryBinding(cwd, { displayPath: cwd }));
2785
+ const payload: Record<string, unknown> = {
2786
+ active: true,
2787
+ current_phase: "planner",
2788
+ skill: "ralplan",
2789
+ version: WORKFLOW_STATE_VERSION,
2790
+ mode: resolved.deliberate ? "deliberate" : "short",
2791
+ interactive: resolved.interactive,
2792
+ task: resolved.task,
2793
+ run_id: runId,
2794
+ updated_at: now,
2795
+ repository_binding: repositoryBinding,
2796
+ };
2797
+ if (existingStateRead.kind === "valid" && existingStateRead.value.active === true) {
2798
+ for (const field of ["handoff_from", "handoff_at"] as const) {
2799
+ if (typeof existingStateRead.value[field] === "string") payload[field] = existingStateRead.value[field];
2800
+ }
2801
+ }
2802
+ if (resolved.architectKind) payload.architect_kind = resolved.architectKind;
2803
+ if (resolved.criticKind) payload.critic_kind = resolved.criticKind;
2804
+ if (resolved.sessionId) payload.session_id = resolved.sessionId;
2805
+ await writeWorkflowEnvelopeAtomic(statePath, payload, {
2806
+ cwd,
2807
+ lockHeld: true,
2808
+ receipt: {
2809
+ cwd,
2810
+ skill: "ralplan",
2811
+ owner: "gjc-runtime",
2812
+ command: "gjc ralplan seed",
2813
+ sessionId: resolved.sessionId,
2814
+ },
2815
+ audit: {
2816
+ category: "state",
2817
+ verb: "write",
2818
+ owner: "gjc-runtime",
2819
+ skill: "ralplan",
2820
+ sessionId: resolved.sessionId,
2821
+ },
2822
+ });
2823
+ await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "ralplan-runtime", path: statePath });
2824
+ return { statePath, runId, repositoryBinding };
2429
2825
  },
2430
- });
2431
- await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "ralplan-runtime", path: statePath });
2432
- return { statePath, runId, repositoryBinding };
2826
+ { cwd },
2827
+ );
2433
2828
  }
2434
2829
 
2435
2830
  async function handleConsensusHandoff(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
@@ -6,6 +6,7 @@ export type StateAction =
6
6
  | "write"
7
7
  | "clear"
8
8
  | "contract"
9
+ | "approve-execution"
9
10
  | "handoff"
10
11
  | "graph"
11
12
  | "prune"
@@ -19,6 +20,7 @@ export const STATE_ACTION_NAMES: ReadonlySet<StateAction> = new Set([
19
20
  "write",
20
21
  "clear",
21
22
  "contract",
23
+ "approve-execution",
22
24
  "handoff",
23
25
  "graph",
24
26
  "prune",