@gajae-code/coding-agent 0.11.11 → 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 (344) hide show
  1. package/CHANGELOG.md +131 -4
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/cli/read-cli.d.ts +1 -0
  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-registry.d.ts +4 -0
  8. package/dist/types/config/provider-auth-health.d.ts +14 -0
  9. package/dist/types/config/provider-ranking.d.ts +59 -0
  10. package/dist/types/config/settings-schema.d.ts +83 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/exa/mcp-client.d.ts +10 -1
  13. package/dist/types/exec/bash-executor.d.ts +45 -12
  14. package/dist/types/extensibility/extensions/types.d.ts +38 -4
  15. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  16. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  17. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  18. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  19. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  20. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  21. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  22. package/dist/types/extensibility/shared-events.d.ts +6 -0
  23. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  24. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  25. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  26. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  27. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  28. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  29. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  30. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -1
  31. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  32. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  33. package/dist/types/gjc-runtime/team-runtime.d.ts +27 -1
  34. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  35. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  36. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  37. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
  38. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  39. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  40. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  41. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  42. package/dist/types/main.d.ts +1 -1
  43. package/dist/types/memories/index.d.ts +2 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +10 -19
  45. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  46. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  47. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  48. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  49. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  50. package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
  51. package/dist/types/modes/components/settings-selector.d.ts +11 -0
  52. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  53. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  54. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  55. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  56. package/dist/types/modes/interactive-mode.d.ts +2 -0
  57. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  58. package/dist/types/modes/types.d.ts +2 -0
  59. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  60. package/dist/types/reminders/star-reminder.d.ts +3 -13
  61. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  62. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  64. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  65. package/dist/types/sdk/broker/transport.d.ts +2 -0
  66. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  68. package/dist/types/sdk/bus/config.d.ts +6 -2
  69. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  70. package/dist/types/sdk/bus/index.d.ts +44 -1
  71. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
  72. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  73. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  74. package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
  75. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  76. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  77. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  78. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  79. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  80. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  81. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  82. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  83. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  84. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  85. package/dist/types/sdk/host/host.d.ts +2 -0
  86. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  87. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  88. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  89. package/dist/types/sdk/prompt-status.d.ts +23 -3
  90. package/dist/types/sdk/session.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +82 -12
  92. package/dist/types/session/artifacts.d.ts +8 -0
  93. package/dist/types/session/blob-store.d.ts +57 -4
  94. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  95. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  96. package/dist/types/session/session-manager.d.ts +48 -19
  97. package/dist/types/session/session-storage.d.ts +4 -0
  98. package/dist/types/session/streaming-output.d.ts +96 -12
  99. package/dist/types/setup/credential-import.d.ts +12 -0
  100. package/dist/types/skill-state/active-state.d.ts +12 -4
  101. package/dist/types/skill-state/workflow-hud.d.ts +9 -0
  102. package/dist/types/task/executor.d.ts +3 -2
  103. package/dist/types/task/index.d.ts +3 -1
  104. package/dist/types/task/provider-retry-status.d.ts +8 -0
  105. package/dist/types/task/receipt.d.ts +7 -1
  106. package/dist/types/task/types.d.ts +23 -0
  107. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  108. package/dist/types/tools/bash-interactive.d.ts +4 -1
  109. package/dist/types/tools/bash.d.ts +3 -1
  110. package/dist/types/tools/browser/launch.d.ts +7 -0
  111. package/dist/types/tools/fetch.d.ts +17 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -0
  113. package/dist/types/tools/index.d.ts +4 -0
  114. package/dist/types/tools/output-meta.d.ts +44 -3
  115. package/dist/types/tools/path-utils.d.ts +1 -0
  116. package/dist/types/tools/read-internals.d.ts +12 -0
  117. package/dist/types/tools/read.d.ts +45 -7
  118. package/dist/types/tools/resource-gc.d.ts +4 -0
  119. package/dist/types/tools/review.d.ts +5 -7
  120. package/dist/types/tools/subagent.d.ts +2 -0
  121. package/dist/types/tools/tool-result.d.ts +7 -1
  122. package/dist/types/utils/gh.d.ts +13 -0
  123. package/dist/types/utils/image-resize.d.ts +1 -0
  124. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  125. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  126. package/dist/types/web/search/providers/xai.d.ts +2 -0
  127. package/package.json +11 -13
  128. package/scripts/acp-conformance-agent.ts +218 -0
  129. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  130. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  131. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  132. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  133. package/scripts/resident-memory-bench.ts +1341 -0
  134. package/scripts/run-acp-conformance.ts +285 -0
  135. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  136. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  137. package/src/async/job-manager.ts +78 -20
  138. package/src/capability/index.ts +8 -0
  139. package/src/cli/fast-help.ts +1 -1
  140. package/src/cli/plugin-cli.ts +176 -18
  141. package/src/cli/read-cli.ts +2 -1
  142. package/src/cli.ts +1 -0
  143. package/src/commands/deep-interview.ts +29 -9
  144. package/src/commands/harness.ts +35 -12
  145. package/src/commands/read.ts +11 -2
  146. package/src/commands/team.ts +20 -2
  147. package/src/config/file-lock.ts +11 -2
  148. package/src/config/model-registry.ts +21 -2
  149. package/src/config/provider-auth-health.ts +42 -0
  150. package/src/config/provider-ranking.ts +119 -0
  151. package/src/config/settings-schema.ts +74 -1
  152. package/src/config/settings.ts +246 -108
  153. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  154. package/src/coordinator-mcp/server.ts +5 -12
  155. package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
  156. package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
  157. package/src/defaults/gjc/skills/team/SKILL.md +3 -1
  158. package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
  159. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  160. package/src/defaults/gjc-defaults.ts +4 -4
  161. package/src/eval/py/executor.ts +7 -2
  162. package/src/exa/mcp-client.ts +12 -3
  163. package/src/exec/bash-executor.ts +144 -27
  164. package/src/extensibility/extensions/runner.ts +15 -0
  165. package/src/extensibility/extensions/types.ts +46 -5
  166. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  167. package/src/extensibility/gjc-plugins/index.ts +19 -2
  168. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  169. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  170. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  171. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  172. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  173. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  174. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  175. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  176. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  177. package/src/extensibility/gjc-plugins/types.ts +141 -0
  178. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  179. package/src/extensibility/shared-events.ts +5 -0
  180. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  181. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  182. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  183. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  184. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  185. package/src/gjc-runtime/gc-render.ts +1 -0
  186. package/src/gjc-runtime/gc-runtime.ts +11 -1
  187. package/src/gjc-runtime/launch-tmux.ts +25 -6
  188. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  189. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  190. package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
  191. package/src/gjc-runtime/state-runtime.ts +2 -1
  192. package/src/gjc-runtime/state-writer.ts +70 -222
  193. package/src/gjc-runtime/team-launch.ts +42 -5
  194. package/src/gjc-runtime/team-runtime.ts +1085 -40
  195. package/src/gjc-runtime/team-store.ts +32 -0
  196. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  197. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  198. package/src/gjc-runtime/team-workers.ts +1 -0
  199. package/src/gjc-runtime/tmux-owner-isolation.ts +28 -0
  200. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  201. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  202. package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
  203. package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
  204. package/src/gjc-runtime/workflow-manifest.ts +38 -247
  205. package/src/hooks/native-skill-hook.ts +17 -4
  206. package/src/hooks/skill-state.ts +1 -23
  207. package/src/internal-urls/docs-index.generated.ts +21 -22
  208. package/src/internal-urls/local-root-gc.ts +127 -0
  209. package/src/main.ts +9 -5
  210. package/src/memories/index.ts +10 -0
  211. package/src/modes/DESIGN.md +98 -0
  212. package/src/modes/acp/acp-agent.ts +339 -191
  213. package/src/modes/acp/acp-event-mapper.ts +14 -7
  214. package/src/modes/acp/acp-mode.ts +39 -3
  215. package/src/modes/components/assistant-message.ts +123 -22
  216. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  217. package/src/modes/components/irc-sidebar.ts +176 -62
  218. package/src/modes/components/model-selector.ts +61 -4
  219. package/src/modes/components/notifications-settings-editor.ts +12 -1
  220. package/src/modes/components/oauth-selector.ts +96 -28
  221. package/src/modes/components/read-tool-group.ts +82 -14
  222. package/src/modes/components/session-observer-overlay.ts +457 -83
  223. package/src/modes/components/settings-selector.ts +116 -23
  224. package/src/modes/components/status-line/gh.ts +16 -0
  225. package/src/modes/components/tool-execution.ts +138 -27
  226. package/src/modes/components/tool-status-header.ts +3 -14
  227. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  228. package/src/modes/controllers/command-controller.ts +4 -0
  229. package/src/modes/controllers/event-controller.ts +109 -27
  230. package/src/modes/controllers/extension-ui-controller.ts +26 -2
  231. package/src/modes/controllers/input-controller.ts +18 -3
  232. package/src/modes/controllers/selector-controller.ts +127 -36
  233. package/src/modes/interactive-mode.ts +62 -3
  234. package/src/modes/irc-observation-ledger.ts +71 -15
  235. package/src/modes/runtime-init.ts +22 -2
  236. package/src/modes/types.ts +2 -0
  237. package/src/modes/utils/ui-helpers.ts +94 -1
  238. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  239. package/src/prompts/agents/architect.md +9 -1
  240. package/src/prompts/agents/critic.md +9 -0
  241. package/src/prompts/system/eager-todo.md +8 -4
  242. package/src/prompts/system/system-prompt.md +11 -5
  243. package/src/prompts/tools/bash.md +5 -5
  244. package/src/prompts/tools/read.md +4 -2
  245. package/src/prompts/tools/search-tool-bm25.md +5 -0
  246. package/src/prompts/tools/task.md +1 -1
  247. package/src/reminders/star-reminder.ts +4 -42
  248. package/src/runtime-mcp/manager.ts +19 -4
  249. package/src/runtime-mcp/smithery-auth.ts +17 -2
  250. package/src/runtime-mcp/smithery-connect.ts +11 -1
  251. package/src/sdk/acp/adapter.ts +22 -6
  252. package/src/sdk/broker/lifecycle.ts +25 -6
  253. package/src/sdk/broker/transport.ts +35 -3
  254. package/src/sdk/bus/attachment-registry.ts +5 -1
  255. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  256. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  257. package/src/sdk/bus/config.ts +18 -2
  258. package/src/sdk/bus/control-drain-lease.ts +104 -0
  259. package/src/sdk/bus/index.ts +984 -146
  260. package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
  261. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  262. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  263. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  264. package/src/sdk/bus/reconciliation-store.ts +303 -0
  265. package/src/sdk/bus/rich-draft.ts +1 -1
  266. package/src/sdk/bus/rich-render.ts +2 -2
  267. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  268. package/src/sdk/bus/telegram-cli.ts +33 -8
  269. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  270. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  271. package/src/sdk/bus/telegram-daemon.ts +916 -23
  272. package/src/sdk/bus/telegram-reference.ts +40 -3
  273. package/src/sdk/bus/threaded-render.ts +48 -3
  274. package/src/sdk/bus/topic-registry.ts +20 -0
  275. package/src/sdk/host/control/dispatch.ts +5 -1
  276. package/src/sdk/host/control/operations.ts +1 -1
  277. package/src/sdk/host/host.ts +23 -1
  278. package/src/sdk/host/query/handlers.ts +55 -0
  279. package/src/sdk/host/query/revision-store.ts +36 -2
  280. package/src/sdk/host/reverse-leases.ts +44 -7
  281. package/src/sdk/mcp/server.ts +46 -18
  282. package/src/sdk/prompt-status.ts +16 -3
  283. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  284. package/src/sdk/protocol/operation-registry.ts +7 -2
  285. package/src/sdk/session.ts +101 -10
  286. package/src/session/agent-session.ts +1286 -220
  287. package/src/session/artifacts.ts +22 -0
  288. package/src/session/auth-broker-config.ts +10 -3
  289. package/src/session/blob-store.ts +915 -24
  290. package/src/session/internal/managed-session-scope.ts +44 -6
  291. package/src/session/internal/managed-session-storage.ts +48 -6
  292. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  293. package/src/session/session-manager.ts +2364 -438
  294. package/src/session/session-storage.ts +9 -1
  295. package/src/session/streaming-output.ts +500 -111
  296. package/src/setup/credential-import.ts +75 -13
  297. package/src/setup/provider-onboarding.ts +19 -4
  298. package/src/skill-state/active-state.ts +32 -10
  299. package/src/skill-state/workflow-hud.ts +42 -2
  300. package/src/skill-state/workflow-mutation-guard.ts +560 -8
  301. package/src/task/executor.ts +122 -39
  302. package/src/task/index.ts +23 -14
  303. package/src/task/provider-retry-status.ts +16 -0
  304. package/src/task/receipt.ts +23 -7
  305. package/src/task/render.ts +6 -2
  306. package/src/task/types.ts +108 -0
  307. package/src/tools/ask-answer-registry.ts +12 -6
  308. package/src/tools/ask.ts +169 -118
  309. package/src/tools/ast-edit.ts +1 -0
  310. package/src/tools/ast-grep.ts +1 -0
  311. package/src/tools/bash-interactive.ts +8 -3
  312. package/src/tools/bash.ts +503 -60
  313. package/src/tools/browser/launch.ts +41 -6
  314. package/src/tools/fetch.ts +114 -23
  315. package/src/tools/find.ts +1 -0
  316. package/src/tools/image-gen.ts +29 -5
  317. package/src/tools/index.ts +4 -0
  318. package/src/tools/output-meta.ts +281 -20
  319. package/src/tools/path-utils.ts +8 -3
  320. package/src/tools/read-internals.ts +44 -0
  321. package/src/tools/read.ts +1560 -310
  322. package/src/tools/resource-gc.ts +182 -18
  323. package/src/tools/review.ts +5 -7
  324. package/src/tools/search.ts +1 -0
  325. package/src/tools/subagent-render.ts +6 -2
  326. package/src/tools/subagent.ts +6 -0
  327. package/src/tools/telegram-send.ts +6 -3
  328. package/src/tools/tool-result.ts +14 -1
  329. package/src/utils/gh.ts +39 -0
  330. package/src/utils/image-resize.ts +3 -0
  331. package/src/web/search/providers/anthropic.ts +4 -4
  332. package/src/web/search/providers/codex.ts +1 -1
  333. package/src/web/search/providers/kimi.ts +15 -5
  334. package/src/web/search/providers/searxng.ts +47 -4
  335. package/src/web/search/providers/xai.ts +7 -2
  336. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  337. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  338. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  339. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  340. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  341. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  342. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  343. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
  344. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -1,8 +1,13 @@
