@gajae-code/coding-agent 0.11.10 → 0.12.0

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 (249) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/cli.d.ts +8 -1
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-profile-activation.d.ts +1 -0
  8. package/dist/types/config/model-profile-contract.d.ts +2 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/provider-auth-health.d.ts +14 -0
  11. package/dist/types/config/provider-ranking.d.ts +59 -0
  12. package/dist/types/config/settings-schema.d.ts +86 -3
  13. package/dist/types/config/settings.d.ts +3 -0
  14. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  15. package/dist/types/extensibility/shared-events.d.ts +6 -0
  16. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  17. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  18. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  19. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  20. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  21. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  22. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  23. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
  24. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  25. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  26. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  28. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  29. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  30. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  31. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  32. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  33. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  34. package/dist/types/main.d.ts +1 -1
  35. package/dist/types/memories/index.d.ts +2 -0
  36. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  37. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  38. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  39. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  40. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  41. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  42. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  43. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  44. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  45. package/dist/types/modes/components/welcome.d.ts +1 -0
  46. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  47. package/dist/types/modes/interactive-mode.d.ts +7 -1
  48. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  49. package/dist/types/modes/types.d.ts +2 -0
  50. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  51. package/dist/types/runtime/memory-domain.d.ts +9 -0
  52. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  53. package/dist/types/runtime/memory-guard.d.ts +39 -0
  54. package/dist/types/runtime/memory-limit.d.ts +11 -0
  55. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  56. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  57. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  58. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  59. package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
  60. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  61. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  62. package/dist/types/sdk/bus/index.d.ts +18 -2
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  65. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  66. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  67. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
  68. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
  69. package/dist/types/sdk/host/control/operations.d.ts +2 -1
  70. package/dist/types/sdk/host/host.d.ts +2 -0
  71. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  72. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  73. package/dist/types/sdk/startup-capability.d.ts +6 -0
  74. package/dist/types/session/agent-session.d.ts +51 -19
  75. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  76. package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
  77. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  78. package/dist/types/session/session-manager.d.ts +59 -13
  79. package/dist/types/session/streaming-output.d.ts +60 -5
  80. package/dist/types/skill-state/active-state.d.ts +0 -4
  81. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  82. package/dist/types/task/executor.d.ts +6 -0
  83. package/dist/types/task/types.d.ts +36 -0
  84. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  85. package/dist/types/tools/browser/launch.d.ts +7 -0
  86. package/dist/types/tools/fetch.d.ts +17 -1
  87. package/dist/types/tools/output-meta.d.ts +21 -1
  88. package/dist/types/tools/read-internals.d.ts +12 -0
  89. package/dist/types/tools/read.d.ts +45 -1
  90. package/dist/types/tools/resource-gc.d.ts +23 -0
  91. package/dist/types/tools/tool-result.d.ts +7 -1
  92. package/package.json +9 -13
  93. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  94. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  95. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  96. package/scripts/run-test-manifest.ts +6 -4
  97. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  98. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  99. package/src/capability/index.ts +8 -0
  100. package/src/cli/args.ts +14 -8
  101. package/src/cli/fast-help.ts +1 -1
  102. package/src/cli/read-cli.ts +2 -1
  103. package/src/cli.ts +49 -2
  104. package/src/commands/deep-interview.ts +29 -9
  105. package/src/commands/harness.ts +35 -12
  106. package/src/commands/read.ts +11 -2
  107. package/src/commands/sdk.ts +91 -7
  108. package/src/commands/team.ts +1 -1
  109. package/src/config/model-profile-activation.ts +1 -0
  110. package/src/config/model-profile-contract.ts +13 -0
  111. package/src/config/model-profiles.ts +24 -0
  112. package/src/config/model-registry.ts +21 -2
  113. package/src/config/provider-auth-health.ts +42 -0
  114. package/src/config/provider-ranking.ts +119 -0
  115. package/src/config/settings-schema.ts +84 -3
  116. package/src/config/settings.ts +254 -108
  117. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  118. package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
  119. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  120. package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
  121. package/src/eval/py/executor.ts +7 -2
  122. package/src/extensibility/extensions/runner.ts +4 -0
  123. package/src/extensibility/extensions/types.ts +42 -4
  124. package/src/extensibility/shared-events.ts +5 -0
  125. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
  126. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  127. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  128. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  129. package/src/gjc-runtime/deep-interview-state.ts +12 -921
  130. package/src/gjc-runtime/gc-render.ts +1 -0
  131. package/src/gjc-runtime/gc-runtime.ts +11 -1
  132. package/src/gjc-runtime/linux-proc.ts +70 -36
  133. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  134. package/src/gjc-runtime/ralplan-runtime.ts +979 -71
  135. package/src/gjc-runtime/state-runtime.ts +50 -12
  136. package/src/gjc-runtime/state-writer.ts +70 -222
  137. package/src/gjc-runtime/team-launch.ts +34 -5
  138. package/src/gjc-runtime/team-runtime.ts +970 -33
  139. package/src/gjc-runtime/team-store.ts +32 -0
  140. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  141. package/src/gjc-runtime/team-workers.ts +1 -0
  142. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  143. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  144. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  145. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  146. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  147. package/src/hooks/native-skill-hook.ts +17 -4
  148. package/src/hooks/skill-state.ts +1 -23
  149. package/src/internal-urls/docs-index.generated.ts +10 -11
  150. package/src/internal-urls/local-protocol.ts +23 -4
  151. package/src/internal-urls/local-root-gc.ts +127 -0
  152. package/src/main.ts +9 -5
  153. package/src/memories/index.ts +10 -0
  154. package/src/modes/DESIGN.md +47 -0
  155. package/src/modes/acp/acp-agent.ts +597 -67
  156. package/src/modes/acp/acp-event-mapper.ts +106 -12
  157. package/src/modes/acp/acp-mode.ts +39 -3
  158. package/src/modes/components/assistant-message.ts +123 -22
  159. package/src/modes/components/irc-sidebar.ts +176 -62
  160. package/src/modes/components/model-selector.ts +76 -5
  161. package/src/modes/components/notifications-settings-editor.ts +1 -1
  162. package/src/modes/components/oauth-selector.ts +42 -12
  163. package/src/modes/components/queue-pane.ts +55 -9
  164. package/src/modes/components/queued-message-selector.ts +53 -10
  165. package/src/modes/components/read-tool-group.ts +82 -14
  166. package/src/modes/components/session-observer-overlay.ts +187 -32
  167. package/src/modes/components/settings-selector.ts +60 -18
  168. package/src/modes/components/tool-execution.ts +138 -27
  169. package/src/modes/components/tool-status-header.ts +1 -4
  170. package/src/modes/components/welcome.ts +18 -3
  171. package/src/modes/controllers/command-controller.ts +4 -0
  172. package/src/modes/controllers/event-controller.ts +109 -27
  173. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  174. package/src/modes/controllers/input-controller.ts +62 -6
  175. package/src/modes/controllers/selector-controller.ts +83 -37
  176. package/src/modes/interactive-mode.ts +127 -29
  177. package/src/modes/irc-observation-ledger.ts +71 -15
  178. package/src/modes/runtime-init.ts +20 -2
  179. package/src/modes/types.ts +2 -0
  180. package/src/modes/utils/ui-helpers.ts +94 -1
  181. package/src/prompts/agents/architect.md +9 -1
  182. package/src/prompts/agents/critic.md +9 -0
  183. package/src/prompts/agents/executor.md +1 -1
  184. package/src/prompts/system/system-prompt.md +0 -1
  185. package/src/prompts/tools/read.md +4 -2
  186. package/src/runtime/memory-domain.ts +66 -0
  187. package/src/runtime/memory-guard-contract.ts +73 -0
  188. package/src/runtime/memory-guard.ts +292 -0
  189. package/src/runtime/memory-limit.ts +50 -0
  190. package/src/runtime-mcp/manager.ts +26 -6
  191. package/src/runtime-mcp/smithery-auth.ts +17 -2
  192. package/src/runtime-mcp/smithery-connect.ts +11 -1
  193. package/src/sdk/acp/adapter.ts +8 -1
  194. package/src/sdk/acp/final-text.ts +40 -0
  195. package/src/sdk/acp/mcp.ts +26 -0
  196. package/src/sdk/broker/lifecycle.ts +107 -7
  197. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  198. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  199. package/src/sdk/bus/config.ts +1 -1
  200. package/src/sdk/bus/control-drain-lease.ts +104 -0
  201. package/src/sdk/bus/index.ts +503 -60
  202. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  203. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  204. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  205. package/src/sdk/bus/reconciliation-store.ts +224 -0
  206. package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
  207. package/src/sdk/bus/telegram-daemon.ts +531 -53
  208. package/src/sdk/host/control/dispatch.ts +7 -1
  209. package/src/sdk/host/control/operations.ts +2 -1
  210. package/src/sdk/host/host.ts +25 -3
  211. package/src/sdk/host/query/handlers.ts +55 -0
  212. package/src/sdk/lifecycle-session.ts +19 -3
  213. package/src/sdk/mcp/server.ts +46 -18
  214. package/src/sdk/protocol/operation-inventory.generated.json +76 -0
  215. package/src/sdk/protocol/operation-registry.ts +8 -2
  216. package/src/sdk/session.ts +25 -2
  217. package/src/sdk/startup-capability.ts +6 -0
  218. package/src/session/agent-session.ts +720 -190
  219. package/src/session/fallback-chain-controller.ts +47 -8
  220. package/src/session/internal/managed-session-scope.ts +40 -10
  221. package/src/session/internal/native-publish-outcome.ts +8 -3
  222. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  223. package/src/session/session-manager.ts +1372 -42
  224. package/src/session/streaming-output.ts +262 -43
  225. package/src/setup/provider-onboarding.ts +19 -4
  226. package/src/skill-state/active-state.ts +0 -10
  227. package/src/skill-state/workflow-hud.ts +25 -2
  228. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  229. package/src/task/executor.ts +11 -3
  230. package/src/task/index.ts +2 -0
  231. package/src/task/types.ts +22 -11
  232. package/src/task/ultragoal-redteam-activation.ts +75 -0
  233. package/src/tools/ask.ts +89 -69
  234. package/src/tools/browser/launch.ts +41 -6
  235. package/src/tools/fetch.ts +114 -23
  236. package/src/tools/output-meta.ts +176 -9
  237. package/src/tools/path-utils.ts +5 -1
  238. package/src/tools/read-internals.ts +44 -0
  239. package/src/tools/read.ts +1542 -310
  240. package/src/tools/resource-gc.ts +665 -125
  241. package/src/tools/tool-result.ts +14 -1
  242. package/src/web/search/providers/codex.ts +1 -1
  243. package/src/workflow/workflow-intent-diff.ts +4 -1
  244. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  245. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  246. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  247. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  248. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  249. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -1,6 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
