@cluesmith/codev 3.2.4 → 3.3.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 (358) hide show
  1. package/dashboard-dist/assets/index-BZRTEMtH.css +32 -0
  2. package/dashboard-dist/assets/{index-BZ1x7x1X.js → index-JCbkpAcV.js} +33 -33
  3. package/dashboard-dist/assets/index-JCbkpAcV.js.map +1 -0
  4. package/dashboard-dist/index.html +2 -2
  5. package/dist/agent-farm/cli.d.ts.map +1 -1
  6. package/dist/agent-farm/cli.js +142 -0
  7. package/dist/agent-farm/cli.js.map +1 -1
  8. package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
  9. package/dist/agent-farm/commands/cleanup.js +16 -0
  10. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  11. package/dist/agent-farm/commands/inbox.d.ts +42 -0
  12. package/dist/agent-farm/commands/inbox.d.ts.map +1 -0
  13. package/dist/agent-farm/commands/inbox.js +129 -0
  14. package/dist/agent-farm/commands/inbox.js.map +1 -0
  15. package/dist/agent-farm/commands/interrupt.d.ts +20 -0
  16. package/dist/agent-farm/commands/interrupt.d.ts.map +1 -0
  17. package/dist/agent-farm/commands/interrupt.js +65 -0
  18. package/dist/agent-farm/commands/interrupt.js.map +1 -0
  19. package/dist/agent-farm/commands/reset/constants.d.ts +52 -0
  20. package/dist/agent-farm/commands/reset/constants.d.ts.map +1 -0
  21. package/dist/agent-farm/commands/reset/constants.js +52 -0
  22. package/dist/agent-farm/commands/reset/constants.js.map +1 -0
  23. package/dist/agent-farm/commands/reset/context.d.ts +230 -0
  24. package/dist/agent-farm/commands/reset/context.d.ts.map +1 -0
  25. package/dist/agent-farm/commands/reset/context.js +428 -0
  26. package/dist/agent-farm/commands/reset/context.js.map +1 -0
  27. package/dist/agent-farm/commands/reset/index.d.ts +192 -0
  28. package/dist/agent-farm/commands/reset/index.d.ts.map +1 -0
  29. package/dist/agent-farm/commands/reset/index.js +488 -0
  30. package/dist/agent-farm/commands/reset/index.js.map +1 -0
  31. package/dist/agent-farm/commands/reset/receipt.d.ts +97 -0
  32. package/dist/agent-farm/commands/reset/receipt.d.ts.map +1 -0
  33. package/dist/agent-farm/commands/reset/receipt.js +149 -0
  34. package/dist/agent-farm/commands/reset/receipt.js.map +1 -0
  35. package/dist/agent-farm/commands/reset/reorient.d.ts +117 -0
  36. package/dist/agent-farm/commands/reset/reorient.d.ts.map +1 -0
  37. package/dist/agent-farm/commands/reset/reorient.js +313 -0
  38. package/dist/agent-farm/commands/reset/reorient.js.map +1 -0
  39. package/dist/agent-farm/commands/reset.d.ts +19 -0
  40. package/dist/agent-farm/commands/reset.d.ts.map +1 -0
  41. package/dist/agent-farm/commands/reset.js +292 -0
  42. package/dist/agent-farm/commands/reset.js.map +1 -0
  43. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  44. package/dist/agent-farm/commands/send.js +49 -8
  45. package/dist/agent-farm/commands/send.js.map +1 -1
  46. package/dist/agent-farm/commands/spawn-worktree.d.ts +105 -6
  47. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  48. package/dist/agent-farm/commands/spawn-worktree.js +289 -66
  49. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  50. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  51. package/dist/agent-farm/commands/spawn.js +13 -1
  52. package/dist/agent-farm/commands/spawn.js.map +1 -1
  53. package/dist/agent-farm/commands/status.d.ts.map +1 -1
  54. package/dist/agent-farm/commands/status.js +80 -8
  55. package/dist/agent-farm/commands/status.js.map +1 -1
  56. package/dist/agent-farm/db/index.d.ts +1 -1
  57. package/dist/agent-farm/db/index.d.ts.map +1 -1
  58. package/dist/agent-farm/db/index.js +80 -1
  59. package/dist/agent-farm/db/index.js.map +1 -1
  60. package/dist/agent-farm/db/mailbox.d.ts +222 -0
  61. package/dist/agent-farm/db/mailbox.d.ts.map +1 -0
  62. package/dist/agent-farm/db/mailbox.js +304 -0
  63. package/dist/agent-farm/db/mailbox.js.map +1 -0
  64. package/dist/agent-farm/db/schema.d.ts +1 -1
  65. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  66. package/dist/agent-farm/db/schema.js +35 -0
  67. package/dist/agent-farm/db/schema.js.map +1 -1
  68. package/dist/agent-farm/db/types.d.ts +45 -0
  69. package/dist/agent-farm/db/types.d.ts.map +1 -1
  70. package/dist/agent-farm/db/types.js.map +1 -1
  71. package/dist/agent-farm/lib/builder-lookup.d.ts +1 -1
  72. package/dist/agent-farm/lib/builder-lookup.js +2 -2
  73. package/dist/agent-farm/lib/builder-lookup.js.map +1 -1
  74. package/dist/agent-farm/lib/reconnect-backoff.d.ts +54 -0
  75. package/dist/agent-farm/lib/reconnect-backoff.d.ts.map +1 -0
  76. package/dist/agent-farm/lib/reconnect-backoff.js +47 -0
  77. package/dist/agent-farm/lib/reconnect-backoff.js.map +1 -0
  78. package/dist/agent-farm/lib/tower-client.d.ts +16 -6
  79. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
  80. package/dist/agent-farm/lib/tower-client.js +37 -6
  81. package/dist/agent-farm/lib/tower-client.js.map +1 -1
  82. package/dist/agent-farm/lib/tunnel-client.d.ts +69 -1
  83. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
  84. package/dist/agent-farm/lib/tunnel-client.js +218 -27
  85. package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
  86. package/dist/agent-farm/servers/cron-delivery.d.ts +72 -0
  87. package/dist/agent-farm/servers/cron-delivery.d.ts.map +1 -0
  88. package/dist/agent-farm/servers/cron-delivery.js +80 -0
  89. package/dist/agent-farm/servers/cron-delivery.js.map +1 -0
  90. package/dist/agent-farm/servers/delayed-send.d.ts +85 -0
  91. package/dist/agent-farm/servers/delayed-send.d.ts.map +1 -0
  92. package/dist/agent-farm/servers/delayed-send.js +165 -0
  93. package/dist/agent-farm/servers/delayed-send.js.map +1 -0
  94. package/dist/agent-farm/servers/gate-profiles.d.ts +67 -0
  95. package/dist/agent-farm/servers/gate-profiles.d.ts.map +1 -0
  96. package/dist/agent-farm/servers/gate-profiles.js +113 -0
  97. package/dist/agent-farm/servers/gate-profiles.js.map +1 -0
  98. package/dist/agent-farm/servers/mailbox-delivery.d.ts +370 -0
  99. package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -0
  100. package/dist/agent-farm/servers/mailbox-delivery.js +617 -0
  101. package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -0
  102. package/dist/agent-farm/servers/mailbox-wiring.d.ts +125 -0
  103. package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -0
  104. package/dist/agent-farm/servers/mailbox-wiring.js +425 -0
  105. package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -0
  106. package/dist/agent-farm/servers/message-write.d.ts +53 -1
  107. package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
  108. package/dist/agent-farm/servers/message-write.js +63 -0
  109. package/dist/agent-farm/servers/message-write.js.map +1 -1
  110. package/dist/agent-farm/servers/overview.d.ts.map +1 -1
  111. package/dist/agent-farm/servers/overview.js +23 -4
  112. package/dist/agent-farm/servers/overview.js.map +1 -1
  113. package/dist/agent-farm/servers/render-gate.d.ts +141 -0
  114. package/dist/agent-farm/servers/render-gate.d.ts.map +1 -0
  115. package/dist/agent-farm/servers/render-gate.js +251 -0
  116. package/dist/agent-farm/servers/render-gate.js.map +1 -0
  117. package/dist/agent-farm/servers/session-submit.d.ts +90 -0
  118. package/dist/agent-farm/servers/session-submit.d.ts.map +1 -0
  119. package/dist/agent-farm/servers/session-submit.js +127 -0
  120. package/dist/agent-farm/servers/session-submit.js.map +1 -0
  121. package/dist/agent-farm/servers/tower-cron.d.ts +10 -7
  122. package/dist/agent-farm/servers/tower-cron.d.ts.map +1 -1
  123. package/dist/agent-farm/servers/tower-cron.js +58 -53
  124. package/dist/agent-farm/servers/tower-cron.js.map +1 -1
  125. package/dist/agent-farm/servers/tower-instances.d.ts +14 -2
  126. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  127. package/dist/agent-farm/servers/tower-instances.js +78 -16
  128. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  129. package/dist/agent-farm/servers/tower-messages.d.ts +38 -1
  130. package/dist/agent-farm/servers/tower-messages.d.ts.map +1 -1
  131. package/dist/agent-farm/servers/tower-messages.js +106 -1
  132. package/dist/agent-farm/servers/tower-messages.js.map +1 -1
  133. package/dist/agent-farm/servers/tower-routes.d.ts +16 -4
  134. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  135. package/dist/agent-farm/servers/tower-routes.js +670 -105
  136. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  137. package/dist/agent-farm/servers/tower-server.js +163 -31
  138. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  139. package/dist/agent-farm/servers/tower-terminals.d.ts +17 -2
  140. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  141. package/dist/agent-farm/servers/tower-terminals.js +104 -122
  142. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  143. package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
  144. package/dist/agent-farm/servers/tower-tunnel.js +63 -4
  145. package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
  146. package/dist/agent-farm/servers/tower-types.d.ts +2 -1
  147. package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
  148. package/dist/agent-farm/servers/tower-utils.d.ts +64 -1
  149. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
  150. package/dist/agent-farm/servers/tower-utils.js +168 -1
  151. package/dist/agent-farm/servers/tower-utils.js.map +1 -1
  152. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
  153. package/dist/agent-farm/servers/tower-websocket.js +6 -21
  154. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  155. package/dist/agent-farm/servers/write-queue.d.ts +35 -0
  156. package/dist/agent-farm/servers/write-queue.d.ts.map +1 -0
  157. package/dist/agent-farm/servers/write-queue.js +56 -0
  158. package/dist/agent-farm/servers/write-queue.js.map +1 -0
  159. package/dist/agent-farm/types.d.ts +39 -0
  160. package/dist/agent-farm/types.d.ts.map +1 -1
  161. package/dist/agent-farm/utils/agent-names.d.ts +2 -2
  162. package/dist/agent-farm/utils/agent-names.d.ts.map +1 -1
  163. package/dist/agent-farm/utils/agent-names.js +3 -3
  164. package/dist/agent-farm/utils/agent-names.js.map +1 -1
  165. package/dist/agent-farm/utils/architect-name.d.ts +2 -2
  166. package/dist/agent-farm/utils/architect-name.d.ts.map +1 -1
  167. package/dist/agent-farm/utils/architect-name.js +3 -3
  168. package/dist/agent-farm/utils/architect-name.js.map +1 -1
  169. package/dist/agent-farm/utils/config.d.ts +13 -0
  170. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  171. package/dist/agent-farm/utils/config.js +33 -1
  172. package/dist/agent-farm/utils/config.js.map +1 -1
  173. package/dist/agent-farm/utils/harness.d.ts +103 -7
  174. package/dist/agent-farm/utils/harness.d.ts.map +1 -1
  175. package/dist/agent-farm/utils/harness.js +126 -19
  176. package/dist/agent-farm/utils/harness.js.map +1 -1
  177. package/dist/cli.d.ts.map +1 -1
  178. package/dist/cli.js +6 -32
  179. package/dist/cli.js.map +1 -1
  180. package/dist/commands/consult/cli-options.d.ts +34 -0
  181. package/dist/commands/consult/cli-options.d.ts.map +1 -0
  182. package/dist/commands/consult/cli-options.js +67 -0
  183. package/dist/commands/consult/cli-options.js.map +1 -0
  184. package/dist/commands/consult/index.d.ts +56 -2
  185. package/dist/commands/consult/index.d.ts.map +1 -1
  186. package/dist/commands/consult/index.js +272 -28
  187. package/dist/commands/consult/index.js.map +1 -1
  188. package/dist/commands/consult/metrics.d.ts +42 -0
  189. package/dist/commands/consult/metrics.d.ts.map +1 -1
  190. package/dist/commands/consult/metrics.js +129 -9
  191. package/dist/commands/consult/metrics.js.map +1 -1
  192. package/dist/commands/doctor.d.ts.map +1 -1
  193. package/dist/commands/doctor.js +71 -19
  194. package/dist/commands/doctor.js.map +1 -1
  195. package/dist/commands/porch/checks.d.ts +23 -0
  196. package/dist/commands/porch/checks.d.ts.map +1 -1
  197. package/dist/commands/porch/checks.js +60 -0
  198. package/dist/commands/porch/checks.js.map +1 -1
  199. package/dist/commands/porch/config.d.ts +20 -0
  200. package/dist/commands/porch/config.d.ts.map +1 -1
  201. package/dist/commands/porch/config.js +20 -0
  202. package/dist/commands/porch/config.js.map +1 -1
  203. package/dist/commands/porch/index.js +21 -28
  204. package/dist/commands/porch/index.js.map +1 -1
  205. package/dist/commands/porch/next.d.ts.map +1 -1
  206. package/dist/commands/porch/next.js +2 -41
  207. package/dist/commands/porch/next.js.map +1 -1
  208. package/dist/lib/config.d.ts +69 -3
  209. package/dist/lib/config.d.ts.map +1 -1
  210. package/dist/lib/config.js +67 -3
  211. package/dist/lib/config.js.map +1 -1
  212. package/dist/lib/consult-lanes.d.ts +149 -0
  213. package/dist/lib/consult-lanes.d.ts.map +1 -0
  214. package/dist/lib/consult-lanes.js +360 -0
  215. package/dist/lib/consult-lanes.js.map +1 -0
  216. package/dist/lib/github.js +1 -1
  217. package/dist/lib/github.js.map +1 -1
  218. package/dist/lib/prompt-behavior-metrics.d.ts +67 -0
  219. package/dist/lib/prompt-behavior-metrics.d.ts.map +1 -0
  220. package/dist/lib/prompt-behavior-metrics.js +290 -0
  221. package/dist/lib/prompt-behavior-metrics.js.map +1 -0
  222. package/dist/lib/skeleton.d.ts +22 -1
  223. package/dist/lib/skeleton.d.ts.map +1 -1
  224. package/dist/lib/skeleton.js +120 -0
  225. package/dist/lib/skeleton.js.map +1 -1
  226. package/dist/lib/test-env.d.ts +74 -0
  227. package/dist/lib/test-env.d.ts.map +1 -0
  228. package/dist/lib/test-env.js +92 -0
  229. package/dist/lib/test-env.js.map +1 -0
  230. package/dist/terminal/pty-manager.d.ts +2 -0
  231. package/dist/terminal/pty-manager.d.ts.map +1 -1
  232. package/dist/terminal/pty-manager.js +16 -6
  233. package/dist/terminal/pty-manager.js.map +1 -1
  234. package/dist/terminal/pty-session.d.ts +131 -0
  235. package/dist/terminal/pty-session.d.ts.map +1 -1
  236. package/dist/terminal/pty-session.js +232 -39
  237. package/dist/terminal/pty-session.js.map +1 -1
  238. package/dist/terminal/ring-buffer.d.ts +39 -6
  239. package/dist/terminal/ring-buffer.d.ts.map +1 -1
  240. package/dist/terminal/ring-buffer.js +74 -6
  241. package/dist/terminal/ring-buffer.js.map +1 -1
  242. package/dist/terminal/session-manager.d.ts +53 -0
  243. package/dist/terminal/session-manager.d.ts.map +1 -1
  244. package/dist/terminal/session-manager.js +94 -9
  245. package/dist/terminal/session-manager.js.map +1 -1
  246. package/dist/terminal/session-screen.d.ts +91 -0
  247. package/dist/terminal/session-screen.d.ts.map +1 -0
  248. package/dist/terminal/session-screen.js +135 -0
  249. package/dist/terminal/session-screen.js.map +1 -0
  250. package/dist/terminal/shellper-main.js +2 -1
  251. package/dist/terminal/shellper-main.js.map +1 -1
  252. package/dist/terminal/shellper-process.d.ts +1 -1
  253. package/dist/terminal/shellper-process.d.ts.map +1 -1
  254. package/dist/terminal/shellper-process.js +17 -8
  255. package/dist/terminal/shellper-process.js.map +1 -1
  256. package/dist/terminal/shellper-replay-buffer.d.ts +69 -7
  257. package/dist/terminal/shellper-replay-buffer.d.ts.map +1 -1
  258. package/dist/terminal/shellper-replay-buffer.js +167 -31
  259. package/dist/terminal/shellper-replay-buffer.js.map +1 -1
  260. package/package.json +8 -6
  261. package/skeleton/.claude/skills/afx/SKILL.md +14 -1
  262. package/skeleton/.claude/skills/arch-init/SKILL.md +22 -9
  263. package/skeleton/.claude/skills/arch-save/SKILL.md +183 -0
  264. package/skeleton/.claude/skills/codev/SKILL.md +31 -0
  265. package/skeleton/.claude/skills/runnable-worktrees/SKILL.md +119 -0
  266. package/skeleton/.codex/skills/afx/SKILL.md +14 -1
  267. package/skeleton/.codex/skills/arch-init/SKILL.md +22 -9
  268. package/skeleton/.codex/skills/arch-save/SKILL.md +183 -0
  269. package/skeleton/.codex/skills/codev/SKILL.md +31 -0
  270. package/skeleton/.codex/skills/runnable-worktrees/SKILL.md +119 -0
  271. package/skeleton/protocols/air/builder-prompt.md +28 -47
  272. package/skeleton/protocols/air/consult-types/impl-review.md +17 -34
  273. package/skeleton/protocols/air/consult-types/pr-review.md +14 -37
  274. package/skeleton/protocols/air/prompts/implement.md +20 -73
  275. package/skeleton/protocols/air/prompts/pr.md +22 -54
  276. package/skeleton/protocols/air/protocol.md +40 -71
  277. package/skeleton/protocols/aspir/builder-prompt.md +29 -66
  278. package/skeleton/protocols/aspir/consult-types/impl-review.md +17 -44
  279. package/skeleton/protocols/aspir/consult-types/phase-review.md +17 -44
  280. package/skeleton/protocols/aspir/consult-types/plan-review.md +14 -38
  281. package/skeleton/protocols/aspir/consult-types/pr-review.md +13 -39
  282. package/skeleton/protocols/aspir/consult-types/spec-review.md +13 -33
  283. package/skeleton/protocols/aspir/prompts/implement.md +27 -196
  284. package/skeleton/protocols/aspir/prompts/plan.md +21 -85
  285. package/skeleton/protocols/aspir/prompts/review.md +29 -228
  286. package/skeleton/protocols/aspir/prompts/specify.md +25 -111
  287. package/skeleton/protocols/aspir/protocol.json +10 -0
  288. package/skeleton/protocols/aspir/protocol.md +36 -82
  289. package/skeleton/protocols/bugfix/builder-prompt.md +23 -45
  290. package/skeleton/protocols/bugfix/consult-types/impl-review.md +21 -45
  291. package/skeleton/protocols/bugfix/consult-types/pr-review.md +18 -52
  292. package/skeleton/protocols/bugfix/prompts/fix.md +16 -61
  293. package/skeleton/protocols/bugfix/prompts/investigate.md +18 -62
  294. package/skeleton/protocols/bugfix/prompts/pr.md +22 -57
  295. package/skeleton/protocols/bugfix/protocol.md +44 -50
  296. package/skeleton/protocols/experiment/builder-prompt.md +25 -58
  297. package/skeleton/protocols/experiment/protocol.md +22 -188
  298. package/skeleton/protocols/experiment/templates/notes.md +11 -71
  299. package/skeleton/protocols/maintain/builder-prompt.md +24 -45
  300. package/skeleton/protocols/maintain/consult-types/impl-review.md +17 -44
  301. package/skeleton/protocols/maintain/consult-types/pr-review.md +13 -39
  302. package/skeleton/protocols/maintain/prompts/maintain.md +1 -1
  303. package/skeleton/protocols/maintain/protocol.md +32 -266
  304. package/skeleton/protocols/maintain/templates/maintenance-run.md +11 -0
  305. package/skeleton/protocols/pir/builder-prompt.md +30 -64
  306. package/skeleton/protocols/pir/consult-types/impl-review.md +17 -50
  307. package/skeleton/protocols/pir/consult-types/pr-review.md +11 -40
  308. package/skeleton/protocols/pir/prompts/implement.md +1 -2
  309. package/skeleton/protocols/pir/prompts/plan.md +0 -3
  310. package/skeleton/protocols/pir/prompts/review.md +1 -1
  311. package/skeleton/protocols/pir/protocol.md +54 -177
  312. package/skeleton/protocols/research/builder-prompt.md +27 -58
  313. package/skeleton/protocols/research/protocol.md +28 -153
  314. package/skeleton/protocols/spike/builder-prompt.md +30 -48
  315. package/skeleton/protocols/spike/protocol.md +24 -107
  316. package/skeleton/protocols/spike/templates/findings.md +9 -39
  317. package/skeleton/protocols/spir/builder-prompt.md +33 -62
  318. package/skeleton/protocols/spir/consult-types/impl-review.md +17 -44
  319. package/skeleton/protocols/spir/consult-types/phase-review.md +17 -44
  320. package/skeleton/protocols/spir/consult-types/plan-review.md +14 -38
  321. package/skeleton/protocols/spir/consult-types/pr-review.md +13 -39
  322. package/skeleton/protocols/spir/consult-types/spec-review.md +13 -33
  323. package/skeleton/protocols/spir/prompts/implement.md +26 -195
  324. package/skeleton/protocols/spir/prompts/plan.md +20 -84
  325. package/skeleton/protocols/spir/prompts/review.md +28 -227
  326. package/skeleton/protocols/spir/prompts/specify.md +24 -110
  327. package/skeleton/protocols/spir/protocol.json +10 -0
  328. package/skeleton/protocols/spir/protocol.md +84 -633
  329. package/skeleton/protocols/spir/templates/plan.md +28 -147
  330. package/skeleton/protocols/spir/templates/review.md +21 -80
  331. package/skeleton/protocols/spir/templates/spec.md +24 -118
  332. package/skeleton/resources/commands/agent-farm.md +259 -0
  333. package/skeleton/resources/commands/consult.md +236 -8
  334. package/skeleton/resources/commands/overview.md +1 -0
  335. package/skeleton/roles/architect.md +67 -317
  336. package/skeleton/roles/builder.md +77 -191
  337. package/skeleton/templates/AGENTS.md +2 -0
  338. package/skeleton/templates/CLAUDE.md +2 -0
  339. package/templates/tower.html +39 -2
  340. package/dashboard-dist/assets/index-BZ1x7x1X.js.map +0 -1
  341. package/dashboard-dist/assets/index-DB1KBSNE.css +0 -32
  342. package/dist/agent-farm/servers/send-buffer.d.ts +0 -59
  343. package/dist/agent-farm/servers/send-buffer.d.ts.map +0 -1
  344. package/dist/agent-farm/servers/send-buffer.js +0 -116
  345. package/dist/agent-farm/servers/send-buffer.js.map +0 -1
  346. package/skeleton/porch/prompts/defend.md +0 -108
  347. package/skeleton/porch/prompts/diagnose.md +0 -70
  348. package/skeleton/porch/prompts/evaluate.md +0 -132
  349. package/skeleton/porch/prompts/fix.md +0 -59
  350. package/skeleton/porch/prompts/implement.md +0 -88
  351. package/skeleton/porch/prompts/plan.md +0 -74
  352. package/skeleton/porch/prompts/pr.md +0 -84
  353. package/skeleton/porch/prompts/review.md +0 -249
  354. package/skeleton/porch/prompts/specify.md +0 -53
  355. package/skeleton/porch/prompts/test.md +0 -63
  356. package/skeleton/protocols/aspir/templates/plan.md +0 -184
  357. package/skeleton/protocols/aspir/templates/review.md +0 -120
  358. package/skeleton/protocols/aspir/templates/spec.md +0 -154