1
1
  import { syncSkillActiveState } from "../skill-state/active-state";
2
2
  import { deriveDeepInterviewHud } from "../skill-state/workflow-hud";
3
+ import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
4
+ import {
5
+ clampReportedAmbiguity,
6
+ computeAmbiguityFloor,
7
+ disputeFactsFromRetractedRound,
8
+ } from "./deep-interview-ambiguity";
3
9
  import {
4
10
  answerHash,
5
- applyDeepInterviewRoundResultV1,
6
11
  assertDeepInterviewInputWithinLimit,
7
12
  assertDeepInterviewIntentManifest,
8
13
  assertDeepInterviewStructuredResponseWithinLimit,
@@ -11,10 +16,8 @@ import {
11
16
  type DeepInterviewIntentItem,
12
17
  type DeepInterviewIntentSubstitution,
13
18
  type DeepInterviewRoundRecord,
14
- type DeepInterviewRoundResultV1,
15
19
  type DeepInterviewStateEnvelope,
16
20
  type DeepInterviewTriggerMetadata,
17
- deepInterviewAnswerIdentityEqual,
18
21
  deriveRoundKey,
19
22
  MAX_USER_RESPONSE_LENGTH,
20
23
  normalizeDeepInterviewEnvelope,
@@ -22,7 +25,7 @@ import {
22
25
  reviewDeepInterviewIntent,
23
26
  } from "./deep-interview-state";
24
27
  import { writeSessionActivityMarker } from "./session-resolution";
25
- import { readExistingStateForMutation, transformGuardedWorkflowEnvelopeAtomic } from "./state-writer";
28
+ import { readExistingStateForMutation, writeGuardedWorkflowEnvelopeAtomic } from "./state-writer";
26
29
 
27
30
  export * from "./deep-interview-ambiguity";
28
31
  export * from "./deep-interview-state";
@@ -69,8 +72,6 @@ export interface DeepInterviewScoringInput {
69
72
  scores: Record<string, number>;
70
73
  ambiguity: number;
71
74
  triggers?: DeepInterviewTriggerMetadata[];
72
- /** Complete native round result for typed callers. */
73
- roundResult?: DeepInterviewRoundResultV1;
74
75
  }
75
76
 
76
77
  export type AppendOrMergeAction = "created" | "noop" | "replaced";
@@ -101,21 +102,6 @@ export interface TransitionValidationResult {
101
102
  // Pure helpers: records
102
103
  // =============================================================================
103
104
 
104
- /**
105
- * `deepInterview.dimension` is agent-supplied free text on post-topology asks, but the persisted
106
- * envelope only accepts canonical dimension ids. Fold obvious case/label variants down, and drop
107
- * anything unrecognized rather than persisting a value the validator rejects.
108
- */
109
- function canonicalDimension(value: string | undefined): string | undefined {
110
- if (value === undefined) return undefined;
111
- const normalized = value.trim().toLowerCase();
112
- if (normalized === "topology") return "topology";
113
- for (const dimension of ["goal", "constraints", "criteria", "context"]) {
114
- if (normalized === dimension || normalized.startsWith(`${dimension} `)) return dimension;
115
- }
116
- return undefined;
117
- }
118
-
119
105
  export function buildAnswerShell(
120
106
  input: DeepInterviewAnswerInput,
121
107
  now: string = new Date().toISOString(),
@@ -131,7 +117,7 @@ export function buildAnswerShell(
131
117
  selected_options: input.selectedOptions,
132
118
  custom_input: input.customInput,
133
119
  component: input.component,
134
- dimension: canonicalDimension(input.dimension),
120
+ dimension: input.dimension,
135
121
  ambiguity_at_ask: input.ambiguity,
136
122
  lifecycle: "answered",
137
123
  answered_at: now,
@@ -139,9 +125,11 @@ export function buildAnswerShell(
139
125
  }
140
126
 
141
127
  /**
142
- * Append-or-merge by `round_key`. An answer is a no-op only when its complete
143
- * canonical shell identity matches; a scored record is immutable evidence and
144
- * cannot be replaced by a late answer.
128
+ * Append-or-merge by `round_key`. Exactly one record per key:
129
+ * - no existing record -> append (`created`);
130
+ * - identical question_hash + answer_hash -> deterministic no-op (`noop`);
131
+ * - same key, different hashes -> deterministic replacement of the prior shell
132
+ * (`replaced`); the prior answer for that key is superseded and lifecycle resets.
145
133
  */
146
134
  export function appendOrMergeRound(
147
135
  rounds: readonly DeepInterviewRoundRecord[],
@@ -154,10 +142,9 @@ export function appendOrMergeRound(
154
142
  return { rounds: next, action: "created", record: shell };
155
143
  }
156
144
  const existing = next[index];
157
- if (deepInterviewAnswerIdentityEqual(existing, shell) && existing.answer_hash === shell.answer_hash) {
145
+ if (existing.question_hash === shell.question_hash && existing.answer_hash === shell.answer_hash) {
158
146
  return { rounds: next, action: "noop", record: existing };
159
147
  }
160
- if (existing.lifecycle === "scored") throw new Error("DI_ANSWER_LIFECYCLE_CONFLICT");
161
148
  next[index] = shell;
162
149
  return { rounds: next, action: "replaced", record: shell };
163
150
  }
@@ -237,10 +224,10 @@ export function validateDeepInterviewScoredTransition(
237
224
  `active trigger ${trigger.kind} did not raise ambiguity (${prior.ambiguity} -> ${next.ambiguity})`,
238
225
  );
239
226
  }
240
- // The runtime derives both dimension values from scored records; callers cannot
241
- // provide parallel trigger metrics.
242
- const priorDim = prior.scores?.[trigger.dimension];
243
- const nextDim = next.scores?.[trigger.dimension];
227
+ // Affected dimension must not improve. Prefer record scores, fall back to the trigger's
228
+ // own prior/new dimension scores; absent metrics cannot prove non-improvement.
229
+ const priorDim = prior.scores?.[trigger.dimension] ?? trigger.priorDimensionScore;
230
+ const nextDim = next.scores?.[trigger.dimension] ?? trigger.newDimensionScore;
244
231
  if (typeof priorDim !== "number" || typeof nextDim !== "number") {
245
232
  violations.push(
246
233
  `active trigger ${trigger.kind} is missing dimension "${trigger.dimension}" scores to prove non-improvement`,
@@ -317,16 +304,19 @@ export function projectCompactState(value: unknown, options: { lastN?: number }
317
304
  // Persistence wrappers (state-writer backed; runtime-owned)
318
305
  // =============================================================================
319
306
 
320
- interface RecorderMutationOptions {
321
- sessionId?: string;
322
- expectedRevision?: number;
323
- }
324
-
325
307
  async function readEnvelope(statePath: string): Promise<DeepInterviewStateEnvelope> {
326
308
  const read = await readExistingStateForMutation(statePath);
327
309
  if (read.kind === "valid") return ensureDeepInterviewStateShape(read.value);
328
- if (read.kind === "absent") throw new Error("DI_STATE_ABSENT");
329
- throw new Error(`deep-interview state at ${statePath} is corrupt or tampered (${read.error})`);
310
+ if (read.kind === "corrupt") {
311
+ // Fail closed: never silently overwrite a corrupt/tampered state file. Callers
312
+ // (e.g. the ask tool) catch this and warn without mutating, preserving the file
313
+ // for recovery. Only a genuinely absent file is defaulted below.
314
+ throw new Error(
315
+ `deep-interview state at ${statePath} is corrupt or tampered (${read.error}); refusing to overwrite`,
316
+ );
317
+ }
318
+ // Absent: start from a defaulted shape.
319
+ return ensureDeepInterviewStateShape(undefined);
330
320
  }
331
321
 
332
322
  function existingStateRevision(value: unknown): number | undefined {
@@ -339,16 +329,49 @@ function interviewIdOf(envelope: DeepInterviewStateEnvelope): string | undefined
339
329
  const inner = (envelope.state ?? {}) as Record<string, unknown>;
340
330
  return typeof inner.interview_id === "string" ? inner.interview_id : undefined;
341
331
  }
342
- interface RecorderHudSyncSlot {
343
- latestCommittedRevision: number;
344
- tail: Promise<void>;
345
- }
346
332
 
347
- const recorderHudSyncSlots = new Map<string, RecorderHudSyncSlot>();
333
+ async function persistEnvelope(
334
+ cwd: string,
335
+ statePath: string,
336
+ envelope: DeepInterviewStateEnvelope,
337
+ sessionId: string | undefined,
338
+ command: string,
339
+ ): Promise<void> {
340
+ if (!sessionId) throw new Error("deep-interview recorder requires a session id");
341
+ const now = new Date().toISOString();
342
+ const payload: Record<string, unknown> = { ...normalizeDeepInterviewEnvelope(envelope), updated_at: now };
343
+ // Guarantee RequiredOnWriteEnvelopeSchema fields for the fresh/absent fallback;
344
+ // existing real state already carries these and is preserved by the spread above.
345
+ payload.skill ??= "deep-interview";
346
+ payload.version ??= WORKFLOW_STATE_VERSION;
347
+ payload.active ??= true;
348
+ payload.current_phase ??= "interviewing";
349
+ const expectedRevision = existingStateRevision(envelope);
350
+ const writeResult = await writeGuardedWorkflowEnvelopeAtomic(statePath, payload, {
351
+ cwd,
352
+ policy: "source",
353
+ expectedRevision,
354
+ receipt: { cwd, skill: "deep-interview", owner: "gjc-runtime", command, sessionId, nowIso: now },
355
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "deep-interview", sessionId },
356
+ });
357
+ // Reflect the freshly written revision back onto the in-memory envelope so a
358
+ // follow-up HUD sync derives its `sourceRevision` from the persisted revision
359
+ // (not the stale pre-write value), otherwise the active-state writer treats the
360
+ // newer HUD as stale and skips it (e.g. dropping the ambiguity chip after scoring).
361
+ if (writeResult.written && typeof expectedRevision === "number") {
362
+ (envelope as Record<string, unknown>).state_revision = expectedRevision + 1;
363
+ }
364
+ await writeSessionActivityMarker(cwd, sessionId, { writer: "deep-interview-recorder", path: statePath });
365
+ }
348
366
 
367
+ /**
368
+ * Best-effort active-state/HUD cache refresh for the deep-interview rail, derived
369
+ * from the complete normalized mode-state envelope. HUD is a cache; a failure here
370
+ * must never change durable record semantics.
371
+ */
349
372
  async function syncRecorderHud(
350
373
  cwd: string,
351
- envelope: Record<string, unknown>,
374
+ envelope: DeepInterviewStateEnvelope,
352
375
  sessionId: string | undefined,
353
376
  ): Promise<void> {
354
377
  const phase = typeof envelope.current_phase === "string" ? envelope.current_phase : "interviewing";
@@ -359,65 +382,26 @@ async function syncRecorderHud(
359
382
  phase,
360
383
  sessionId,
361
384
  source: "gjc-runtime-deep-interview-recorder",
362
- hud: deriveDeepInterviewHud(envelope, { phase }),
363
- ...(typeof envelope.state_revision === "number" ? { committedModeRevision: envelope.state_revision } : {}),
385
+ hud: deriveDeepInterviewHud(normalizeDeepInterviewEnvelope(envelope) as Record<string, unknown>, { phase }),
386
+ sourceRevision: (existingStateRevision(envelope) ?? 0) + 1,
364
387
  });
365
388
  }
366
389
 
367
- async function syncRecorderHudAtCommittedRevision(
390
+ /**
391
+ * Repair the cached HUD after a no-op append. A no-op writes no mode-state, so the
392
+ * HUD is derived from a fresh read of the current persisted state (never from the
393
+ * pre-noop in-memory envelope) to avoid overwriting newer active-state with stale values.
394
+ */
395
+ async function repairRecorderHudFromPersisted(
368
396
  cwd: string,
369
- envelope: Record<string, unknown>,
370
- sessionId: string,
371
- revision: number,
397
+ statePath: string,
398
+ sessionId: string | undefined,
372
399
  ): Promise<void> {
373
- const key = `${cwd}\0${sessionId}`;
374
- const slot = recorderHudSyncSlots.get(key) ?? { latestCommittedRevision: -1, tail: Promise.resolve() };
375
- recorderHudSyncSlots.set(key, slot);
376
- const sync = slot.tail
377
- .catch(() => undefined)
378
- .then(async () => {
379
- if (revision <= slot.latestCommittedRevision) return;
380
- await syncRecorderHud(cwd, { ...envelope, state_revision: revision }, sessionId);
381
- slot.latestCommittedRevision = revision;
382
- });
383
- slot.tail = sync;
384
- await sync;
385
- }
386
- export interface DeepInterviewPostCommitWarning {
387
- code: "DI_POST_COMMIT_AUDIT_FAILED" | "DI_POST_COMMIT_ACTIVITY_FAILED" | "DI_POST_COMMIT_HUD_FAILED";
388
- message: string;
389
- }
390
-
391
- export async function runDeepInterviewPostCommitEffects(options: {
392
- cwd: string;
393
- statePath: string;
394
- sessionId: string;
395
- envelope: Record<string, unknown>;
396
- revision: number;
397
- writer: string;
398
- auditWarnings?: readonly { code: "DI_POST_COMMIT_AUDIT_FAILED"; message: string }[];
399
- }): Promise<DeepInterviewPostCommitWarning[]> {
400
- const warnings: DeepInterviewPostCommitWarning[] = [...(options.auditWarnings ?? [])];
401
- try {
402
- await writeSessionActivityMarker(options.cwd, options.sessionId, {
403
- writer: options.writer,
404
- path: options.statePath,
405
- });
406
- } catch (error) {
407
- warnings.push({
408
- code: "DI_POST_COMMIT_ACTIVITY_FAILED",
409
- message: error instanceof Error ? error.message : String(error),
410
- });
411
- }
412
400
  try {
413
- await syncRecorderHudAtCommittedRevision(options.cwd, options.envelope, options.sessionId, options.revision);
414
- } catch (error) {
415
- warnings.push({
416
- code: "DI_POST_COMMIT_HUD_FAILED",
417
- message: error instanceof Error ? error.message : String(error),
418
- });
401
+ await syncDeepInterviewRecorderHud(cwd, statePath, sessionId);
402
+ } catch {
403
+ // HUD sync is best-effort cache maintenance and must not change record semantics.
419
404
  }
420
- return warnings;
421
405
  }
422
406
 
423
407
  /** Refresh the best-effort HUD cache from persisted deep-interview state. */
@@ -428,129 +412,110 @@ export async function syncDeepInterviewRecorderHud(
428
412
  ): Promise<void> {
429
413
  const read = await readExistingStateForMutation(statePath);
430
414
  if (read.kind !== "valid") return;
431
- const envelope = normalizeDeepInterviewEnvelope(read.value);
432
- const revision = existingStateRevision(read.value);
433
- if (revision === undefined || !sessionId) return;
434
- await syncRecorderHudAtCommittedRevision(cwd, envelope, sessionId, revision);
415
+ await syncRecorderHud(cwd, normalizeDeepInterviewEnvelope(read.value), sessionId);
435
416
  }
436
417
 
437
418
  /**
438
419
  * Record an `answered` shell for one round (append-or-merge by durable key).
439
420
  *
440
- * Replacing an already-scored answer is rejected with
441
- * `DI_ANSWER_LIFECYCLE_CONFLICT`; scored evidence is immutable.
421
+ * Retraction dynamics: replacing an already-`scored` answer is a mechanical
422
+ * contradiction signal (the user pivoted). The facts that round established are
423
+ * marked disputed, which raises the deterministic ambiguity floor immediately —
424
+ * without waiting for the LLM scorer to self-report a trigger.
442
425
  */
443
426
  export async function appendOrMergeDeepInterviewRound(
444
427
  cwd: string,
445
428
  statePath: string,
446
429
  input: DeepInterviewAnswerInput,
447
- options: RecorderMutationOptions = {},
448
- ): Promise<{
449
- action: AppendOrMergeAction;
450
- record: DeepInterviewRoundRecord;
451
- warnings: DeepInterviewPostCommitWarning[];
452
- }> {
430
+ options: { sessionId?: string } = {},
431
+ ): Promise<{ action: AppendOrMergeAction; record: DeepInterviewRoundRecord; disputedFactIds?: string[] }> {
453
432
  assertDeepInterviewStructuredResponseWithinLimit(input);
454
433
  if (input.customInput !== undefined)
455
434
  assertDeepInterviewInputWithinLimit(input.customInput, MAX_USER_RESPONSE_LENGTH, "user_response");
456
- if (!options.sessionId) throw new Error("deep-interview recorder requires a session id");
457
- const initial = await readEnvelope(statePath);
458
- let result: AppendOrMergeResult | undefined;
459
- const now = new Date().toISOString();
460
- const committed = await transformGuardedWorkflowEnvelopeAtomic(statePath, {
461
- cwd,
462
- expectedRevision: options.expectedRevision ?? existingStateRevision(initial) ?? 0,
463
- receipt: {
464
- cwd,
465
- skill: "deep-interview",
466
- owner: "gjc-runtime",
467
- command: "gjc deep-interview record-answer",
468
- sessionId: options.sessionId,
469
- nowIso: now,
470
- },
471
- audit: {
472
- category: "state",
473
- verb: "write",
474
- owner: "gjc-runtime",
475
- skill: "deep-interview",
476
- sessionId: options.sessionId,
477
- },
478
- transform: current => {
479
- const envelope = ensureDeepInterviewStateShape(current);
480
- const shell = buildAnswerShell(
481
- {
482
- ...input,
483
- interviewId: input.interviewId ?? interviewIdOf(envelope),
484
- customInput: input.intent_contract || input.intent_review ? undefined : input.customInput,
485
- },
486
- now,
487
- );
488
- result = appendOrMergeRound(readRounds(envelope), shell);
489
- const state = envelope.state as Record<string, unknown>;
490
- let intentStateChanged = false;
491
- if (input.intent_contract) {
492
- if (input.round !== 0 || input.component !== "review-topology" || input.dimension !== "topology")
493
- throw new Error("intent contract requires Round 0 topology metadata");
494
- const confirmed =
495
- input.selectedOptions?.length === 1 &&
496
- input.intent_contract.confirmation_options.includes(input.selectedOptions[0]);
497
- if (confirmed) {
498
- const contract = createDeepInterviewIntentManifest(input.intent_contract.items, {
499
- round: 0,
435
+ const envelope = await readEnvelope(statePath);
436
+ const interviewId = input.interviewId ?? interviewIdOf(envelope);
437
+ const shell = buildAnswerShell({
438
+ ...input,
439
+ interviewId,
440
+ customInput: input.intent_contract || input.intent_review ? undefined : input.customInput,
441
+ });
442
+ const rounds = readRounds(envelope);
443
+ const priorRecord = rounds.find(r => r.round_key === shell.round_key);
444
+ const result = appendOrMergeRound(rounds, shell);
445
+ const inner = envelope.state as Record<string, unknown>;
446
+ let intentStateChanged = false;
447
+ if (input.intent_contract) {
448
+ if (input.round !== 0 || input.component !== "review-topology" || input.dimension !== "topology")
449
+ throw new Error("intent contract requires Round 0 topology metadata");
450
+ const confirmed =
451
+ input.selectedOptions?.length === 1 &&
452
+ input.intent_contract.confirmation_options.includes(input.selectedOptions[0]);
453
+ if (confirmed) {
454
+ const contract = createDeepInterviewIntentManifest(input.intent_contract.items, {
455
+ round: 0,
456
+ answer_hash: shell.answer_hash,
457
+ });
458
+ const existingContract = inner.intent_contract;
459
+ if (existingContract !== undefined) {
460
+ assertDeepInterviewIntentManifest(existingContract);
461
+ if (
462
+ existingContract.digest !== contract.digest ||
463
+ existingContract.confirmation_answer_hash !== contract.confirmation_answer_hash
464
+ )
465
+ throw new Error("locked intent contract cannot be replaced");
466
+ } else {
467
+ inner.intent_contract = contract;
468
+ intentStateChanged = true;
469
+ }
470
+ }
471
+ }
472
+ if (input.intent_review) {
473
+ if (input.round <= 0) throw new Error("intent review requires a post-Round-0 answer");
474
+ const locked = inner.intent_contract;
475
+ assertDeepInterviewIntentManifest(locked);
476
+ const approved =
477
+ input.selectedOptions?.length === 1 && input.intent_review.approval_options.includes(input.selectedOptions[0]);
478
+ inner.intent_review = reviewDeepInterviewIntent(locked, input.intent_review.observed_items, {
479
+ status: approved ? "approved" : "pending",
480
+ supporting_substitutions: input.intent_review.supporting_substitutions,
481
+ ...(approved
482
+ ? {
483
+ approval_round: input.round,
500
484
  answer_hash: shell.answer_hash,
501
- });
502
- const existingContract = state.intent_contract;
503
- if (existingContract !== undefined) {
504
- assertDeepInterviewIntentManifest(existingContract);
505
- if (
506
- existingContract.digest !== contract.digest ||
507
- existingContract.confirmation_answer_hash !== contract.confirmation_answer_hash
508
- )
509
- throw new Error("locked intent contract cannot be replaced");
510
- } else {
511
- state.intent_contract = contract;
512
- intentStateChanged = true;
485
+ user_answer_evidence: `answer_hash:${shell.answer_hash}`,
513
486
  }
514
- }
515
- }
516
- if (input.intent_review) {
517
- if (input.round <= 0) throw new Error("intent review requires a post-Round-0 answer");
518
- const locked = state.intent_contract;
519
- assertDeepInterviewIntentManifest(locked);
520
- const approved =
521
- input.selectedOptions?.length === 1 &&
522
- input.intent_review.approval_options.includes(input.selectedOptions[0]);
523
- state.intent_review = reviewDeepInterviewIntent(locked, input.intent_review.observed_items, {
524
- status: approved ? "approved" : "pending",
525
- supporting_substitutions: input.intent_review.supporting_substitutions,
526
- ...(approved
527
- ? {
528
- approval_round: input.round,
529
- answer_hash: shell.answer_hash,
530
- user_answer_evidence: `answer_hash:${shell.answer_hash}`,
531
- }
532
- : {}),
533
- });
534
- intentStateChanged = true;
487
+ : {}),
488
+ });
489
+ intentStateChanged = true;
490
+ }
491
+ if (result.action === "noop" && !intentStateChanged) {
492
+ await repairRecorderHudFromPersisted(cwd, statePath, options.sessionId);
493
+ return { action: result.action, record: result.record };
494
+ }
495
+ inner.rounds = result.rounds;
496
+ let disputedFactIds: string[] | undefined;
497
+ if (result.action === "replaced" && priorRecord?.lifecycle === "scored") {
498
+ const facts = Array.isArray(inner.established_facts) ? inner.established_facts : [];
499
+ const disputed = disputeFactsFromRetractedRound(facts, shell.round);
500
+ if (disputed.disputedIds.length > 0) {
501
+ inner.established_facts = disputed.facts;
502
+ disputedFactIds = disputed.disputedIds;
503
+ // Surface the rise right away: the retraction changed the evidence, so the
504
+ // gating score must reflect the new floor before the next scoring pass.
505
+ const breakdown = computeAmbiguityFloor(inner);
506
+ inner.ambiguity_floor = breakdown;
507
+ if (typeof inner.current_ambiguity === "number") {
508
+ inner.current_ambiguity = clampReportedAmbiguity(inner.current_ambiguity, breakdown.floor).effective;
535
509
  }
536
- if (result.action === "noop" && !intentStateChanged) return { kind: "noop" as const };
537
- state.rounds = result.rounds;
538
- return { kind: "write" as const, value: { ...envelope, updated_at: now } as Record<string, unknown> };
539
- },
540
- });
541
- if (!result) throw new Error("DI_STATE_SCHEMA_INVALID");
542
- const warnings = committed.written
543
- ? await runDeepInterviewPostCommitEffects({
544
- cwd,
545
- statePath,
546
- sessionId: options.sessionId,
547
- envelope: committed.stamped,
548
- revision: committed.revision,
549
- writer: "deep-interview-recorder",
550
- auditWarnings: committed.warnings,
551
- })
552
- : [];
553
- return { action: result.action, record: result.record, warnings };
510
+ }
511
+ }
512
+ await persistEnvelope(cwd, statePath, envelope, options.sessionId, "gjc deep-interview record-answer");
513
+ try {
514
+ await syncRecorderHud(cwd, envelope, options.sessionId);
515
+ } catch {
516
+ // HUD sync is best-effort cache maintenance and must not change record semantics.
517
+ }
518
+ return { action: result.action, record: result.record, disputedFactIds };
554
519
  }
555
520
 
556
521
  /**
@@ -564,58 +529,21 @@ export async function appendOrMergeDeepInterviewRound(
564
529
  * not finite, that comparison is treated as non-matching, so no prior is selected
565
530
  * rather than risking a spurious comparison against an unrelated round.
566
531
  */
567
- function legacyScoringRoundResult(
568
- envelope: DeepInterviewStateEnvelope,
569
- input: DeepInterviewScoringInput,
570
- ): DeepInterviewRoundResultV1 {
571
- if (input.roundResult) return input.roundResult;
572
- const state = envelope.state as Record<string, unknown>;
573
- const type = state.type;
574
- const dimensions =
575
- type === "brownfield"
576
- ? ["goal", "constraints", "criteria", "context"]
577
- : type === "greenfield"
578
- ? ["goal", "constraints", "criteria"]
579
- : undefined;
580
- if (
581
- !dimensions?.every(
582
- dimension => typeof input.scores[dimension] === "number" && Number.isFinite(input.scores[dimension]),
583
- )
584
- ) {
585
- throw new Error(
586
- "DI_LEGACY_SCORING_INPUT_INSUFFICIENT: provide roundResult with complete global and component scores",
587
- );
588
- }
589
- const topology = state.topology;
590
- const hasActiveComponents =
591
- !!topology &&
592
- typeof topology === "object" &&
593
- !Array.isArray(topology) &&
594
- (() => {
595
- const topologyRecord = topology as Record<string, unknown>;
596
- if (!Array.isArray(topologyRecord.components)) return false;
597
- const components = topologyRecord.components as unknown[];
598
- return components.some(
599
- (component: unknown) =>
600
- component &&
601
- typeof component === "object" &&
602
- !Array.isArray(component) &&
603
- (component as Record<string, unknown>).active !== false &&
604
- (component as Record<string, unknown>).status !== "deferred",
605
- );
606
- })();
607
- if (hasActiveComponents) {
608
- throw new Error(
609
- "DI_LEGACY_SCORING_INPUT_INSUFFICIENT: provide roundResult with complete global and component scores",
610
- );
532
+ function latestPriorScoredRound(
533
+ rounds: readonly DeepInterviewRoundRecord[],
534
+ currentKey: string,
535
+ currentRound: number,
536
+ ): DeepInterviewRoundRecord | undefined {
537
+ if (!Number.isFinite(currentRound)) return undefined;
538
+ let prior: DeepInterviewRoundRecord | undefined;
539
+ for (const candidate of rounds) {
540
+ if (candidate.lifecycle !== "scored") continue;
541
+ if (candidate.round_key === currentKey) continue;
542
+ if (!Number.isFinite(candidate.round)) continue;
543
+ if (!(candidate.round < currentRound)) continue;
544
+ if (prior === undefined || candidate.round > prior.round) prior = candidate;
611
545
  }
612
- return {
613
- global_scores: Object.fromEntries(
614
- dimensions.map(dimension => [dimension, input.scores[dimension]]),
615
- ) as DeepInterviewRoundResultV1["global_scores"],
616
- component_scores: {},
617
- ...(input.triggers === undefined ? {} : { triggers: input.triggers }),
618
- };
546
+ return prior;
619
547
  }
620
548
 
621
549
  /** Merge scoring output into the same round record, transitioning to `scored`. */
@@ -623,64 +551,53 @@ export async function enrichDeepInterviewRoundScoring(
623
551
  cwd: string,
624
552
  statePath: string,
625
553
  input: DeepInterviewScoringInput,
626
- options: RecorderMutationOptions = {},
627
- ): Promise<{ record: DeepInterviewRoundRecord; warnings: DeepInterviewPostCommitWarning[] }> {
554
+ options: { sessionId?: string } = {},
555
+ ): Promise<{ record: DeepInterviewRoundRecord }> {
628
556
  assertDeepInterviewStructuredResponseWithinLimit(input);
629
- if (!options.sessionId) throw new Error("deep-interview recorder requires a session id");
630
- const initial = await readEnvelope(statePath);
631
- const now = new Date().toISOString();
632
- let record: DeepInterviewRoundRecord | undefined;
633
- const committed = await transformGuardedWorkflowEnvelopeAtomic(statePath, {
634
- cwd,
635
- expectedRevision: options.expectedRevision ?? existingStateRevision(initial) ?? 0,
636
- receipt: {
637
- cwd,
638
- skill: "deep-interview",
639
- owner: "gjc-runtime",
640
- command: "gjc deep-interview score-round",
641
- sessionId: options.sessionId,
642
- nowIso: now,
643
- },
644
- audit: {
645
- category: "state",
646
- verb: "write",
647
- owner: "gjc-runtime",
648
- skill: "deep-interview",
649
- sessionId: options.sessionId,
650
- },
651
- transform: current => {
652
- const envelope = ensureDeepInterviewStateShape(current);
653
- const interviewId = input.interviewId ?? interviewIdOf(envelope);
654
- const roundKey = deriveRoundKey(interviewId, input);
655
- const outcome = applyDeepInterviewRoundResultV1(
656
- envelope,
657
- roundKey,
658
- legacyScoringRoundResult(envelope, input),
659
- now,
660
- );
661
- const next = ensureDeepInterviewStateShape(outcome.envelope);
662
- record = readRounds(next).find(round => round.round_key === roundKey);
663
- if (!record) throw new Error("DI_STATE_SCHEMA_INVALID");
664
- if (outcome.kind === "noop") return { kind: "noop" as const };
665
- return {
666
- kind: "write" as const,
667
- value: { ...next, schema_version: 1, updated_at: now } as Record<string, unknown>,
668
- };
669
- },
557
+ const envelope = await readEnvelope(statePath);
558
+ const interviewId = input.interviewId ?? interviewIdOf(envelope);
559
+ const rounds = readRounds(envelope);
560
+ const { rounds: enrichedRounds, record: reportedRecord } = enrichRoundWithScoring(rounds, {
561
+ ...input,
562
+ interviewId,
670
563
  });
671
- if (!record) throw new Error("DI_STATE_SCHEMA_INVALID");
672
- const warnings = committed.written
673
- ? await runDeepInterviewPostCommitEffects({
674
- cwd,
675
- statePath,
676
- sessionId: options.sessionId,
677
- envelope: committed.stamped,
678
- revision: committed.revision,
679
- writer: "deep-interview-recorder",
680
- auditWarnings: committed.warnings,
681
- })
682
- : [];
683
- return { record, warnings };
564
+ // Deterministic floor (Ouroboros principle): the LLM-reported ambiguity is only a
565
+ // lower-bound input. Evidence persisted in state — unresolved disputed facts,
566
+ // unscored active components, auto-answer dilution — sets a code-computed floor
567
+ // the reported score can never fall under, so a pivot mechanically raises the
568
+ // gating score even when the scorer omits triggers.
569
+ const inner = envelope.state as Record<string, unknown>;
570
+ const breakdown = computeAmbiguityFloor({ ...inner, rounds: enrichedRounds });
571
+ const clampResult = clampReportedAmbiguity(input.ambiguity, breakdown.floor);
572
+ let record = reportedRecord;
573
+ let nextRounds = enrichedRounds;
574
+ if (clampResult.clamped) {
575
+ record = {
576
+ ...reportedRecord,
577
+ ambiguity: clampResult.effective,
578
+ reported_ambiguity: input.ambiguity,
579
+ ambiguity_floor: breakdown.floor,
580
+ };
581
+ nextRounds = enrichedRounds.map(item => (item.round_key === record.round_key ? record : item));
582
+ }
583
+ // Fail closed: a scored transition that violates the bidirectional invariant
584
+ // (an active trigger that improves the affected dimension or fails to raise
585
+ // overall ambiguity, or a disputed/unresolved trigger lacking a rationale) must
586
+ // never be persisted — storing it lets the interview falsely converge. Validate
587
+ // the effective (floor-clamped) record against the most recent prior scored round.
588
+ const prior = latestPriorScoredRound(rounds, record.round_key, record.round);
589
+ const validation = validateDeepInterviewScoredTransition(prior, record);
590
+ if (!validation.ok) {
591
+ throw new Error(
592
+ `deep-interview scored transition for round ${record.round} is invalid and was refused: ${validation.violations.join("; ")}`,
593
+ );
594
+ }
595
+ inner.rounds = nextRounds;
596
+ inner.current_ambiguity = clampResult.effective;
597
+ inner.ambiguity_floor = breakdown;
598
+ await persistEnvelope(cwd, statePath, envelope, options.sessionId, "gjc deep-interview score-round");
599
+ await syncRecorderHud(cwd, envelope, options.sessionId);
600
+ return { record };
684
601
  }
685
602
 
686
603
  /** Compact projection so callers read a slice instead of the full transcript. */