+ // Subpath import keeps this module native-free for the gjc-state-gates shards:
5
+ // the package barrel pulls procmgr/ptree → @gajae-code/natives.
6
+ import * as logger from "@gajae-code/utils/logger";
4
7
  import type { WorkflowHudSummary } from "../skill-state/active-state";
5
8
  import {
6
9
  applyHandoffToActiveState,
@@ -265,7 +268,21 @@ async function describeStaleClearState(
265
268
  return undefined;
266
269
  }
267
270
 
268
- async function readJsonFile(filePath: string): Promise<Record<string, unknown> | null> {
271
+ /**
272
+ * Route a workflow-state warning through the TUI-safe centralized file logger
273
+ * (console transport off by default) so interactive sessions never paint raw
274
+ * bytes into the alternate-screen stream (#3002). CLI command handlers may also
275
+ * collect the warning via an `onWarning` sink to surface it on the structured
276
+ * {@link StateCommandResult.stderr} channel, so `gjc state` automation still
277
+ * distinguishes corrupt state from absent state.
278
+ */
279
+ function emitStateWarning(warning: string, context?: Record<string, unknown>): void {
280
+ logger.warn(warning, context);
281
+ }
282
+
283
+ type StateWarningSink = (warning: string) => void;
284
+
285
+ async function readJsonFile(filePath: string, onWarning?: StateWarningSink): Promise<Record<string, unknown> | null> {
269
286
  try {
270
287
  const raw = await fs.readFile(filePath, "utf-8");
271
288
  const parsed = JSON.parse(raw);
@@ -276,18 +293,22 @@ async function readJsonFile(filePath: string): Promise<Record<string, unknown> |
276
293
  } catch (error) {
277
294
  const err = error as NodeJS.ErrnoException;
278
295
  if (err.code === "ENOENT") return null;
279
- process.stderr.write(`WARNING: failed to read ${filePath}; ignoring corrupt state: ${err.message}\n`);
296
+ const warning = `WARNING: failed to read ${filePath}; ignoring corrupt state: ${err.message}`;
297
+ emitStateWarning(warning, { filePath, error: err.message });
298
+ onWarning?.(warning);
280
299
  return null;
281
300
  }
282
301
  }
283
302
 
284
- async function readJsonValue(filePath: string): Promise<unknown | null> {
303
+ async function readJsonValue(filePath: string, onWarning?: StateWarningSink): Promise<unknown | null> {
285
304
  try {
286
305
  return JSON.parse(await fs.readFile(filePath, "utf-8"));
287
306
  } catch (error) {
288
307
  const err = error as NodeJS.ErrnoException;
289
308
  if (err.code === "ENOENT") return null;
290
- process.stderr.write(`WARNING: failed to read ${filePath}; ignoring corrupt state: ${err.message}\n`);
309
+ const warning = `WARNING: failed to read ${filePath}; ignoring corrupt state: ${err.message}`;
310
+ emitStateWarning(warning, { filePath, error: err.message });
311
+ onWarning?.(warning);
291
312
  return null;
292
313
  }
293
314
  }
@@ -836,7 +857,8 @@ function buildHudForMode(
836
857
  : typeof payload.mode === "string"
837
858
  ? (payload.mode as string)
838
859
  : undefined;
839
- const verdict = typeof payload.verdict === "string" ? (payload.verdict as string) : undefined;
860
+ const rawVerdict = payload.last_review_verdict ?? payload.verdict;
861
+ const verdict = typeof rawVerdict === "string" ? rawVerdict : undefined;
840
862
  const iteration = typeof payload.iteration === "number" ? (payload.iteration as number) : undefined;
841
863
  const pendingApproval = payload.pending_approval === true || stage === "final";
842
864
  return buildRalplanHudSummary({
@@ -1101,21 +1123,30 @@ export async function readWorkflowStateJson(
1101
1123
  cwd: string,
1102
1124
  skill: CanonicalGjcWorkflowSkill,
1103
1125
  sessionId?: string,
1126
+ onWarning?: StateWarningSink,
1104
1127
  ): Promise<Record<string, unknown>> {
1105
1128
  const session = await resolveGjcSessionForRead(cwd, {
1106
1129
  payloadSessionId: sessionId,
1107
1130
  envSessionId: process.env.GJC_SESSION_ID,
1108
1131
  });
1109
- return (await readJsonFile(modeStateFile(cwd, skill, session.gjcSessionId))) ?? {};
1132
+ return (await readJsonFile(modeStateFile(cwd, skill, session.gjcSessionId), onWarning)) ?? {};
1110
1133
  }
1111
1134
 
1112
1135
  async function handleRead(args: readonly string[], cwd: string): Promise<StateCommandResult> {
1113
1136
  const selectors = await resolveSelectors(args, cwd, "read");
1114
1137
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
1115
1138
  const fields = parseFieldsFlag(args);
1139
+ // Corrupt-state warnings are TUI-safe file-logged inside the readers; the CLI
1140
+ // path also surfaces them on the command result so `gjc state read`
1141
+ // automation can tell corrupt state from absent state (#3002).
1142
+ const warnings: string[] = [];
1143
+ const warningStderr = (): Pick<StateCommandResult, "stderr"> =>
1144
+ warnings.length ? { stderr: warnings.map(warning => `${warning}\n`).join("") } : {};
1116
1145
  if (mode) {
1117
1146
  const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
1118
- const existing = await readWorkflowStateJson(cwd, mode, selectors.gjcSessionId);
1147
+ const existing = await readWorkflowStateJson(cwd, mode, selectors.gjcSessionId, warning =>
1148
+ warnings.push(warning),
1149
+ );
1119
1150
  const envelope = { skill: mode, state: existing, storage_path: filePath };
1120
1151
  const manifest = getSkillManifest(mode);
1121
1152
  if (fields) {
@@ -1125,6 +1156,7 @@ async function handleRead(args: readonly string[], cwd: string): Promise<StateCo
1125
1156
  stdout: hasFlag(args, "--json")
1126
1157
  ? `${JSON.stringify(projected, null, 2)}\n`
1127
1158
  : renderStateMarkdown(mode, projected, manifest),
1159
+ ...warningStderr(),
1128
1160
  };
1129
1161
  }
1130
1162
  if (hasFlag(args, "--compact")) {
@@ -1134,6 +1166,7 @@ async function handleRead(args: readonly string[], cwd: string): Promise<StateCo
1134
1166
  stdout: hasFlag(args, "--json")
1135
1167
  ? `${JSON.stringify(compact, null, 2)}\n`
1136
1168
  : renderStateMarkdown(mode, envelope, manifest),
1169
+ ...warningStderr(),
1137
1170
  };
1138
1171
  }
1139
1172
  return {
@@ -1141,12 +1174,13 @@ async function handleRead(args: readonly string[], cwd: string): Promise<StateCo
1141
1174
  stdout: hasFlag(args, "--json")
1142
1175
  ? `${JSON.stringify(envelope, null, 2)}\n`
1143
1176
  : renderStateMarkdown(mode, envelope, manifest),
1177
+ ...warningStderr(),
1144
1178
  };
1145
1179
  }
1146
1180
  const filePath = activeStateFile(cwd, selectors.gjcSessionId);
1147
- const existingRaw = await readJsonValue(filePath);
1181
+ const existingRaw = await readJsonValue(filePath, warning => warnings.push(warning));
1148
1182
  const existing = isPlainObject(existingRaw) ? existingRaw : null;
1149
- return { status: 0, stdout: `${JSON.stringify(existing ?? {}, null, 2)}\n` };
1183
+ return { status: 0, stdout: `${JSON.stringify(existing ?? {}, null, 2)}\n`, ...warningStderr() };
1150
1184
  }
1151
1185
 
1152
1186
  async function handleStatus(args: readonly string[], cwd: string): Promise<StateCommandResult> {
@@ -1159,7 +1193,8 @@ async function handleStatus(args: readonly string[], cwd: string): Promise<State
1159
1193
  );
1160
1194
  }
1161
1195
  const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
1162
- const existing = await readWorkflowStateJson(cwd, mode, selectors.gjcSessionId);
1196
+ const warnings: string[] = [];
1197
+ const existing = await readWorkflowStateJson(cwd, mode, selectors.gjcSessionId, warning => warnings.push(warning));
1163
1198
  const summary = buildStateStatusSummary(
1164
1199
  mode,
1165
1200
  { skill: mode, state: existing, storage_path: filePath },
@@ -1169,6 +1204,7 @@ async function handleStatus(args: readonly string[], cwd: string): Promise<State
1169
1204
  return {
1170
1205
  status: 0,
1171
1206
  stdout: hasFlag(args, "--json") ? `${JSON.stringify(summary, null, 2)}\n` : renderStateStatusLine(summary),
1207
+ ...(warnings.length ? { stderr: warnings.map(warning => `${warning}\n`).join("") } : {}),
1172
1208
  };
1173
1209
  }
1174
1210
 
@@ -1603,7 +1639,7 @@ async function handleHandoffUnlocked(args: readonly string[], cwd: string): Prom
1603
1639
  toPhase: "handoff",
1604
1640
  });
1605
1641
  await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, { steps: ["caller-mode-state"] });