@@ -15,12 +15,14 @@ import { query as claudeQuery } from '@anthropic-ai/claude-agent-sdk';
15
15
  import { Codex } from '@openai/codex-sdk';
16
16
  import { readCodevFile, findWorkspaceRoot } from '../../lib/skeleton.js';
17
17
  import { resolveDefaultBranch } from '../../lib/default-branch.js';
18
- import { loadConfig } from '../../lib/config.js';
18
+ import { loadConfig, findConfigSource } from '../../lib/config.js';
19
+ import { resolveLaneModel, resolveReasoningEffort, validateModelId, assertLaneAcceptsModelOverride, } from '../../lib/consult-lanes.js';
19
20
  import { getResolver, GitRefResolver } from '../porch/artifacts.js';
20
21
  import { MetricsDB } from './metrics.js';
21
22
  import { extractUsage, extractReviewText } from './usage-extractor.js';
22
23
  import { executeForgeCommandSync } from '../../lib/forge.js';
23
24
  import { preflightAgyAuth, recordAgyAuthState } from './agy-auth-cache.js';
25
+ import { assertAgyLaneAllowedUnderTest } from '../../lib/test-env.js';
24
26
  const MODEL_CONFIGS = {
25
27
  // gemini dispatches to the Antigravity CLI (`agy`) via runAgyConsultation —
26
28
  // this entry exists only for model validation and the `pro` alias; its
@@ -50,6 +52,7 @@ function recordMetrics(ctx, extra) {
50
52
  db.record({
51
53
  timestamp: ctx.timestamp,
52
54
  model: ctx.model,
55
+ modelId: extra.modelId,
53
56
  reviewType: ctx.reviewType,
54
57
  subcommand: ctx.subcommand,
55
58
  protocol: ctx.protocol,
@@ -292,13 +295,147 @@ function commandExists(cmd) {
292
295
  return false;
293
296
  }
294
297
  }
295
- // Codex pricing for cost computation (matches values from old SUBPROCESS_MODEL_PRICING)
296
- const CODEX_PRICING = { inputPer1M: 2.00, cachedInputPer1M: 1.00, outputPer1M: 8.00 };
298
+ /**
299
+ * Shipped default model id for the codex consult lane (#1288).
300
+ *
301
+ * The `-sol` suffix is LOAD-BEARING. Both plain `gpt-5.6` and `gpt-5.6-codex`
302
+ * were live-probed on 2026-07-29 and rejected by Codex with a ChatGPT account
303
+ * ("The '<id>' model is not supported when using Codex with a ChatGPT
304
+ * account."). Do not "simplify" this id — `default-models.test.ts` guards it.
305
+ */
306
+ export const DEFAULT_CODEX_MODEL = 'gpt-5.6-sol';
307
+ /** Shipped default reasoning effort for the codex consult lane. */
308
+ export const DEFAULT_CODEX_REASONING_EFFORT = 'medium';
309
+ /** Shipped default model id for the claude consult lane (#1288). */
310
+ export const DEFAULT_CLAUDE_MODEL = 'claude-opus-5';
311
+ /**
312
+ * Per-1M-token codex rates, keyed by model id. Verified against
313
+ * https://developers.openai.com/api/docs/pricing on 2026-07-30.
314
+ *
315
+ * Only OpenAI's *standard* tier is modelled; the separate long-context tier
316
+ * (charged above the standard context threshold) is not, so cost is
317
+ * under-reported for unusually large consultations.
318
+ *
319
+ * A model id absent from this table yields `costUsd: null` rather than a cost
320
+ * computed from some other model's rates — a confidently wrong number is worse
321
+ * than none.
322
+ */
323
+ const CODEX_PRICING = {
324
+ 'gpt-5.6-sol': { inputPer1M: 5.00, cachedInputPer1M: 0.50, outputPer1M: 30.00 },
325
+ };
326
+ /**
327
+ * Compute codex consultation cost in USD, or null when the model's published
328
+ * rates are unknown.
329
+ */
330
+ export function computeCodexCost(model, inputTokens, cachedInputTokens, outputTokens, workspaceRoot) {
331
+ // `consult.pricing.codex` (spec 1286) outranks the shipped table, so a workspace running a model
332
+ // Codev has no rates for can still get real costs instead of nulls. Optional param: main's
333
+ // 4-arg callers and tests are unaffected.
334
+ const configured = workspaceRoot ? loadConfig(workspaceRoot).consult?.pricing?.codex : undefined;
335
+ const pricing = configured ?? CODEX_PRICING[model];
336
+ if (!pricing)
337
+ return null;
338
+ const uncached = inputTokens - cachedInputTokens;
339
+ return (uncached / 1_000_000) * pricing.inputPer1M
340
+ + (cachedInputTokens / 1_000_000) * pricing.cachedInputPer1M
341
+ + (outputTokens / 1_000_000) * pricing.outputPer1M;
342
+ }
343
+ /**
344
+ * Resolve which model id an SDK lane runs, and record where it came from.
345
+ *
346
+ * Precedence: `--model-id` > `consult.models.<lane>` > the shipped default constant.
347
+ *
348
+ * The provenance is not decoration: with five config layers, telling a user their
349
+ * `consult.models.codex` is wrong doesn't tell them which of five files to edit.
350
+ */
351
+ export function resolveLaneModelChoice(workspaceRoot, lane, defaultId, modelIdOverride) {
352
+ if (modelIdOverride !== undefined) {
353
+ validateModelId(modelIdOverride, '--model-id');
354
+ return { id: modelIdOverride, key: '--model-id', source: null, fromFlag: true };
355
+ }
356
+ const { id, key } = resolveLaneModel(loadConfig(workspaceRoot).consult, lane);
357
+ if (id === undefined || key === undefined) {
358
+ return { id: defaultId, key: null, source: null, fromFlag: false };
359
+ }
360
+ return { id, key, source: findConfigSource(workspaceRoot, ['consult', 'models', lane]), fromFlag: false };
361
+ }
362
+ /**
363
+ * Remove a stale review file before failing a consultation.
364
+ *
365
+ * "No review file" has to mean none *exists*, not merely that this run declined to write one.
366
+ * Porch keys off the file's presence, and consult writes to a deterministic per-iteration path — so
367
+ * a review left by an earlier run of the same iteration would be accepted as though the failed run
368
+ * had succeeded, and the phase would advance on a stale verdict. Found by codex reviewing the agy
369
+ * lane; applied to all three lanes because the exposure is identical wherever a runner throws after
370
+ * a previous run wrote output.
371
+ */
372
+ function discardStaleOutput(outputPath) {
373
+ if (!outputPath)
374
+ return;
375
+ try {
376
+ if (fs.existsSync(outputPath))
377
+ fs.unlinkSync(outputPath);
378
+ }
379
+ catch (err) {
380
+ // Best-effort — failing to unlink must not mask the underlying error. But it must not be
381
+ // silent either: this is precisely the state where porch could accept a stale review, so it
382
+ // has to be visible rather than undetectable.
383
+ console.error(`\n[warning] could not remove stale review at ${outputPath}: ` +
384
+ `${err instanceof Error ? err.message : String(err)}\n` +
385
+ `Delete it manually — porch may otherwise accept it as this iteration's review.`);
386
+ }
387
+ }
388
+ /**
389
+ * Resolve a model for a lane that has **no built-in default** — agy picks its own model when
390
+ * `--model` is absent, so there is nothing to fall back to.
391
+ *
392
+ * `null` means "omit the flag entirely", which is what preserves zero-config parity: an
393
+ * unconfigured gemini lane must produce byte-identical argv to before this spec.
394
+ */
395
+ export function resolveOptionalLaneModelChoice(workspaceRoot, lane, modelIdOverride) {
396
+ if (modelIdOverride !== undefined) {
397
+ validateModelId(modelIdOverride, '--model-id');
398
+ return { id: modelIdOverride, key: '--model-id', source: null, fromFlag: true };
399
+ }
400
+ const { id, key } = resolveLaneModel(loadConfig(workspaceRoot).consult, lane);
401
+ if (id === undefined || key === undefined)
402
+ return null;
403
+ return { id, key, source: findConfigSource(workspaceRoot, ['consult', 'models', lane]), fromFlag: false };
404
+ }
405
+ /**
406
+ * Attach model provenance to a provider rejection.
407
+ *
408
+ * Deliberately does NOT substitute a working id or otherwise recover — a bad model id must fail
409
+ * loudly. The provider's own text is preserved verbatim and merely annotated, because paraphrasing
410
+ * a provider error is how you lose the one detail that identifies the real problem.
411
+ */
412
+ function annotateModelError(err, lane, choice) {
413
+ // A shipped default can't be misconfigured by the user — nothing useful to add.
414
+ if (choice.key === null)
415
+ return err;
416
+ const providerText = err instanceof Error ? err.message : String(err);
417
+ const where = choice.fromFlag
418
+ ? 'passed via --model-id'
419
+ : `from \`${choice.key}\`${choice.source ? ` in ${choice.source}` : ''}`;
420
+ const annotated = new Error(`${providerText}\n\n` +
421
+ `The ${lane} lane requested model "${choice.id}" (${where}).\n` +
422
+ `If the provider rejected that id, correct it at the source above. ` +
423
+ `Codev does not fall back to a default model.`);
424
+ if (err instanceof Error && err.stack)
425
+ annotated.stack = err.stack;
426
+ return annotated;
427
+ }
297
428
  /**
298
429
  * Run Codex consultation via @openai/codex-sdk.
299
430
  * Mirrors runClaudeConsultation() — streams events, captures usage, records metrics.
300
431
  */
301
- export async function runCodexConsultation(queryText, role, workspaceRoot, outputPath, metricsCtx) {
432
+ export async function runCodexConsultation(queryText, role, workspaceRoot, outputPath, metricsCtx, modelChoice, reasoningEffort) {
433
+ // Absent an explicit choice (direct callers), resolve from config so behavior is identical
434
+ // whether the caller threads it through or not.
435
+ const choice = modelChoice ?? resolveLaneModelChoice(workspaceRoot, 'codex', DEFAULT_CODEX_MODEL);
436
+ const effort = reasoningEffort
437
+ ?? resolveReasoningEffort(loadConfig(workspaceRoot).consult)
438
+ ?? DEFAULT_CODEX_REASONING_EFFORT;
302
439
  const chunks = [];
303
440
  const startTime = Date.now();
304
441
  let usageData = null;
@@ -314,9 +451,9 @@ export async function runCodexConsultation(queryText, role, workspaceRoot, outpu
314
451
  },
315
452
  });
316
453
  const thread = codex.startThread({
317
- model: 'gpt-5.4',
454
+ model: choice.id,
318
455
  sandboxMode: 'read-only',
319
- modelReasoningEffort: 'medium',
456
+ modelReasoningEffort: effort,
320
457
  workingDirectory: workspaceRoot,
321
458
  });
322
459
  const { events } = await thread.runStreamed(queryText);
@@ -334,10 +471,7 @@ export async function runCodexConsultation(queryText, role, workspaceRoot, outpu
334
471
  // output_tokens already includes reasoning_output_tokens (OpenAI Responses-API
335
472
  // convention) — do NOT add the latter to cost or reasoning is double-billed.
336
473
  const output = event.usage.output_tokens;
337
- const uncached = input - cached;
338
- const cost = (uncached / 1_000_000) * CODEX_PRICING.inputPer1M
339
- + (cached / 1_000_000) * CODEX_PRICING.cachedInputPer1M
340
- + (output / 1_000_000) * CODEX_PRICING.outputPer1M;
474
+ const cost = computeCodexCost(choice.id, input, cached, output, workspaceRoot);
341
475
  usageData = { inputTokens: input, cachedInputTokens: cached, outputTokens: output, costUsd: cost };
342
476
  }
343
477
  if (event.type === 'turn.failed') {
@@ -365,7 +499,8 @@ export async function runCodexConsultation(queryText, role, workspaceRoot, outpu
365
499
  errorMessage = (err instanceof Error ? err.message : String(err)).substring(0, 500);
366
500
  exitCode = 1;
367
501
  }
368
- throw err;
502
+ discardStaleOutput(outputPath);
503
+ throw annotateModelError(err, 'codex', choice);
369
504
  }
370
505
  finally {
371
506
  // Clean up temp file
@@ -375,6 +510,7 @@ export async function runCodexConsultation(queryText, role, workspaceRoot, outpu
375
510
  if (metricsCtx) {
376
511
  const duration = (Date.now() - startTime) / 1000;
377
512
  recordMetrics(metricsCtx, {
513
+ modelId: choice.id,
378
514
  durationSeconds: duration,
379
515
  inputTokens: usageData?.inputTokens ?? null,
380
516
  cachedInputTokens: usageData?.cachedInputTokens ?? null,
@@ -420,7 +556,10 @@ export function buildClaudeConsultEnv(processEnv) {
420
556
  * Uses the SDK's query() function instead of CLI subprocess.
421
557
  * This avoids the CLAUDECODE nesting guard and enables tool use during reviews.
422
558
  */
423
- async function runClaudeConsultation(queryText, role, workspaceRoot, outputPath, metricsCtx) {
559
+ export async function runClaudeConsultation(queryText, role, workspaceRoot, outputPath, metricsCtx, modelChoice) {
560
+ // Absent an explicit choice (direct callers), resolve from config so behavior is identical
561
+ // whether the caller threads it through or not.
562
+ const choice = modelChoice ?? resolveLaneModelChoice(workspaceRoot, 'claude', DEFAULT_CLAUDE_MODEL);
424
563
  const chunks = [];
425
564
  const startTime = Date.now();
426
565
  let sdkResult;
@@ -440,7 +579,7 @@ async function runClaudeConsultation(queryText, role, workspaceRoot, outputPath,
440
579
  allowedTools: ['Read', 'Glob', 'Grep'],
441
580
  permissionMode: 'bypassPermissions',
442
581
  allowDangerouslySkipPermissions: true,
443
- model: 'claude-opus-4-6',
582
+ model: choice.id,
444
583
  maxTurns: CLAUDE_MAX_TURNS,
445
584
  maxBudgetUsd: 25,
446
585
  cwd: workspaceRoot,
@@ -481,7 +620,8 @@ async function runClaudeConsultation(queryText, role, workspaceRoot, outputPath,
481
620
  errorMessage = (err instanceof Error ? err.message : String(err)).substring(0, 500);
482
621
  exitCode = 1;
483
622
  }
484
- throw err;
623
+ discardStaleOutput(outputPath);
624
+ throw annotateModelError(err, 'claude', choice);
485
625
  }
486
626
  finally {
487
627
  if (savedClaudeCode !== undefined) {
@@ -492,6 +632,7 @@ async function runClaudeConsultation(queryText, role, workspaceRoot, outputPath,
492
632
  const duration = (Date.now() - startTime) / 1000;
493
633
  const usage = sdkResult ? extractUsage('claude', '', sdkResult) : null;
494
634
  recordMetrics(metricsCtx, {
635
+ modelId: choice.id,
495
636
  durationSeconds: duration,
496
637
  inputTokens: usage?.inputTokens ?? null,
497
638
  cachedInputTokens: usage?.cachedInputTokens ?? null,
@@ -520,6 +661,10 @@ const AGY_PRINT_TIMEOUT = '5m'; // passed to `agy --print-timeout`
520
661
  const AGY_TIMEOUT_MS = 6 * 60 * 1000; // Codev-owned hard cap (> agy's own timeout)
521
662
  // OAuth banner appears before any review text; only scan the early stream.
522
663
  const AGY_MARKER_SCAN_LIMIT = 8192;
664
+ // Bounded tail of agy's own output retained for a configured-lane hard failure. agy's rejection
665
+ // text is the only thing that explains WHY a model id was refused, but it lands in an error
666
+ // message, so it is capped rather than accumulated.
667
+ const AGY_FAILURE_TAIL_MAX_CHARS = 2000;
523
668
  /**
524
669
  * How long a prober waits, marker-free, before publishing `auth` to the shared
525
670
  * cache (#1077). The OAuth banner is the very first thing an unauthenticated agy
@@ -581,6 +726,12 @@ export function resolveAgyBin() {
581
726
  const override = process.env.CODEV_AGY_BIN;
582
727
  if (override)
583
728
  return isRealAgyCli(override) ? override : null;
729
+ // Past this point we go looking for the developer's real install, so this is
730
+ // the true chokepoint for the test-isolation guard (#1323) — not the spawn
731
+ // sites. Resolution is not passive: the PATH branch below runs
732
+ // `agyRespondsToVersion`, which *executes* the candidate binary. Guarding only
733
+ // the spawn would still let a suite run the real agy.
734
+ assertAgyLaneAllowedUnderTest();
584
735
  // Canonical install path — trusted location; realpath-reject the IDE only.
585
736
  const preferred = path.join(homedir(), '.local', 'bin', 'agy');
586
737
  if (isRealAgyCli(preferred))
@@ -638,10 +789,15 @@ function writeConsultOutput(outputPath, content) {
638
789
  fs.writeFileSync(outputPath, content);
639
790
  console.error(`\nOutput written to: ${outputPath}`);
640
791
  }
641
- function recordAgyMetrics(metricsCtx, startTime, exitCode, errorMessage) {
792
+ function recordAgyMetrics(metricsCtx, startTime, exitCode, errorMessage,
793
+ // No default: every caller states the id or states null. A default would quietly reintroduce
794
+ // the silent-NULL path that making modelId required on MetricsRecord exists to prevent.
795
+ modelId) {
642
796
  if (!metricsCtx)
643
797
  return;
644
798
  recordMetrics(metricsCtx, {
799
+ // Null on a skip with no model configured — "no model was chosen", not "we forgot".
800
+ modelId,
645
801
  durationSeconds: (Date.now() - startTime) / 1000,
646
802
  // agy --print emits plain text, no token usage → cost rows degrade gracefully (null).
647
803
  inputTokens: null,
@@ -658,15 +814,25 @@ function recordAgyMetrics(metricsCtx, startTime, exitCode, errorMessage) {
658
814
  * prompt, and NEVER blocks the run: a missing/unauthed/invalid CLI or a
659
815
  * timeout/error produces a non-blocking COMMENT skip instead of throwing.
660
816
  */
661
- async function runAgyConsultation(queryText, role, workspaceRoot, outputPath, metricsCtx) {
817
+ async function runAgyConsultation(queryText, role, workspaceRoot, outputPath, metricsCtx, modelChoice) {
662
818
  const startTime = Date.now();
819
+ // `undefined` means "resolve it yourself" (direct callers); an explicit `null` means "no model
820
+ // configured", which must stay distinguishable from "not yet resolved".
821
+ const choice = modelChoice === undefined
822
+ ? resolveOptionalLaneModelChoice(workspaceRoot, 'gemini')
823
+ : modelChoice;
824
+ // The test-isolation guard (#1323) lives inside resolveAgyBin, at the point
825
+ // where an unpinned lookup would reach the real install. It throws rather than
826
+ // falling through to the non-blocking skip below — deliberately: a misconfigured
827
+ // test must fail loudly instead of passing on a machine that happens not to have
828
+ // agy installed while spawning the real CLI on one that does.
663
829
  const bin = resolveAgyBin();
664
830
  if (!bin) {
665
831
  const reason = 'agy CLI not found (install: https://antigravity.google/cli/install.sh)';
666
832
  const content = agySkipContent(reason);
667
833
  process.stdout.write(content);
668
834
  writeConsultOutput(outputPath, content);
669
- recordAgyMetrics(metricsCtx, startTime, 0, reason);
835
+ recordAgyMetrics(metricsCtx, startTime, 0, reason, choice?.id ?? null);
670
836
  console.error(`\n[gemini (agy) skipped: ${reason}]`);
671
837
  return;
672
838
  }
@@ -680,7 +846,7 @@ async function runAgyConsultation(queryText, role, workspaceRoot, outputPath, me
680
846
  const content = agySkipContent(reason);
681
847
  process.stdout.write(content);
682
848
  writeConsultOutput(outputPath, content);
683
- recordAgyMetrics(metricsCtx, startTime, 0, reason);
849
+ recordAgyMetrics(metricsCtx, startTime, 0, reason, choice?.id ?? null);
684
850
  console.error(`\n[gemini (agy) skipped without spawning: ${reason}]`);
685
851
  return;
686
852
  }
@@ -716,6 +882,11 @@ async function runAgyConsultation(queryText, role, workspaceRoot, outputPath, me
716
882
  const args = ['--sandbox', '--print-timeout', AGY_PRINT_TIMEOUT];
717
883
  for (const d of addDirs)
718
884
  args.push('--add-dir', d);
885
+ // Omitted entirely when unconfigured, so an unconfigured lane's argv is byte-identical to
886
+ // pre-1286 and agy keeps choosing its own model. Must precede --print: agy parses --print as a
887
+ // string-valued option, so its value has to be the immediately following argument.
888
+ if (choice)
889
+ args.push('--model', choice.id);
719
890
  // agy 1.0.10 defines --print as a string-valued option, so its prompt must
720
891
  // immediately follow the flag rather than another option such as --sandbox.
721
892
  args.push('--print', promptArg);
@@ -727,7 +898,7 @@ async function runAgyConsultation(queryText, role, workspaceRoot, outputPath, me
727
898
  catch { /* best-effort */ }
728
899
  }
729
900
  };
730
- return new Promise((resolve) => {
901
+ return new Promise((resolve, reject) => {
731
902
  const proc = spawn(bin, args, {
732
903
  cwd: workspaceRoot,
733
904
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -735,6 +906,11 @@ async function runAgyConsultation(queryText, role, workspaceRoot, outputPath, me
735
906
  const outChunks = [];
736
907
  let scanBuf = '';
737
908
  let settled = false;
909
+ // stderr is watched for auth markers but otherwise discarded today, so a hard failure would
910
+ // have nothing but an exit code to report. Retain a bounded tail of BOTH streams: agy's own
911
+ // text is the only thing that explains *why* a model was rejected, and this lands in an error
912
+ // message, so it must not be unbounded.
913
+ let outputTail = '';
738
914
  // When we hold the probe lock, other consult processes are polling the cache
739
915
  // for our verdict — publish it as soon as it is knowable, and always release
740
916
  // the lock, whatever this run turns into.
@@ -766,7 +942,7 @@ async function runAgyConsultation(queryText, role, workspaceRoot, outputPath, me
766
942
  const content = agySkipContent(reason);
767
943
  process.stdout.write(content);
768
944
  writeConsultOutput(outputPath, content);
769
- recordAgyMetrics(metricsCtx, startTime, exitCode, reason);
945
+ recordAgyMetrics(metricsCtx, startTime, exitCode, reason, choice?.id ?? null);
770
946
  console.error(`\n[gemini (agy) skipped: ${reason}]`);
771
947
  resolve();
772
948
  };
@@ -774,6 +950,7 @@ async function runAgyConsultation(queryText, role, workspaceRoot, outputPath, me
774
950
  const watch = (buf, isStdout) => {
775
951
  if (isStdout)
776
952
  outChunks.push(buf);
953
+ outputTail = (outputTail + buf.toString('utf-8')).slice(-AGY_FAILURE_TAIL_MAX_CHARS);
777
954
  if (scanBuf.length < AGY_MARKER_SCAN_LIMIT) {
778
955
  scanBuf += buf.toString('utf-8');
779
956
  if (AGY_OAUTH_MARKERS.some((m) => scanBuf.includes(m))) {
@@ -806,7 +983,45 @@ async function runAgyConsultation(queryText, role, workspaceRoot, outputPath, me
806
983
  clearTimeout(timer);
807
984
  cleanup();
808
985
  const raw = Buffer.concat(outChunks).toString('utf-8').trim();
809
- if (code !== 0 || raw.length === 0 || raw.includes(AGY_NONRESPONSE_MARKER)) {
986
+ // THE PHASE 3 INVARIANT: a skip may only be reached for an ENVIRONMENT cause.
987
+ //
988
+ // Configuring `consult.models.gemini` is opting out of "quietly proceed without this lane" —
989
+ // a non-zero exit then means the model was probably rejected, and swallowing that as a
990
+ // COMMENT skip would let a typo'd model id silently reduce every review to two lanes.
991
+ //
992
+ // Deliberately narrow: ONLY a non-zero exit hard-fails. Auth, timeout, non-response and
993
+ // empty output stay skips even when configured, because those are environment causes and the
994
+ // degraded-agy lane (#1032/#1033) must keep its non-blocking property. Widening this to
995
+ // "any failure" would wedge phases for workspaces whose agy is merely unauthenticated.
996
+ // Environment causes are classified FIRST. agy can emit its non-response marker *and* exit
997
+ // non-zero, and checking the exit code before the marker would misfile that timeout as a
998
+ // configuration failure — breaking the "timeout stays a skip in both cases" half of the
999
+ // invariant for exactly the degraded lane it exists to protect. (Found by codex at review.)
1000
+ // ONLY the non-response marker, deliberately — NOT empty stdout. A rejected model id writes
1001
+ // its error to stderr and exits non-zero with empty stdout, so treating "no stdout" as an
1002
+ // environment cause would make the hard failure unreachable for the exact case it exists to
1003
+ // catch. (My own stale-review test caught that overcorrection.) Empty stdout still means
1004
+ // "no review" on the zero-exit path below.
1005
+ // Checked against BOTH streams: agy may print its timeout notice to stderr, and matching
1006
+ // stdout alone would hard-fail a configured lane on a plain timeout — the same invariant hole
1007
+ // twice over. `outputTail` is capped, but a timeout notice is by nature near the end of the
1008
+ // stream, so the tail is where it lands. (Found by claude at review.)
1009
+ const timedOutProducing = raw.includes(AGY_NONRESPONSE_MARKER) || outputTail.includes(AGY_NONRESPONSE_MARKER);
1010
+ // `code === null` means agy was killed by a signal (OOM, external kill) — an environment
1011
+ // cause, not a rejected model, so it must not hard-fail either. `code !== 0` alone is true
1012
+ // for null and would misfile it. (Found by claude at review.)
1013
+ if (code !== null && code !== 0 && choice && !timedOutProducing) {
1014
+ publishAuth();
1015
+ recordAgyMetrics(metricsCtx, startTime, code, `agy exited with code ${code}`, choice?.id ?? null);
1016
+ console.error(`\n[gemini (agy) FAILED: configured model "${choice.id}" — see error]`);
1017
+ // "No review file" must mean none EXISTS, not merely that this run wrote none.
1018
+ discardStaleOutput(outputPath);
1019
+ const providerError = new Error(`agy exited with code ${code}.` +
1020
+ (outputTail.trim() ? `\n\nagy output (last ${AGY_FAILURE_TAIL_MAX_CHARS} chars):\n${outputTail.trim()}` : ''));
1021
+ reject(annotateModelError(providerError, 'gemini', choice));
1022
+ return;
1023
+ }
1024
+ if (code !== 0 || raw.length === 0 || timedOutProducing) {
810
1025
  // A broken run tells us nothing about auth — release without a verdict
811
1026
  // and let the next call re-probe.
812
1027
  publishAuth();
@@ -818,7 +1033,7 @@ async function runAgyConsultation(queryText, role, workspaceRoot, outputPath, me
818
1033
  const content = agySkipContent(reason);
819
1034
  process.stdout.write(content);
820
1035
  writeConsultOutput(outputPath, content);
821
- recordAgyMetrics(metricsCtx, startTime, code ?? 1, reason);
1036
+ recordAgyMetrics(metricsCtx, startTime, code ?? 1, reason, choice?.id ?? null);
822
1037
  console.error(`\n[gemini (agy) skipped: ${reason}]`);
823
1038
  resolve();
824
1039
  return;
@@ -828,20 +1043,40 @@ async function runAgyConsultation(queryText, role, workspaceRoot, outputPath, me
828
1043
  // Plain-text stdout IS the review.
829
1044
  process.stdout.write(raw);
830
1045
  writeConsultOutput(outputPath, raw);
831
- recordAgyMetrics(metricsCtx, startTime, 0, null);
1046
+ recordAgyMetrics(metricsCtx, startTime, 0, null, choice?.id ?? null);
832
1047
  console.error(`\n[gemini (agy) completed in ${((Date.now() - startTime) / 1000).toFixed(1)}s]`);
833
1048
  resolve();
834
1049
  });
835
1050
  });
836
1051
  }
1052
+ /**
1053
+ * Record the model a lane actually ran, so a transcript answers "what did this use?".
1054
+ *
1055
+ * Logged from the dispatch branch that owns the resolved `choice`, NOT re-derived for display: a
1056
+ * second resolution path is exactly how `--model-id` came to be documented, parsed, and inert.
1057
+ * Naming the source too means a surprising id points at the file to edit.
1058
+ */
1059
+ function logResolvedModel(lane, id, key, effort) {
1060
+ const from = key ? ` (from ${key})` : '';
1061
+ const at = effort ? ` at ${effort} reasoning effort` : '';
1062
+ console.error(`[${lane.toUpperCase()}] model: ${id}${at}${from}`);
1063
+ }
837
1064
  /**
838
1065
  * Run the consultation — dispatches to the correct model runner.
839
1066
  */
840
- async function runConsultation(model, query, workspaceRoot, role, outputPath, metricsCtx, generalMode) {
1067
+ async function runConsultation(model, query, workspaceRoot, role, outputPath, metricsCtx, generalMode, modelIdOverride) {
1068
+ // Fail before dispatch if the selected lane cannot honour the override. Checked here rather than
1069
+ // per-branch so a lane that never reads it can't silently ignore it (codex caught exactly that for
1070
+ // hermes). Syntax is validated per-lane in resolveLaneModelChoice.
1071
+ if (modelIdOverride !== undefined) {
1072
+ assertLaneAcceptsModelOverride(model);
1073
+ }
841
1074
  // SDK-based models
842
1075
  if (model === 'claude') {
843
1076
  const startTime = Date.now();
844
- await runClaudeConsultation(query, role, workspaceRoot, outputPath, metricsCtx);
1077
+ const choice = resolveLaneModelChoice(workspaceRoot, 'claude', DEFAULT_CLAUDE_MODEL, modelIdOverride);
1078
+ logResolvedModel(model, choice.id, choice.key);
1079
+ await runClaudeConsultation(query, role, workspaceRoot, outputPath, metricsCtx, choice);
845
1080
  const duration = (Date.now() - startTime) / 1000;
846
1081
  logQuery(workspaceRoot, model, query, duration);
847
1082
  console.error(`\n[${model} completed in ${duration.toFixed(1)}s]`);
@@ -849,7 +1084,10 @@ async function runConsultation(model, query, workspaceRoot, role, outputPath, me
849
1084
  }
850
1085
  if (model === 'codex') {
851
1086
  const startTime = Date.now();
852
- await runCodexConsultation(query, role, workspaceRoot, outputPath, metricsCtx);
1087
+ const choice = resolveLaneModelChoice(workspaceRoot, 'codex', DEFAULT_CODEX_MODEL, modelIdOverride);
1088
+ const effort = resolveReasoningEffort(loadConfig(workspaceRoot).consult) ?? DEFAULT_CODEX_REASONING_EFFORT;
1089
+ logResolvedModel(model, choice.id, choice.key, effort);
1090
+ await runCodexConsultation(query, role, workspaceRoot, outputPath, metricsCtx, choice, effort);
853
1091
  const duration = (Date.now() - startTime) / 1000;
854
1092
  logQuery(workspaceRoot, model, query, duration);
855
1093
  console.error(`\n[${model} completed in ${duration.toFixed(1)}s]`);
@@ -859,7 +1097,10 @@ async function runConsultation(model, query, workspaceRoot, role, outputPath, me
859
1097
  // and non-blocking skip (see runAgyConsultation).
860
1098
  if (model === 'gemini') {
861
1099
  const startTime = Date.now();
862
- await runAgyConsultation(query, role, workspaceRoot, outputPath, metricsCtx);
1100
+ const choice = resolveOptionalLaneModelChoice(workspaceRoot, 'gemini', modelIdOverride);
1101
+ // No configured id means agy chooses; say so rather than printing a value we did not set.
1102
+ logResolvedModel(model, choice?.id ?? "agy's own default", choice?.key ?? null);
1103
+ await runAgyConsultation(query, role, workspaceRoot, outputPath, metricsCtx, choice);
863
1104
  logQuery(workspaceRoot, model, query, (Date.now() - startTime) / 1000);
864
1105
  return;
865
1106
  }
@@ -935,6 +1176,8 @@ async function runConsultation(model, query, workspaceRoot, role, outputPath, me
935
1176
  if (metricsCtx) {
936
1177
  const usage = extractUsage(model, rawOutput);
937
1178
  recordMetrics(metricsCtx, {
1179
+ // Subprocess lanes (hermes) expose no model selector — see MODEL_CONFIGURABLE_LANES.
1180
+ modelId: null,
938
1181
  durationSeconds: duration,
939
1182
  inputTokens: usage?.inputTokens ?? null,
940
1183
  cachedInputTokens: usage?.cachedInputTokens ?? null,
@@ -960,6 +1203,7 @@ async function runConsultation(model, query, workspaceRoot, role, outputPath, me
960
1203
  if (metricsCtx) {
961
1204
  const duration = (Date.now() - startTime) / 1000;
962
1205
  recordMetrics(metricsCtx, {
1206
+ modelId: null,
963
1207
  durationSeconds: duration,
964
1208
  inputTokens: null,
965
1209
  cachedInputTokens: null,
@@ -1833,7 +2077,7 @@ export async function consult(options) {
1833
2077
  }
1834
2078
  }
1835
2079
  const isGeneralMode = !hasType;
1836
- await runConsultation(model, query, workspaceRoot, role, outputPath, metricsCtx, isGeneralMode);
2080
+ await runConsultation(model, query, workspaceRoot, role, outputPath, metricsCtx, isGeneralMode, options.modelId);
1837
2081
  }
1838
2082
  // Exported for testing
1839
2083
  export { getDiffStat as _getDiffStat, buildSpecQuery as _buildSpecQuery, buildPlanQuery as _buildPlanQuery, buildPRQuery as _buildPRQuery, composePRQueryText as _composePRQueryText, computeLocalPRDiff as _computeLocalPRDiff, resolveIntegrationBase as _resolveIntegrationBase, resolveArchitectQuery as _resolveArchitectQuery, computePersistentOutputPath as _computePersistentOutputPath, MODEL_CONFIGS as _MODEL_CONFIGS, MODEL_ALIASES as _MODEL_ALIASES, runAgyConsultation as _runAgyConsultation, agySkipContent as _agySkipContent, };