1606
- if (callerWrite.warning) process.stderr.write(`${callerWrite.warning}\n`);
1642
+ if (callerWrite.warning) emitStateWarning(callerWrite.warning);
1607
1643
  const stampedCallerReceipt = isPlainObject(callerWrite.stamped.receipt) ? callerWrite.stamped.receipt : {};
1608
1644
  await syncSkillActiveState({
1609
1645
  cwd,
@@ -1646,6 +1682,7 @@ async function handleHandoffUnlocked(args: readonly string[], cwd: string): Prom
1646
1682
  active_state: activeStateFile(cwd, sessionId),
1647
1683
  },
1648
1684
  }),
1685
+ ...(callerWrite.warning ? { stderr: `${callerWrite.warning}\n` } : {}),
1649
1686
  };
1650
1687
  }
1651
1688
 
@@ -1746,7 +1783,7 @@ async function handleHandoffUnlocked(args: readonly string[], cwd: string): Prom
1746
1783
  );
1747
1784
  const stampedCallerReceipt = isPlainObject(callerWrite.stamped.receipt) ? callerWrite.stamped.receipt : {};
1748
1785
  const stampedCalleeReceipt = isPlainObject(calleeWrite.stamped.receipt) ? calleeWrite.stamped.receipt : {};
1749
- for (const warning of warnings) process.stderr.write(`${warning}\n`);
1786
+ for (const warning of warnings) emitStateWarning(warning);
1750
1787
  if (process.env.GJC_STATE_HANDOFF_FAIL_AFTER_CALLER === mutationId) {
1751
1788
  throw new StateCommandError(1, `injected handoff failure after caller write for ${mutationId}`);
1752
1789
  }
@@ -1818,6 +1855,7 @@ async function handleHandoffUnlocked(args: readonly string[], cwd: string): Prom
1818
1855
  active_state: activeStateFile(cwd, sessionId),
1819
1856
  },
1820
1857
  }),
1858
+ ...(warnings.length ? { stderr: warnings.map(warning => `${warning}\n`).join("") } : {}),
1821
1859
  };
1822
1860
  }
1823
1861
 
@@ -11,7 +11,6 @@ import {
11
11
  type WorkflowStateMutationOwner,
12
12
  type WorkflowStateReceipt,
13
13
  } from "../skill-state/workflow-state-contract";
14
- import { validateDeepInterviewV1Envelope } from "./deep-interview-state";
15
14
  import {
16
15
  activeEntryPath as layoutActiveEntryPath,
17
16
  activeSnapshotPath as layoutActiveSnapshotPath,
@@ -78,47 +77,6 @@ export interface WorkflowEnvelopeIntegrityMismatch {
78
77
  expected: string;
79
78
  actual: string;
80
79
  }
81
- export type WorkflowReceiptVerification =
82
- | "legacy"
83
- | "native-valid"
84
- | "receipt-malformed"
85
- | "receipt-missing"
86
- | "checksum-mismatch";
87
-
88
- export interface GuardedWorkflowEnvelopeTransformOptions extends StateWriterOptions {
89
- expectedRevision: number;
90
- /** Additional native schema validation applied to both current and candidate envelopes. */
91
- validate?: (value: Record<string, unknown>, legacy: boolean) => void;
92
- transform: (
93
- current: Record<string, unknown>,
94
- context: { revision: number; legacy: boolean },
95
- ) =>
96
- | { kind: "noop" }
97
- | { kind: "write"; value: Record<string, unknown> }
98
- | Promise<{ kind: "noop" } | { kind: "write"; value: Record<string, unknown> }>;
99
- }
100
-
101
- export interface GuardedWorkflowEnvelopeWarning {
102
- code: "DI_POST_COMMIT_AUDIT_FAILED";
103
- message: string;
104
- }
105
-
106
- export type GuardedWorkflowEnvelopeTransformResult =
107
- | { path: string; written: false; reason: "noop"; revision: number; warnings: [] }
108
- | {
109
- path: string;
110
- written: true;
111
- revision: number;
112
- stamped: Record<string, unknown>;
113
- warnings: GuardedWorkflowEnvelopeWarning[];
114
- };
115
-
116
- export class GuardedWorkflowEnvelopeError extends Error {
117
- constructor(public readonly code: string) {
118
- super(code);
119
- this.name = "GuardedWorkflowEnvelopeError";
120
- }
121
- }
122
80
 
123
81
  export interface WorkflowTransactionJournal {
124
82
  version: 1;
@@ -138,7 +96,6 @@ export interface GuardedStateWriterOptions extends StateWriterOptions {
138
96
  policy: StateWritePolicy;
139
97
  expectedRevision?: number;
140
98
  sourceRevision?: number;
141
- lockHeld?: boolean;
142
99
  }
143
100
 
144
101
  export type GuardedWriteResult =
@@ -150,13 +107,17 @@ export interface StateWriterOptions {
150
107
  receipt?: StateWriterReceiptContext;
151
108
  audit?: StateWriterAuditContext;
152
109
  sourceRevision?: number;
153
- orderingRevision?: number;
154
110
  /**
155
111
  * Cross-process lock tuning for read-modify-write paths that route through
156
112
  * `withWorkflowStateLock` / `updateJsonAtomic`. Omit for the hardened
157
113
  * `withFileLock` defaults.
158
114
  */
159
115
  lock?: FileLockOptions;
116
+ /**
117
+ * Caller already holds the workflow state lock for this target path (via
118
+ * `withWorkflowStateLock`). Skip re-acquisition to avoid self-deadlock.
119
+ */
120
+ lockHeld?: boolean;
160
121
  }
161
122
 
162
123
  export class StateWriteConflictError extends Error {
@@ -298,49 +259,6 @@ export function workflowEnvelopeContentSha256(value: unknown): string {
298
259
  .update(JSON.stringify(canonicalizeJson(withoutReceiptChecksum(value))))
299
260
  .digest("hex");
300
261
  }
301
- export function isNativeDeepInterviewV1(value: Record<string, unknown>): boolean {
302
- if (value.skill !== "deep-interview") return false;
303
- return value.schema_version === 1 || (isPlainObject(value.state) && value.state.schema_version === 1);
304
- }
305
-
306
- export function verifyWorkflowEnvelopeReceiptValue(value: unknown, filePath: string): WorkflowReceiptVerification {
307
- if (!isPlainObject(value)) return "receipt-malformed";
308
- const native = isNativeDeepInterviewV1(value);
309
- if (!Object.hasOwn(value, "receipt")) return native ? "receipt-missing" : "legacy";
310
- if (!isPlainObject(value.receipt) || !isPlainObject(value.receipt.content_sha256)) return "receipt-malformed";
311
-
312
- const receipt = value.receipt as Record<string, unknown>;
313
- const checksum = receipt.content_sha256 as Record<string, unknown>;
314
- if (
315
- typeof value.skill !== "string" ||
316
- receipt.version !== 1 ||
317
- receipt.skill !== value.skill ||
318
- (receipt.skill !== "deep-interview" &&
319
- receipt.skill !== "ralplan" &&
320
- receipt.skill !== "ultragoal" &&
321
- receipt.skill !== "team") ||
322
- (receipt.owner !== "gjc-state-cli" && receipt.owner !== "gjc-runtime" && receipt.owner !== "gjc-hook") ||
323
- typeof receipt.command !== "string" ||
324
- typeof receipt.state_path !== "string" ||
325
- typeof receipt.storage_path !== "string" ||
326
- typeof receipt.mutated_at !== "string" ||
327
- !Number.isFinite(Date.parse(receipt.mutated_at)) ||
328
- typeof receipt.fresh_until !== "string" ||
329
- !Number.isFinite(Date.parse(receipt.fresh_until)) ||
330
- (receipt.status !== "fresh" && receipt.status !== "stale") ||
331
- typeof receipt.mutation_id !== "string" ||
332
- checksum.algorithm !== "sha256" ||
333
- typeof checksum.value !== "string" ||
334
- !/^[0-9a-f]{64}$/.test(checksum.value) ||
335
- typeof checksum.covered_path !== "string" ||
336
- path.resolve(checksum.covered_path) !== path.resolve(filePath) ||
337
- typeof checksum.computed_at !== "string" ||
338
- !Number.isFinite(Date.parse(checksum.computed_at))
339
- ) {
340
- return "receipt-malformed";
341
- }
342
- return workflowEnvelopeContentSha256(value) === checksum.value ? "native-valid" : "checksum-mismatch";
343
- }
344
262
 
345
263
  export function stampWorkflowEnvelopeChecksum<T>(value: T, filePath: string, computedAt = new Date().toISOString()): T {
346
264
  if (!value || typeof value !== "object" || Array.isArray(value)) return value;
@@ -507,11 +425,6 @@ function persistedSourceRevision(value: unknown): number {
507
425
  const revision = value.source_state_revision;
508
426
  return typeof revision === "number" && Number.isFinite(revision) ? revision : persistedStateRevision(value);
509
427
  }
510
- function persistedCommittedModeRevision(value: unknown): number {
511
- if (!isPlainObject(value)) return 0;
512
- const revision = value.committed_mode_state_revision;
513
- return typeof revision === "number" && Number.isFinite(revision) ? revision : 0;
514
- }
515
428
 
516
429
  function withoutCandidateRevision(value: unknown): unknown {
517
430
  if (!isPlainObject(value)) return value;
@@ -620,12 +533,7 @@ async function writeGuardedResolvedJsonAtomic(
620
533
 
621
534
  const incomingSourceRevision =
622
535
  options.sourceRevision ?? (isPlainObject(value) ? persistedStateRevision(value) : 0);
623
- if (
624
- current !== undefined &&
625
- (options.orderingRevision !== undefined
626
- ? options.orderingRevision <= persistedCommittedModeRevision(current)
627
- : incomingSourceRevision <= persistedSourceRevision(current))
628
- ) {
536
+ if (current !== undefined && incomingSourceRevision <= persistedSourceRevision(current)) {
629
537
  return { path: filePath, written: false, reason: "stale-skip", revision: currentRevision };
630
538
  }
631
539
  const next = stampStateRevision(
@@ -659,7 +567,6 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
659
567
  const write = async (): Promise<GuardedWriteResult> => {
660
568
  const current = await readJsonIfPresentTolerant(filePath);
661
569
  const currentRevision = persistedStateRevision(current);
662
-
663
570
  if (options.policy === "source") {
664
571
  if (options.expectedRevision !== undefined && options.expectedRevision !== currentRevision) {
665
572
  throw new StateWriteConflictError(filePath, options.expectedRevision, currentRevision);
@@ -683,15 +590,9 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
683
590
  await maybeAudit(filePath, options);
684
591
  return { path: filePath, written: true, revision: currentRevision + 1, stamped: next };
685
592
  }
686
-
687
593
  const incomingSourceRevision =
688
594
  options.sourceRevision ?? (isPlainObject(value) ? persistedStateRevision(value) : 0);
689
- if (
690
- current !== undefined &&
691
- (options.orderingRevision !== undefined
692
- ? options.orderingRevision <= persistedCommittedModeRevision(current)
693
- : incomingSourceRevision <= persistedSourceRevision(current))
694
- ) {
595
+ if (current !== undefined && incomingSourceRevision <= persistedSourceRevision(current)) {
695
596
  return { path: filePath, written: false, reason: "stale-skip", revision: currentRevision };
696
597
  }
697
598
  const next = stampWorkflowEnvelopeRevisionAndChecksum(
@@ -715,67 +616,6 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
715
616
  };
716
617
  return options.lockHeld ? write() : lockResolvedWorkflowTarget(filePath, write, options.lock);
717
618
  }
718
- export async function transformGuardedWorkflowEnvelopeAtomic(
719
- targetPath: string,
720
- options: GuardedWorkflowEnvelopeTransformOptions,
721
- ): Promise<GuardedWorkflowEnvelopeTransformResult> {
722
- const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
723
- return lockResolvedWorkflowTarget(
724
- filePath,
725
- async () => {
726
- const read = await readExistingStateForMutation(filePath);
727
- if (read.kind === "absent") throw new GuardedWorkflowEnvelopeError("DI_STATE_ABSENT");
728
- if (read.kind === "corrupt") throw new GuardedWorkflowEnvelopeError("DI_STATE_CORRUPT");
729
- const receipt = verifyWorkflowEnvelopeReceiptValue(read.value, filePath);
730
- if (receipt === "receipt-malformed") throw new GuardedWorkflowEnvelopeError("DI_RECEIPT_MALFORMED");
731
- if (receipt === "receipt-missing") throw new GuardedWorkflowEnvelopeError("DI_RECEIPT_MISSING");
732
- if (receipt === "checksum-mismatch") throw new GuardedWorkflowEnvelopeError("DI_RECEIPT_CHECKSUM_MISMATCH");
733
- const legacy = receipt === "legacy";
734
- try {
735
- if (isNativeDeepInterviewV1(read.value)) validateDeepInterviewV1Envelope(read.value);
736
- options.validate?.(read.value, legacy);
737
- } catch {
738
- throw new GuardedWorkflowEnvelopeError("DI_STATE_SCHEMA_INVALID");
739
- }
740
- const phase = read.value.current_phase;
741
- if (phase === "complete" || phase === "handoff" || read.value.active === false) {
742
- throw new GuardedWorkflowEnvelopeError("DI_PHASE_NOT_REPAIRABLE");
743
- }
744
- const revision = persistedStateRevision(read.value);
745
- if (revision !== options.expectedRevision) throw new GuardedWorkflowEnvelopeError("DI_REVISION_CONFLICT");
746
- const transformed = await options.transform(read.value, { revision, legacy });
747
- if (transformed.kind === "noop")
748
- return { path: filePath, written: false, reason: "noop", revision, warnings: [] };
749
- try {
750
- if (isNativeDeepInterviewV1(transformed.value)) validateDeepInterviewV1Envelope(transformed.value);
751
- options.validate?.(transformed.value, false);
752
- } catch {
753
- throw new GuardedWorkflowEnvelopeError("DI_STATE_SCHEMA_INVALID");
754
- }
755
- const stamped = stampWorkflowEnvelopeRevisionAndChecksum(
756
- transformed.value,
757
- filePath,
758
- revision + 1,
759
- undefined,
760
- options,
761
- ) as Record<string, unknown>;
762
- const parsed = RequiredOnWriteEnvelopeSchema.safeParse(stamped);
763
- if (!parsed.success) throw new GuardedWorkflowEnvelopeError("DI_STATE_SCHEMA_INVALID");
764
- await atomicWrite(filePath, jsonText(stamped));
765
- const warnings: GuardedWorkflowEnvelopeWarning[] = [];
766
- try {
767
- await maybeAudit(filePath, options);
768
- } catch (error) {
769
- warnings.push({
770
- code: "DI_POST_COMMIT_AUDIT_FAILED",
771
- message: error instanceof Error ? error.message : String(error),
772
- });
773
- }
774
- return { path: filePath, written: true, revision: revision + 1, stamped, warnings };
775
- },
776
- options.lock,
777
- );
778
- }
779
619
 
780
620
  export async function writeJsonAtomic(
781
621
  targetPath: string,
@@ -845,63 +685,71 @@ export async function writeWorkflowEnvelopeAtomic(
845
685
  options?: StateWriterOptions,
846
686
  ): Promise<string> {
847
687
  const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
848
- const withReceipt = withWorkflowReceipt(value, buildReceipt(options));
849
- const stamped = stampWorkflowEnvelopeChecksum(withReceipt, filePath);
850
- const parsed = RequiredOnWriteEnvelopeSchema.safeParse(stamped);
851
- if (!parsed.success) {
852
- throw new Error(
853
- `Refusing to write invalid workflow state envelope to ${filePath}: ${parsed.error.issues
854
- .map(issue => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
855
- .join("; ")}`,
856
- );
857
- }
858
- // #658: internal runtime writers (ralplan/ultragoal/deep-interview/team) persist
859
- // envelopes directly, bypassing the `gjc state` CLI transition gate (`isValidTransition`,
860
- // historically the sole call site in state-runtime.ts). Re-assert that gate on every
861
- // sanctioned envelope write so internal writes cannot persist invalid state-machine phase
862
- // transitions silently. Forced writes (`gjc state ... --force`, reconcile repairs) carry
863
- // `audit.forced` and bypass, mirroring the CLI's `use --force to bypass`.
864
- //
865
- // The gate governs ACTIVE workflow progression only. Deactivation/teardown writes
866
- // (`active: false`, e.g. `gjc state clear`, which persists the universal `complete`
867
- // sentinel that is not a per-skill manifest state) leave the transition graph and are
868
- // intentionally exempt.
869
- if (options?.audit?.forced !== true && parsed.data.active === true) {
870
- const toPhase = parsed.data.current_phase.trim();
871
- if (toPhase) {
872
- // Lazy import: workflow-manifest dereferences CANONICAL_GJC_WORKFLOW_SKILLS at
873
- // module load, and active-state -> state-writer -> workflow-manifest -> active-state
874
- // is a load-time cycle. Importing at call time (after init) avoids the TDZ.
875
- const { isKnownWorkflowState, isValidTransition } = await import("./workflow-manifest");
876
- const skill = parsed.data.skill;
877
- // Structural invariant (hard): a `current_phase` absent from the skill's manifest is
878
- // never a legitimate internal write, matching the CLI/reconcile unknown-phase gate.
879
- if (!isKnownWorkflowState(skill, toPhase)) {
880
- throw new Error(
881
- `Refusing to write unknown ${skill} phase "${toPhase}" to ${filePath}: not a known ${skill} manifest state (forced writes bypass via audit.forced)`,
882
- );
883
- }
884
- // Transition invariant (#658, diagnostic-only safety net): resolve the prior phase
885
- // (caller-supplied `audit.fromPhase`, else the active persisted envelope on disk) and
886
- // flag edges the manifest does not define. Intentionally NON-blocking and audit-only
887
- // — the CLI path already hard-fails invalid edges before reaching here, and legitimate
888
- // internal repairs / ralplan short-mode stage skips move between valid states without a
889
- // direct manifest edge. It records an `invalid_transition_detected` audit entry (no
890
- // stderr) so such transitions are non-silent without breaking those flows.
891
- const fromPhase = (options?.audit?.fromPhase ?? (await readPersistedPhase(filePath)))?.trim();
892
- if (
893
- fromPhase &&
894
- fromPhase !== toPhase &&
895
- isKnownWorkflowState(skill, fromPhase) &&
896
- !isValidTransition(skill, fromPhase, toPhase)
897
- ) {
898
- await recordInvalidWorkflowTransition({ filePath, skill, fromPhase, toPhase, options });
688
+ const write = async (): Promise<string> => {
689
+ const withReceipt = withWorkflowReceipt(value, buildReceipt(options));
690
+ const stamped = stampWorkflowEnvelopeChecksum(withReceipt, filePath);
691
+ const parsed = RequiredOnWriteEnvelopeSchema.safeParse(stamped);
692
+ if (!parsed.success) {
693
+ throw new Error(
694
+ `Refusing to write invalid workflow state envelope to ${filePath}: ${parsed.error.issues
695
+ .map(issue => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
696
+ .join("; ")}`,
697
+ );
698
+ }
699
+ // #658: internal runtime writers (ralplan/ultragoal/deep-interview/team) persist
700
+ // envelopes directly, bypassing the `gjc state` CLI transition gate (`isValidTransition`,
701
+ // historically the sole call site in state-runtime.ts). Re-assert that gate on every
702
+ // sanctioned envelope write so internal writes cannot persist invalid state-machine phase
703
+ // transitions silently. Forced writes (`gjc state ... --force`, reconcile repairs) carry
704
+ // `audit.forced` and bypass, mirroring the CLI's `use --force to bypass`.
705
+ //
706
+ // The gate governs ACTIVE workflow progression only. Deactivation/teardown writes
707
+ // (`active: false`, e.g. `gjc state clear`, which persists the universal `complete`
708
+ // sentinel that is not a per-skill manifest state) leave the transition graph and are
709
+ // intentionally exempt.
710
+ if (options?.audit?.forced !== true && parsed.data.active === true) {
711
+ const toPhase = parsed.data.current_phase.trim();
712
+ if (toPhase) {
713
+ // Lazy import: workflow-manifest dereferences CANONICAL_GJC_WORKFLOW_SKILLS at
714
+ // module load, and active-state -> state-writer -> workflow-manifest -> active-state
715
+ // is a load-time cycle. Importing at call time (after init) avoids the TDZ.
716
+ const { isKnownWorkflowState, isValidTransition } = await import("./workflow-manifest");
717
+ const skill = parsed.data.skill;
718
+ // Structural invariant (hard): a `current_phase` absent from the skill's manifest is
719
+ // never a legitimate internal write, matching the CLI/reconcile unknown-phase gate.
720
+ if (!isKnownWorkflowState(skill, toPhase)) {
721
+ throw new Error(
722
+ `Refusing to write unknown ${skill} phase "${toPhase}" to ${filePath}: not a known ${skill} manifest state (forced writes bypass via audit.forced)`,
723
+ );
724
+ }
725
+ // Transition invariant (#658, diagnostic-only safety net): resolve the prior phase
726
+ // (caller-supplied `audit.fromPhase`, else the active persisted envelope on disk) and
727
+ // flag edges the manifest does not define. Intentionally NON-blocking and audit-only
728
+ // — the CLI path already hard-fails invalid edges before reaching here, and legitimate
729
+ // internal repairs / ralplan short-mode stage skips move between valid states without a
730
+ // direct manifest edge. It records an `invalid_transition_detected` audit entry (no
731
+ // stderr) so such transitions are non-silent without breaking those flows.
732
+ const fromPhase = (options?.audit?.fromPhase ?? (await readPersistedPhase(filePath)))?.trim();
733
+ if (
734
+ fromPhase &&
735
+ fromPhase !== toPhase &&
736
+ isKnownWorkflowState(skill, fromPhase) &&
737
+ !isValidTransition(skill, fromPhase, toPhase)
738
+ ) {
739
+ await recordInvalidWorkflowTransition({ filePath, skill, fromPhase, toPhase, options });
740
+ }
899
741
  }
900
742
  }
901
- }
902
- await atomicWrite(filePath, jsonText(stamped));
903
- await maybeAudit(filePath, options);
904
- return filePath;
743
+ await atomicWrite(filePath, jsonText(stamped));
744
+ await maybeAudit(filePath, options);
745
+ return filePath;
746
+ };
747
+ // Serialize with every other writer of the same state path. Without this, a
748
+ // revision-preserving envelope write (seed/spec persistence) can interleave
749
+ // inside a staged apply's check-then-write window and be silently overwritten
750
+ // (#3387 architect finding 1). Callers already inside `withWorkflowStateLock`
751
+ // for this path pass `lockHeld: true`.
752
+ return options?.lockHeld ? write() : lockResolvedWorkflowTarget(filePath, write, options?.lock);
905
753
  }
906
754
 
907
755
  export async function writeTextAtomic(targetPath: string, text: string, options?: StateWriterOptions): Promise<string> {