@cluesmith/codev 2.0.2 → 2.0.6

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 (312) hide show
  1. package/dashboard/dist/assets/index-B-s8BA2l.js +135 -0
  2. package/dashboard/dist/assets/index-B-s8BA2l.js.map +1 -0
  3. package/dashboard/dist/assets/index-DB2AxRP7.css +32 -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 +32 -14
  7. package/dist/agent-farm/cli.js.map +1 -1
  8. package/dist/agent-farm/commands/architect.d.ts +1 -1
  9. package/dist/agent-farm/commands/architect.js +3 -3
  10. package/dist/agent-farm/commands/architect.js.map +1 -1
  11. package/dist/agent-farm/commands/attach.d.ts +19 -0
  12. package/dist/agent-farm/commands/attach.d.ts.map +1 -1
  13. package/dist/agent-farm/commands/attach.js +172 -12
  14. package/dist/agent-farm/commands/attach.js.map +1 -1
  15. package/dist/agent-farm/commands/cleanup.js +6 -6
  16. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  17. package/dist/agent-farm/commands/open.js +5 -5
  18. package/dist/agent-farm/commands/open.js.map +1 -1
  19. package/dist/agent-farm/commands/send.d.ts +22 -2
  20. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  21. package/dist/agent-farm/commands/send.js +100 -181
  22. package/dist/agent-farm/commands/send.js.map +1 -1
  23. package/dist/agent-farm/commands/shell.js +5 -5
  24. package/dist/agent-farm/commands/shell.js.map +1 -1
  25. package/dist/agent-farm/commands/spawn-roles.d.ts +3 -9
  26. package/dist/agent-farm/commands/spawn-roles.d.ts.map +1 -1
  27. package/dist/agent-farm/commands/spawn-roles.js +14 -53
  28. package/dist/agent-farm/commands/spawn-roles.js.map +1 -1
  29. package/dist/agent-farm/commands/spawn-worktree.d.ts +11 -17
  30. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  31. package/dist/agent-farm/commands/spawn-worktree.js +32 -13
  32. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  33. package/dist/agent-farm/commands/spawn.d.ts +8 -6
  34. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  35. package/dist/agent-farm/commands/spawn.js +183 -69
  36. package/dist/agent-farm/commands/spawn.js.map +1 -1
  37. package/dist/agent-farm/commands/start.d.ts +4 -4
  38. package/dist/agent-farm/commands/start.js +16 -16
  39. package/dist/agent-farm/commands/start.js.map +1 -1
  40. package/dist/agent-farm/commands/status.d.ts +1 -1
  41. package/dist/agent-farm/commands/status.d.ts.map +1 -1
  42. package/dist/agent-farm/commands/status.js +15 -26
  43. package/dist/agent-farm/commands/status.js.map +1 -1
  44. package/dist/agent-farm/commands/stop.d.ts +4 -4
  45. package/dist/agent-farm/commands/stop.js +9 -9
  46. package/dist/agent-farm/commands/stop.js.map +1 -1
  47. package/dist/agent-farm/db/index.d.ts.map +1 -1
  48. package/dist/agent-farm/db/index.js +82 -7
  49. package/dist/agent-farm/db/index.js.map +1 -1
  50. package/dist/agent-farm/db/schema.d.ts +2 -2
  51. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  52. package/dist/agent-farm/db/schema.js +21 -4
  53. package/dist/agent-farm/db/schema.js.map +1 -1
  54. package/dist/agent-farm/lib/tower-client.d.ts +36 -26
  55. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
  56. package/dist/agent-farm/lib/tower-client.js +50 -25
  57. package/dist/agent-farm/lib/tower-client.js.map +1 -1
  58. package/dist/agent-farm/lib/tunnel-client.d.ts +12 -2
  59. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
  60. package/dist/agent-farm/lib/tunnel-client.js +59 -1
  61. package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
  62. package/dist/agent-farm/servers/overview.d.ts +111 -0
  63. package/dist/agent-farm/servers/overview.d.ts.map +1 -0
  64. package/dist/agent-farm/servers/overview.js +385 -0
  65. package/dist/agent-farm/servers/overview.js.map +1 -0
  66. package/dist/agent-farm/servers/tower-instances.d.ts +18 -20
  67. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  68. package/dist/agent-farm/servers/tower-instances.js +97 -100
  69. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  70. package/dist/agent-farm/servers/tower-messages.d.ts +87 -0
  71. package/dist/agent-farm/servers/tower-messages.d.ts.map +1 -0
  72. package/dist/agent-farm/servers/tower-messages.js +202 -0
  73. package/dist/agent-farm/servers/tower-messages.js.map +1 -0
  74. package/dist/agent-farm/servers/tower-routes.d.ts +1 -1
  75. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  76. package/dist/agent-farm/servers/tower-routes.js +343 -174
  77. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  78. package/dist/agent-farm/servers/tower-server.js +50 -21
  79. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  80. package/dist/agent-farm/servers/tower-terminals.d.ts +35 -31
  81. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  82. package/dist/agent-farm/servers/tower-terminals.js +208 -184
  83. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  84. package/dist/agent-farm/servers/tower-tunnel.d.ts +2 -2
  85. package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
  86. package/dist/agent-farm/servers/tower-tunnel.js +12 -12
  87. package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
  88. package/dist/agent-farm/servers/tower-types.d.ts +8 -12
  89. package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
  90. package/dist/agent-farm/servers/tower-utils.d.ts +9 -9
  91. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
  92. package/dist/agent-farm/servers/tower-utils.js +18 -18
  93. package/dist/agent-farm/servers/tower-utils.js.map +1 -1
  94. package/dist/agent-farm/servers/tower-websocket.d.ts +2 -2
  95. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
  96. package/dist/agent-farm/servers/tower-websocket.js +39 -18
  97. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  98. package/dist/agent-farm/types.d.ts +5 -6
  99. package/dist/agent-farm/types.d.ts.map +1 -1
  100. package/dist/agent-farm/utils/agent-names.d.ts +85 -0
  101. package/dist/agent-farm/utils/agent-names.d.ts.map +1 -0
  102. package/dist/agent-farm/utils/agent-names.js +140 -0
  103. package/dist/agent-farm/utils/agent-names.js.map +1 -0
  104. package/dist/agent-farm/utils/config.d.ts +1 -1
  105. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  106. package/dist/agent-farm/utils/config.js +16 -16
  107. package/dist/agent-farm/utils/config.js.map +1 -1
  108. package/dist/agent-farm/utils/file-tabs.d.ts +3 -3
  109. package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -1
  110. package/dist/agent-farm/utils/file-tabs.js +9 -9
  111. package/dist/agent-farm/utils/file-tabs.js.map +1 -1
  112. package/dist/agent-farm/utils/index.d.ts +0 -1
  113. package/dist/agent-farm/utils/index.d.ts.map +1 -1
  114. package/dist/agent-farm/utils/index.js +0 -1
  115. package/dist/agent-farm/utils/index.js.map +1 -1
  116. package/dist/agent-farm/utils/message-format.d.ts +17 -0
  117. package/dist/agent-farm/utils/message-format.d.ts.map +1 -0
  118. package/dist/agent-farm/utils/message-format.js +41 -0
  119. package/dist/agent-farm/utils/message-format.js.map +1 -0
  120. package/dist/agent-farm/utils/notifications.d.ts +4 -4
  121. package/dist/agent-farm/utils/notifications.d.ts.map +1 -1
  122. package/dist/agent-farm/utils/notifications.js +18 -18
  123. package/dist/agent-farm/utils/notifications.js.map +1 -1
  124. package/dist/cli.d.ts.map +1 -1
  125. package/dist/cli.js +26 -1
  126. package/dist/cli.js.map +1 -1
  127. package/dist/commands/adopt.d.ts +2 -2
  128. package/dist/commands/adopt.d.ts.map +1 -1
  129. package/dist/commands/adopt.js +13 -15
  130. package/dist/commands/adopt.js.map +1 -1
  131. package/dist/commands/consult/index.d.ts +26 -2
  132. package/dist/commands/consult/index.d.ts.map +1 -1
  133. package/dist/commands/consult/index.js +296 -83
  134. package/dist/commands/consult/index.js.map +1 -1
  135. package/dist/commands/consult/metrics.d.ts +90 -0
  136. package/dist/commands/consult/metrics.d.ts.map +1 -0
  137. package/dist/commands/consult/metrics.js +203 -0
  138. package/dist/commands/consult/metrics.js.map +1 -0
  139. package/dist/commands/consult/stats.d.ts +18 -0
  140. package/dist/commands/consult/stats.d.ts.map +1 -0
  141. package/dist/commands/consult/stats.js +150 -0
  142. package/dist/commands/consult/stats.js.map +1 -0
  143. package/dist/commands/consult/usage-extractor.d.ts +38 -0
  144. package/dist/commands/consult/usage-extractor.d.ts.map +1 -0
  145. package/dist/commands/consult/usage-extractor.js +99 -0
  146. package/dist/commands/consult/usage-extractor.js.map +1 -0
  147. package/dist/commands/doctor.d.ts.map +1 -1
  148. package/dist/commands/doctor.js +11 -9
  149. package/dist/commands/doctor.js.map +1 -1
  150. package/dist/commands/import.js +4 -4
  151. package/dist/commands/import.js.map +1 -1
  152. package/dist/commands/init.d.ts +2 -2
  153. package/dist/commands/init.d.ts.map +1 -1
  154. package/dist/commands/init.js +13 -15
  155. package/dist/commands/init.js.map +1 -1
  156. package/dist/commands/porch/index.d.ts +6 -6
  157. package/dist/commands/porch/index.d.ts.map +1 -1
  158. package/dist/commands/porch/index.js +37 -37
  159. package/dist/commands/porch/index.js.map +1 -1
  160. package/dist/commands/porch/next.d.ts +1 -1
  161. package/dist/commands/porch/next.d.ts.map +1 -1
  162. package/dist/commands/porch/next.js +86 -92
  163. package/dist/commands/porch/next.js.map +1 -1
  164. package/dist/commands/porch/notify.d.ts +11 -0
  165. package/dist/commands/porch/notify.d.ts.map +1 -0
  166. package/dist/commands/porch/notify.js +30 -0
  167. package/dist/commands/porch/notify.js.map +1 -0
  168. package/dist/commands/porch/plan.d.ts +1 -1
  169. package/dist/commands/porch/plan.d.ts.map +1 -1
  170. package/dist/commands/porch/plan.js +3 -3
  171. package/dist/commands/porch/plan.js.map +1 -1
  172. package/dist/commands/porch/prompts.d.ts +10 -1
  173. package/dist/commands/porch/prompts.d.ts.map +1 -1
  174. package/dist/commands/porch/prompts.js +59 -35
  175. package/dist/commands/porch/prompts.js.map +1 -1
  176. package/dist/commands/porch/protocol.d.ts +1 -1
  177. package/dist/commands/porch/protocol.d.ts.map +1 -1
  178. package/dist/commands/porch/protocol.js +8 -8
  179. package/dist/commands/porch/protocol.js.map +1 -1
  180. package/dist/commands/porch/state.d.ts +6 -6
  181. package/dist/commands/porch/state.d.ts.map +1 -1
  182. package/dist/commands/porch/state.js +14 -12
  183. package/dist/commands/porch/state.js.map +1 -1
  184. package/dist/commands/update.d.ts.map +1 -1
  185. package/dist/commands/update.js +10 -11
  186. package/dist/commands/update.js.map +1 -1
  187. package/dist/lib/github.d.ts +81 -0
  188. package/dist/lib/github.d.ts.map +1 -0
  189. package/dist/lib/github.js +141 -0
  190. package/dist/lib/github.js.map +1 -0
  191. package/dist/lib/scaffold.d.ts +13 -21
  192. package/dist/lib/scaffold.d.ts.map +1 -1
  193. package/dist/lib/scaffold.js +34 -57
  194. package/dist/lib/scaffold.js.map +1 -1
  195. package/dist/lib/skeleton.d.ts +7 -7
  196. package/dist/lib/skeleton.d.ts.map +1 -1
  197. package/dist/lib/skeleton.js +10 -10
  198. package/dist/lib/skeleton.js.map +1 -1
  199. package/dist/terminal/index.d.ts +14 -0
  200. package/dist/terminal/index.d.ts.map +1 -1
  201. package/dist/terminal/index.js +12 -0
  202. package/dist/terminal/index.js.map +1 -1
  203. package/dist/terminal/pty-manager.d.ts +1 -1
  204. package/dist/terminal/pty-manager.d.ts.map +1 -1
  205. package/dist/terminal/pty-manager.js +10 -7
  206. package/dist/terminal/pty-manager.js.map +1 -1
  207. package/dist/terminal/pty-session.js +3 -3
  208. package/dist/terminal/pty-session.js.map +1 -1
  209. package/dist/terminal/session-manager.d.ts +64 -0
  210. package/dist/terminal/session-manager.d.ts.map +1 -1
  211. package/dist/terminal/session-manager.js +299 -10
  212. package/dist/terminal/session-manager.js.map +1 -1
  213. package/dist/terminal/shellper-client.d.ts +2 -1
  214. package/dist/terminal/shellper-client.d.ts.map +1 -1
  215. package/dist/terminal/shellper-client.js +4 -2
  216. package/dist/terminal/shellper-client.js.map +1 -1
  217. package/dist/terminal/shellper-main.js +33 -4
  218. package/dist/terminal/shellper-main.js.map +1 -1
  219. package/dist/terminal/shellper-process.d.ts +24 -7
  220. package/dist/terminal/shellper-process.d.ts.map +1 -1
  221. package/dist/terminal/shellper-process.js +139 -36
  222. package/dist/terminal/shellper-process.js.map +1 -1
  223. package/dist/terminal/shellper-protocol.d.ts +1 -0
  224. package/dist/terminal/shellper-protocol.d.ts.map +1 -1
  225. package/dist/terminal/shellper-protocol.js.map +1 -1
  226. package/package.json +4 -1
  227. package/skeleton/.claude/skills/af/SKILL.md +7 -7
  228. package/skeleton/.claude/skills/consult/SKILL.md +1 -1
  229. package/skeleton/builders.md +2 -2
  230. package/skeleton/maintain/.gitkeep +1 -1
  231. package/skeleton/porch/prompts/specify.md +1 -1
  232. package/skeleton/protocols/bugfix/prompts/pr.md +15 -4
  233. package/skeleton/protocols/experiment/protocol.md +17 -17
  234. package/skeleton/protocols/maintain/prompts/audit.md +2 -2
  235. package/skeleton/protocols/maintain/prompts/sync.md +1 -1
  236. package/skeleton/protocols/maintain/prompts/verify.md +1 -1
  237. package/skeleton/protocols/maintain/protocol.md +8 -9
  238. package/skeleton/protocols/maintain/templates/maintenance-run.md +2 -2
  239. package/skeleton/protocols/spir/protocol.json +5 -5
  240. package/skeleton/protocols/spir/protocol.md +8 -8
  241. package/skeleton/protocols/tick/protocol.md +31 -31
  242. package/skeleton/resources/commands/agent-farm.md +14 -14
  243. package/skeleton/resources/commands/codev.md +0 -1
  244. package/skeleton/resources/commands/consult.md +3 -3
  245. package/skeleton/resources/spikes.md +3 -3
  246. package/skeleton/resources/workflow-reference.md +14 -14
  247. package/skeleton/roles/architect.md +25 -25
  248. package/skeleton/roles/builder.md +1 -1
  249. package/skeleton/roles/consultant.md +6 -0
  250. package/skeleton/templates/AGENTS.md +5 -5
  251. package/skeleton/templates/CLAUDE.md +5 -5
  252. package/skeleton/templates/lifecycle.md +9 -9
  253. package/templates/open.html +19 -16
  254. package/templates/tower.html +54 -94
  255. package/templates/vendor/marked.min.js +6 -0
  256. package/templates/vendor/prism-bash.min.js +1 -0
  257. package/templates/vendor/prism-css.min.js +1 -0
  258. package/templates/vendor/prism-javascript.min.js +1 -0
  259. package/templates/vendor/prism-json.min.js +1 -0
  260. package/templates/vendor/prism-markdown.min.js +1 -0
  261. package/templates/vendor/prism-markup.min.js +1 -0
  262. package/templates/vendor/prism-python.min.js +1 -0
  263. package/templates/vendor/prism-tomorrow.min.css +1 -0
  264. package/templates/vendor/prism-typescript.min.js +1 -0
  265. package/templates/vendor/prism-yaml.min.js +1 -0
  266. package/templates/vendor/prism.min.js +1 -0
  267. package/templates/vendor/purify.min.js +3 -0
  268. package/dashboard/dist/assets/index-4n9zpWLY.css +0 -32
  269. package/dashboard/dist/assets/index-b38SaXk5.js +0 -136
  270. package/dashboard/dist/assets/index-b38SaXk5.js.map +0 -1
  271. package/dist/agent-farm/hq-connector.d.ts +0 -19
  272. package/dist/agent-farm/hq-connector.d.ts.map +0 -1
  273. package/dist/agent-farm/hq-connector.js +0 -351
  274. package/dist/agent-farm/hq-connector.js.map +0 -1
  275. package/dist/agent-farm/utils/deps.d.ts +0 -51
  276. package/dist/agent-farm/utils/deps.d.ts.map +0 -1
  277. package/dist/agent-farm/utils/deps.js +0 -162
  278. package/dist/agent-farm/utils/deps.js.map +0 -1
  279. package/dist/agent-farm/utils/gate-status.d.ts +0 -16
  280. package/dist/agent-farm/utils/gate-status.d.ts.map +0 -1
  281. package/dist/agent-farm/utils/gate-status.js +0 -79
  282. package/dist/agent-farm/utils/gate-status.js.map +0 -1
  283. package/dist/agent-farm/utils/gate-watcher.d.ts +0 -38
  284. package/dist/agent-farm/utils/gate-watcher.d.ts.map +0 -1
  285. package/dist/agent-farm/utils/gate-watcher.js +0 -122
  286. package/dist/agent-farm/utils/gate-watcher.js.map +0 -1
  287. package/dist/agent-farm/utils/session.d.ts +0 -32
  288. package/dist/agent-farm/utils/session.d.ts.map +0 -1
  289. package/dist/agent-farm/utils/session.js +0 -57
  290. package/dist/agent-farm/utils/session.js.map +0 -1
  291. package/dist/lib/projectlist-parser.d.ts +0 -70
  292. package/dist/lib/projectlist-parser.d.ts.map +0 -1
  293. package/dist/lib/projectlist-parser.js +0 -200
  294. package/dist/lib/projectlist-parser.js.map +0 -1
  295. package/skeleton/templates/projectlist-archive.md +0 -21
  296. package/skeleton/templates/projectlist.md +0 -147
  297. package/templates/dashboard/css/dialogs.css +0 -149
  298. package/templates/dashboard/css/files.css +0 -558
  299. package/templates/dashboard/css/layout.css +0 -133
  300. package/templates/dashboard/css/projects.css +0 -501
  301. package/templates/dashboard/css/statusbar.css +0 -23
  302. package/templates/dashboard/css/tabs.css +0 -314
  303. package/templates/dashboard/css/utilities.css +0 -50
  304. package/templates/dashboard/css/variables.css +0 -45
  305. package/templates/dashboard/index.html +0 -149
  306. package/templates/dashboard/js/dialogs.js +0 -368
  307. package/templates/dashboard/js/files.js +0 -448
  308. package/templates/dashboard/js/main.js +0 -476
  309. package/templates/dashboard/js/projects.js +0 -544
  310. package/templates/dashboard/js/state.js +0 -91
  311. package/templates/dashboard/js/tabs.js +0 -518
  312. package/templates/dashboard/js/utils.js +0 -191
@@ -9,15 +9,15 @@ import { spawn, execSync } from 'node:child_process';
9
9
  import { tmpdir } from 'node:os';
10
10
  import chalk from 'chalk';
11
11
  import { query as claudeQuery } from '@anthropic-ai/claude-agent-sdk';
12
- import { readCodevFile, findProjectRoot, hasLocalOverride } from '../../lib/skeleton.js';
12
+ import { Codex } from '@openai/codex-sdk';
13
+ import { readCodevFile, findWorkspaceRoot, hasLocalOverride } from '../../lib/skeleton.js';
14
+ import { MetricsDB } from './metrics.js';
15
+ import { extractUsage, extractReviewText } from './usage-extractor.js';
13
16
  const MODEL_CONFIGS = {
14
17
  gemini: { cli: 'gemini', args: ['--yolo'], envVar: 'GEMINI_SYSTEM_MD' },
15
- // Codex uses experimental_instructions_file config flag (not env var)
16
- // See: https://github.com/openai/codex/discussions/3896
17
- codex: { cli: 'codex', args: ['exec', '-m', 'gpt-5.2-codex', '--full-auto'], envVar: null },
18
18
  };
19
- // Models that use the Agent SDK instead of CLI subprocess
20
- const SDK_MODELS = ['claude'];
19
+ // Models that use an Agent SDK instead of CLI subprocess
20
+ const SDK_MODELS = ['claude', 'codex'];
21
21
  // Claude Agent SDK turn limit. Claude explores the codebase with Read/Glob/Grep
22
22
  // tools before producing its verdict, so it needs a generous turn budget.
23
23
  const CLAUDE_MAX_TURNS = 200;
@@ -27,6 +27,36 @@ const MODEL_ALIASES = {
27
27
  gpt: 'codex',
28
28
  opus: 'claude',
29
29
  };
30
+ // Helper to record a metrics entry, opening and closing the DB
31
+ function recordMetrics(ctx, extra) {
32
+ try {
33
+ const db = new MetricsDB();
34
+ try {
35
+ db.record({
36
+ timestamp: ctx.timestamp,
37
+ model: ctx.model,
38
+ reviewType: ctx.reviewType,
39
+ subcommand: ctx.subcommand,
40
+ protocol: ctx.protocol,
41
+ projectId: ctx.projectId,
42
+ durationSeconds: extra.durationSeconds,
43
+ inputTokens: extra.inputTokens,
44
+ cachedInputTokens: extra.cachedInputTokens,
45
+ outputTokens: extra.outputTokens,
46
+ costUsd: extra.costUsd,
47
+ exitCode: extra.exitCode,
48
+ workspacePath: ctx.workspacePath,
49
+ errorMessage: extra.errorMessage,
50
+ });
51
+ }
52
+ finally {
53
+ db.close();
54
+ }
55
+ }
56
+ catch (err) {
57
+ console.error(`[warn] Failed to record metrics: ${err instanceof Error ? err.message : String(err)}`);
58
+ }
59
+ }
30
60
  // Valid review types
31
61
  const VALID_REVIEW_TYPES = [
32
62
  'spec-review',
@@ -46,8 +76,8 @@ function isValidRoleName(roleName) {
46
76
  * List available roles in codev/roles/
47
77
  * Excludes non-role files like README.md, review-types/, etc.
48
78
  */
49
- function listAvailableRoles(projectRoot) {
50
- const rolesDir = path.join(projectRoot, 'codev', 'roles');
79
+ function listAvailableRoles(workspaceRoot) {
80
+ const rolesDir = path.join(workspaceRoot, 'codev', 'roles');
51
81
  if (!fs.existsSync(rolesDir))
52
82
  return [];
53
83
  const excludePatterns = ['readme', 'review-types', 'overview', 'index'];
@@ -64,7 +94,7 @@ function listAvailableRoles(projectRoot) {
64
94
  * Load a custom role from codev/roles/<name>.md
65
95
  * Falls back to embedded skeleton if not found locally.
66
96
  */
67
- function loadCustomRole(projectRoot, roleName) {
97
+ function loadCustomRole(workspaceRoot, roleName) {
68
98
  // Validate role name to prevent directory traversal
69
99
  if (!isValidRoleName(roleName)) {
70
100
  throw new Error(`Invalid role name: '${roleName}'\n` +
@@ -72,9 +102,9 @@ function loadCustomRole(projectRoot, roleName) {
72
102
  }
73
103
  // Use readCodevFile which handles local-first with skeleton fallback
74
104
  const rolePath = `roles/${roleName}.md`;
75
- const roleContent = readCodevFile(rolePath, projectRoot);
105
+ const roleContent = readCodevFile(rolePath, workspaceRoot);
76
106
  if (!roleContent) {
77
- const available = listAvailableRoles(projectRoot);
107
+ const available = listAvailableRoles(workspaceRoot);
78
108
  const availableStr = available.length > 0
79
109
  ? `\n\nAvailable roles:\n${available.map(r => ` - ${r}`).join('\n')}`
80
110
  : '\n\nNo custom roles found in codev/roles/';
@@ -86,8 +116,8 @@ function loadCustomRole(projectRoot, roleName) {
86
116
  * Load the consultant role.
87
117
  * Checks local codev/roles/consultant.md first, then falls back to embedded skeleton.
88
118
  */
89
- function loadRole(projectRoot) {
90
- const role = readCodevFile('roles/consultant.md', projectRoot);
119
+ function loadRole(workspaceRoot) {
120
+ const role = readCodevFile('roles/consultant.md', workspaceRoot);
91
121
  if (!role) {
92
122
  throw new Error('consultant.md not found.\n' +
93
123
  'Checked: local codev/roles/consultant.md and embedded skeleton.\n' +
@@ -100,21 +130,21 @@ function loadRole(projectRoot) {
100
130
  * Checks consult-types/{type}.md first (new location),
101
131
  * then falls back to roles/review-types/{type}.md (deprecated) with a warning.
102
132
  */
103
- function loadReviewTypePrompt(projectRoot, reviewType) {
133
+ function loadReviewTypePrompt(workspaceRoot, reviewType) {
104
134
  const primaryPath = `consult-types/${reviewType}.md`;
105
135
  const fallbackPath = `roles/review-types/${reviewType}.md`;
106
136
  // 1. Check LOCAL consult-types/ first (preferred location)
107
- if (hasLocalOverride(primaryPath, projectRoot)) {
108
- return readCodevFile(primaryPath, projectRoot);
137
+ if (hasLocalOverride(primaryPath, workspaceRoot)) {
138
+ return readCodevFile(primaryPath, workspaceRoot);
109
139
  }
110
140
  // 2. Check LOCAL roles/review-types/ (deprecated location with warning)
111
- if (hasLocalOverride(fallbackPath, projectRoot)) {
141
+ if (hasLocalOverride(fallbackPath, workspaceRoot)) {
112
142
  console.error(chalk.yellow('Warning: Review types in roles/review-types/ are deprecated.'));
113
143
  console.error(chalk.yellow('Move your custom types to consult-types/ for future compatibility.'));
114
- return readCodevFile(fallbackPath, projectRoot);
144
+ return readCodevFile(fallbackPath, workspaceRoot);
115
145
  }
116
146
  // 3. Fall back to embedded skeleton consult-types/ (default)
117
- const skeletonPrompt = readCodevFile(primaryPath, projectRoot);
147
+ const skeletonPrompt = readCodevFile(primaryPath, workspaceRoot);
118
148
  if (skeletonPrompt) {
119
149
  return skeletonPrompt;
120
150
  }
@@ -123,8 +153,8 @@ function loadReviewTypePrompt(projectRoot, reviewType) {
123
153
  /**
124
154
  * Load .env file if it exists
125
155
  */
126
- function loadDotenv(projectRoot) {
127
- const envFile = path.join(projectRoot, '.env');
156
+ function loadDotenv(workspaceRoot) {
157
+ const envFile = path.join(workspaceRoot, '.env');
128
158
  if (!fs.existsSync(envFile))
129
159
  return;
130
160
  const content = fs.readFileSync(envFile, 'utf-8');
@@ -151,8 +181,8 @@ function loadDotenv(projectRoot) {
151
181
  /**
152
182
  * Find a spec file by number
153
183
  */
154
- function findSpec(projectRoot, number) {
155
- const specsDir = path.join(projectRoot, 'codev', 'specs');
184
+ function findSpec(workspaceRoot, number) {
185
+ const specsDir = path.join(workspaceRoot, 'codev', 'specs');
156
186
  const pattern = String(number).padStart(4, '0');
157
187
  if (fs.existsSync(specsDir)) {
158
188
  const files = fs.readdirSync(specsDir);
@@ -167,8 +197,8 @@ function findSpec(projectRoot, number) {
167
197
  /**
168
198
  * Find a plan file by number
169
199
  */
170
- function findPlan(projectRoot, number) {
171
- const plansDir = path.join(projectRoot, 'codev', 'plans');
200
+ function findPlan(workspaceRoot, number) {
201
+ const plansDir = path.join(workspaceRoot, 'codev', 'plans');
172
202
  const pattern = String(number).padStart(4, '0');
173
203
  if (fs.existsSync(plansDir)) {
174
204
  const files = fs.readdirSync(plansDir);
@@ -183,9 +213,9 @@ function findPlan(projectRoot, number) {
183
213
  /**
184
214
  * Log query to history file
185
215
  */
186
- function logQuery(projectRoot, model, query, duration) {
216
+ function logQuery(workspaceRoot, model, query, duration) {
187
217
  try {
188
- const logDir = path.join(projectRoot, '.consult');
218
+ const logDir = path.join(workspaceRoot, '.consult');
189
219
  if (!fs.existsSync(logDir)) {
190
220
  fs.mkdirSync(logDir, { recursive: true });
191
221
  }
@@ -211,13 +241,109 @@ function commandExists(cmd) {
211
241
  return false;
212
242
  }
213
243
  }
244
+ // Codex pricing for cost computation (matches values from old SUBPROCESS_MODEL_PRICING)
245
+ const CODEX_PRICING = { inputPer1M: 2.00, cachedInputPer1M: 1.00, outputPer1M: 8.00 };
246
+ /**
247
+ * Run Codex consultation via @openai/codex-sdk.
248
+ * Mirrors runClaudeConsultation() — streams events, captures usage, records metrics.
249
+ */
250
+ export async function runCodexConsultation(queryText, role, workspaceRoot, outputPath, metricsCtx) {
251
+ const chunks = [];
252
+ const startTime = Date.now();
253
+ let usageData = null;
254
+ let errorMessage = null;
255
+ let exitCode = 0;
256
+ // Write role to temp file — SDK requires file path for instructions
257
+ const tempFile = path.join(tmpdir(), `codev-role-${Date.now()}.md`);
258
+ fs.writeFileSync(tempFile, role);
259
+ try {
260
+ const codex = new Codex({
261
+ config: {
262
+ experimental_instructions_file: tempFile,
263
+ },
264
+ });
265
+ const thread = codex.startThread({
266
+ model: 'gpt-5.2-codex',
267
+ sandboxMode: 'read-only',
268
+ modelReasoningEffort: 'medium',
269
+ workingDirectory: workspaceRoot,
270
+ });
271
+ const { events } = await thread.runStreamed(queryText);
272
+ for await (const event of events) {
273
+ if (event.type === 'item.completed') {
274
+ const item = event.item;
275
+ if (item.type === 'agent_message') {
276
+ process.stdout.write(item.text);
277
+ chunks.push(item.text);
278
+ }
279
+ }
280
+ if (event.type === 'turn.completed') {
281
+ const input = event.usage.input_tokens;
282
+ const cached = event.usage.cached_input_tokens;
283
+ const output = event.usage.output_tokens;
284
+ const uncached = input - cached;
285
+ const cost = (uncached / 1_000_000) * CODEX_PRICING.inputPer1M
286
+ + (cached / 1_000_000) * CODEX_PRICING.cachedInputPer1M
287
+ + (output / 1_000_000) * CODEX_PRICING.outputPer1M;
288
+ usageData = { inputTokens: input, cachedInputTokens: cached, outputTokens: output, costUsd: cost };
289
+ }
290
+ if (event.type === 'turn.failed') {
291
+ errorMessage = event.error.message ?? 'Codex turn failed';
292
+ exitCode = 1;
293
+ throw new Error(errorMessage);
294
+ }
295
+ if (event.type === 'error') {
296
+ errorMessage = event.message ?? 'Codex stream error';
297
+ exitCode = 1;
298
+ throw new Error(errorMessage);
299
+ }
300
+ }
301
+ // Write output file
302
+ if (outputPath) {
303
+ const outputDir = path.dirname(outputPath);
304
+ if (!fs.existsSync(outputDir))
305
+ fs.mkdirSync(outputDir, { recursive: true });
306
+ fs.writeFileSync(outputPath, chunks.join(''));
307
+ console.error(`\nOutput written to: ${outputPath}`);
308
+ }
309
+ }
310
+ catch (err) {
311
+ if (!errorMessage) {
312
+ errorMessage = (err instanceof Error ? err.message : String(err)).substring(0, 500);
313
+ exitCode = 1;
314
+ }
315
+ throw err;
316
+ }
317
+ finally {
318
+ // Clean up temp file
319
+ if (fs.existsSync(tempFile))
320
+ fs.unlinkSync(tempFile);
321
+ // Record metrics (always, even on error)
322
+ if (metricsCtx) {
323
+ const duration = (Date.now() - startTime) / 1000;
324
+ recordMetrics(metricsCtx, {
325
+ durationSeconds: duration,
326
+ inputTokens: usageData?.inputTokens ?? null,
327
+ cachedInputTokens: usageData?.cachedInputTokens ?? null,
328
+ outputTokens: usageData?.outputTokens ?? null,
329
+ costUsd: usageData?.costUsd ?? null,
330
+ exitCode,
331
+ errorMessage,
332
+ });
333
+ }
334
+ }
335
+ }
214
336
  /**
215
337
  * Run Claude consultation via Agent SDK.
216
338
  * Uses the SDK's query() function instead of CLI subprocess.
217
339
  * This avoids the CLAUDECODE nesting guard and enables tool use during reviews.
218
340
  */
219
- async function runClaudeConsultation(queryText, role, projectRoot, outputPath) {
341
+ async function runClaudeConsultation(queryText, role, workspaceRoot, outputPath, metricsCtx) {
220
342
  const chunks = [];
343
+ const startTime = Date.now();
344
+ let sdkResult;
345
+ let errorMessage = null;
346
+ let exitCode = 0;
221
347
  // The SDK spawns a Claude Code subprocess that checks process.env.CLAUDECODE.
222
348
  // We must remove it from process.env (not just the options env) to avoid
223
349
  // the nesting guard. Restore it after the SDK call.
@@ -240,7 +366,7 @@ async function runClaudeConsultation(queryText, role, projectRoot, outputPath) {
240
366
  model: 'claude-opus-4-6',
241
367
  maxTurns: CLAUDE_MAX_TURNS,
242
368
  maxBudgetUsd: 25,
243
- cwd: projectRoot,
369
+ cwd: workspaceRoot,
244
370
  env,
245
371
  },
246
372
  });
@@ -254,9 +380,14 @@ async function runClaudeConsultation(queryText, role, projectRoot, outputPath) {
254
380
  }
255
381
  }
256
382
  if (message.type === 'result') {
257
- if (message.subtype !== 'success') {
383
+ if (message.subtype === 'success') {
384
+ sdkResult = message;
385
+ }
386
+ else {
258
387
  const errors = 'errors' in message ? message.errors : [];
259
- throw new Error(`Claude SDK error (${message.subtype}): ${errors.join(', ')}`);
388
+ errorMessage = `Claude SDK error (${message.subtype}): ${errors.join(', ')}`.substring(0, 500);
389
+ exitCode = 1;
390
+ throw new Error(errorMessage);
260
391
  }
261
392
  }
262
393
  }
@@ -268,21 +399,42 @@ async function runClaudeConsultation(queryText, role, projectRoot, outputPath) {
268
399
  console.error(`\nOutput written to: ${outputPath}`);
269
400
  }
270
401
  }
402
+ catch (err) {
403
+ if (!errorMessage) {
404
+ errorMessage = (err instanceof Error ? err.message : String(err)).substring(0, 500);
405
+ exitCode = 1;
406
+ }
407
+ throw err;
408
+ }
271
409
  finally {
272
410
  if (savedClaudeCode !== undefined) {
273
411
  process.env.CLAUDECODE = savedClaudeCode;
274
412
  }
413
+ // Record metrics (always, even on error)
414
+ if (metricsCtx) {
415
+ const duration = (Date.now() - startTime) / 1000;
416
+ const usage = sdkResult ? extractUsage('claude', '', sdkResult) : null;
417
+ recordMetrics(metricsCtx, {
418
+ durationSeconds: duration,
419
+ inputTokens: usage?.inputTokens ?? null,
420
+ cachedInputTokens: usage?.cachedInputTokens ?? null,
421
+ outputTokens: usage?.outputTokens ?? null,
422
+ costUsd: usage?.costUsd ?? null,
423
+ exitCode,
424
+ errorMessage,
425
+ });
426
+ }
275
427
  }
276
428
  }
277
429
  /**
278
430
  * Run the consultation
279
431
  */
280
- async function runConsultation(model, query, projectRoot, dryRun, reviewType, customRole, outputPath) {
432
+ async function runConsultation(model, query, workspaceRoot, dryRun, reviewType, customRole, outputPath, metricsCtx) {
281
433
  // Use custom role if specified, otherwise use default consultant role
282
- let role = customRole ? loadCustomRole(projectRoot, customRole) : loadRole(projectRoot);
434
+ let role = customRole ? loadCustomRole(workspaceRoot, customRole) : loadRole(workspaceRoot);
283
435
  // Append review type prompt if specified
284
436
  if (reviewType) {
285
- const typePrompt = loadReviewTypePrompt(projectRoot, reviewType);
437
+ const typePrompt = loadReviewTypePrompt(workspaceRoot, reviewType);
286
438
  if (typePrompt) {
287
439
  role = role + '\n\n---\n\n' + typePrompt;
288
440
  console.error(`Review type: ${reviewType}`);
@@ -291,7 +443,7 @@ async function runConsultation(model, query, projectRoot, dryRun, reviewType, cu
291
443
  console.error(chalk.yellow(`Warning: Review type prompt not found: ${reviewType}`));
292
444
  }
293
445
  }
294
- // Claude uses the Agent SDK — handle separately from CLI-based models
446
+ // SDK-based models — handle separately from CLI subprocess models
295
447
  if (model === 'claude') {
296
448
  if (dryRun) {
297
449
  console.log(chalk.yellow(`[claude] Would invoke Agent SDK:`));
@@ -304,9 +456,26 @@ async function runConsultation(model, query, projectRoot, dryRun, reviewType, cu
304
456
  return;
305
457
  }
306
458
  const startTime = Date.now();
307
- await runClaudeConsultation(query, role, projectRoot, outputPath);
459
+ await runClaudeConsultation(query, role, workspaceRoot, outputPath, metricsCtx);
308
460
  const duration = (Date.now() - startTime) / 1000;
309
- logQuery(projectRoot, model, query, duration);
461
+ logQuery(workspaceRoot, model, query, duration);
462
+ console.error(`\n[${model} completed in ${duration.toFixed(1)}s]`);
463
+ return;
464
+ }
465
+ if (model === 'codex') {
466
+ if (dryRun) {
467
+ console.log(chalk.yellow(`[codex] Would invoke Codex SDK:`));
468
+ console.log(` Model: gpt-5.2-codex`);
469
+ console.log(` Sandbox: read-only`);
470
+ console.log(` Reasoning effort: medium`);
471
+ const promptPreview = query.substring(0, 200) + (query.length > 200 ? '...' : '');
472
+ console.log(` Prompt: ${promptPreview}`);
473
+ return;
474
+ }
475
+ const startTime = Date.now();
476
+ await runCodexConsultation(query, role, workspaceRoot, outputPath, metricsCtx);
477
+ const duration = (Date.now() - startTime) / 1000;
478
+ logQuery(workspaceRoot, model, query, duration);
310
479
  console.error(`\n[${model} completed in ${duration.toFixed(1)}s]`);
311
480
  return;
312
481
  }
@@ -327,21 +496,7 @@ async function runConsultation(model, query, projectRoot, dryRun, reviewType, cu
327
496
  tempFile = path.join(tmpdir(), `codev-role-${Date.now()}.md`);
328
497
  fs.writeFileSync(tempFile, role);
329
498
  env['GEMINI_SYSTEM_MD'] = tempFile;
330
- cmd = [config.cli, ...config.args, query];
331
- }
332
- else if (model === 'codex') {
333
- // Codex uses experimental_instructions_file config flag (not env var)
334
- // This is the official approach per https://github.com/openai/codex/discussions/3896
335
- tempFile = path.join(tmpdir(), `codev-role-${Date.now()}.md`);
336
- fs.writeFileSync(tempFile, role);
337
- cmd = [
338
- config.cli,
339
- 'exec',
340
- '-c', `experimental_instructions_file=${tempFile}`,
341
- '-c', 'model_reasoning_effort=low', // Faster responses (10-20% improvement)
342
- '--full-auto',
343
- query,
344
- ];
499
+ cmd = [config.cli, ...config.args, '--output-format', 'json', query];
345
500
  }
346
501
  else {
347
502
  throw new Error(`Unknown model: ${model}`);
@@ -369,35 +524,56 @@ async function runConsultation(model, query, projectRoot, dryRun, reviewType, cu
369
524
  // When outputPath is set, capture stdout to write to file (used by porch)
370
525
  const fullEnv = { ...process.env, ...env };
371
526
  const startTime = Date.now();
372
- const stdoutMode = outputPath ? 'pipe' : 'inherit';
527
+ const stdoutMode = 'pipe'; // Always pipe to capture structured output for metrics
373
528
  return new Promise((resolve, reject) => {
374
529
  const proc = spawn(cmd[0], cmd.slice(1), {
530
+ cwd: workspaceRoot,
375
531
  env: fullEnv,
376
532
  stdio: ['ignore', stdoutMode, 'inherit'],
377
533
  });
378
534
  const chunks = [];
379
- if (outputPath && proc.stdout) {
535
+ if (proc.stdout) {
380
536
  proc.stdout.on('data', (chunk) => {
381
537
  chunks.push(chunk);
382
- // Also write to stdout so the user can still see output
383
- process.stdout.write(chunk);
538
+ // Gemini: buffer only (JSON is one blob, text emitted on close)
384
539
  });
385
540
  }
386
541
  proc.on('close', (code) => {
387
542
  const duration = (Date.now() - startTime) / 1000;
388
- logQuery(projectRoot, model, query, duration);
543
+ logQuery(workspaceRoot, model, query, duration);
389
544
  if (tempFile && fs.existsSync(tempFile)) {
390
545
  fs.unlinkSync(tempFile);
391
546
  }
392
- // Write captured output to file
393
- if (outputPath && chunks.length > 0) {
547
+ const rawOutput = Buffer.concat(chunks).toString('utf-8');
548
+ // Extract review text from structured output (JSON/JSONL plain text)
549
+ const reviewText = extractReviewText(model, rawOutput);
550
+ const outputContent = reviewText ?? rawOutput; // Fallback to raw on parse failure
551
+ // Write extracted text to stdout for Gemini (was fully buffered)
552
+ if (model === 'gemini') {
553
+ process.stdout.write(outputContent);
554
+ }
555
+ // Write to output file
556
+ if (outputPath && outputContent.length > 0) {
394
557
  const outputDir = path.dirname(outputPath);
395
558
  if (!fs.existsSync(outputDir)) {
396
559
  fs.mkdirSync(outputDir, { recursive: true });
397
560
  }
398
- fs.writeFileSync(outputPath, Buffer.concat(chunks).toString('utf-8'));
561
+ fs.writeFileSync(outputPath, outputContent);
399
562
  console.error(`\nOutput written to: ${outputPath}`);
400
563
  }
564
+ // Record metrics
565
+ if (metricsCtx) {
566
+ const usage = extractUsage(model, rawOutput);
567
+ recordMetrics(metricsCtx, {
568
+ durationSeconds: duration,
569
+ inputTokens: usage?.inputTokens ?? null,
570
+ cachedInputTokens: usage?.cachedInputTokens ?? null,
571
+ outputTokens: usage?.outputTokens ?? null,
572
+ costUsd: usage?.costUsd ?? null,
573
+ exitCode: code ?? 1,
574
+ errorMessage: code !== 0 ? `Process exited with code ${code}` : null,
575
+ });
576
+ }
401
577
  console.error(`\n[${model} completed in ${duration.toFixed(1)}s]`);
402
578
  if (code !== 0) {
403
579
  reject(new Error(`Process exited with code ${code}`));
@@ -410,6 +586,19 @@ async function runConsultation(model, query, projectRoot, dryRun, reviewType, cu
410
586
  if (tempFile && fs.existsSync(tempFile)) {
411
587
  fs.unlinkSync(tempFile);
412
588
  }
589
+ // Record metrics for spawn failures
590
+ if (metricsCtx) {
591
+ const duration = (Date.now() - startTime) / 1000;
592
+ recordMetrics(metricsCtx, {
593
+ durationSeconds: duration,
594
+ inputTokens: null,
595
+ cachedInputTokens: null,
596
+ outputTokens: null,
597
+ costUsd: null,
598
+ exitCode: 1,
599
+ errorMessage: (error.message || String(error)).substring(0, 500),
600
+ });
601
+ }
413
602
  reject(error);
414
603
  });
415
604
  });
@@ -418,9 +607,9 @@ async function runConsultation(model, query, projectRoot, dryRun, reviewType, cu
418
607
  * Get a compact diff stat summary and list of changed files.
419
608
  * Returns { stat, files } where stat is the `--stat` output and files is the list of paths.
420
609
  */
421
- function getDiffStat(projectRoot, ref) {
422
- const stat = execSync(`git diff --stat ${ref}`, { cwd: projectRoot, encoding: 'utf-8' }).toString();
423
- const nameOnly = execSync(`git diff --name-only ${ref}`, { cwd: projectRoot, encoding: 'utf-8' }).toString();
610
+ function getDiffStat(workspaceRoot, ref) {
611
+ const stat = execSync(`git diff --stat ${ref}`, { cwd: workspaceRoot, encoding: 'utf-8' }).toString();
612
+ const nameOnly = execSync(`git diff --name-only ${ref}`, { cwd: workspaceRoot, encoding: 'utf-8' }).toString();
424
613
  const files = nameOnly.trim().split('\n').filter(Boolean);
425
614
  return { stat, files };
426
615
  }
@@ -450,7 +639,7 @@ function fetchPRData(prNumber) {
450
639
  * Build query for PR review.
451
640
  * Provides file list and instructs reviewers to read files from disk.
452
641
  */
453
- function buildPRQuery(prNumber, _projectRoot) {
642
+ function buildPRQuery(prNumber, _workspaceRoot) {
454
643
  const prData = fetchPRData(prNumber);
455
644
  const fileList = prData.changedFiles.map(f => `- ${f}`).join('\n');
456
645
  return `Review Pull Request #${prNumber}
@@ -466,7 +655,7 @@ ${fileList}
466
655
  ## How to Review
467
656
  **Read the changed files from disk** to review their current content. You have full filesystem access.
468
657
  For each changed file listed above, read it and evaluate the code quality, correctness, and test coverage.
469
- Use \`git diff HEAD~1 -- <file>\` or \`git log -p -- <file>\` if you need to see what specifically changed.
658
+ Do NOT rely on git diffs to determine the current state of code diffs miss uncommitted changes in worktrees.
470
659
 
471
660
  ## Comments
472
661
  ${prData.comments}
@@ -504,6 +693,11 @@ Please read and review this specification:
504
693
  query += `- Plan file: ${planPath}\n`;
505
694
  }
506
695
  query += `
696
+ ## How to Review
697
+ **Read the files listed above directly from disk.** You have full filesystem access.
698
+ Do NOT rely on \`git diff\` or \`git log\` to review content — diffs may be truncated or miss uncommitted work.
699
+ Open the spec file, read it in full, and evaluate it directly.
700
+
507
701
  Please review:
508
702
  1. Clarity and completeness of requirements
509
703
  2. Technical feasibility
@@ -527,15 +721,17 @@ KEY_ISSUES: [List of critical issues if any, or "None"]`;
527
721
  * Build query for implementation review.
528
722
  * Provides diff stat + file list and instructs reviewers to read files from disk.
529
723
  */
530
- function buildImplQuery(projectNumber, projectRoot, planPhase) {
531
- const specPath = findSpec(projectRoot, projectNumber);
532
- const planPath = findPlan(projectRoot, projectNumber);
724
+ function buildImplQuery(projectNumber, workspaceRoot, planPhase) {
725
+ const specPath = findSpec(workspaceRoot, projectNumber);
726
+ const planPath = findPlan(workspaceRoot, projectNumber);
533
727
  // Get compact diff summary against base branch
534
728
  let diffStat = '';
535
729
  let changedFiles = [];
536
730
  try {
537
- const mergeBase = execSync('git merge-base HEAD main', { cwd: projectRoot, encoding: 'utf-8' }).trim();
538
- const result = getDiffStat(projectRoot, `${mergeBase}..HEAD`);
731
+ const mergeBase = execSync('git merge-base HEAD main', { cwd: workspaceRoot, encoding: 'utf-8' }).trim();
732
+ // Use mergeBase (not mergeBase..HEAD) to include uncommitted working tree changes.
733
+ // The ..HEAD syntax is commit-to-commit and misses uncommitted work in builder worktrees.
734
+ const result = getDiffStat(workspaceRoot, mergeBase);
539
735
  diffStat = result.stat;
540
736
  changedFiles = result.files;
541
737
  }
@@ -569,7 +765,7 @@ function buildImplQuery(projectNumber, projectRoot, planPhase) {
569
765
  query += `\n\n## How to Review\n`;
570
766
  query += `**Read the changed files from disk** to review their actual content. You have full filesystem access.\n`;
571
767
  query += `For each file listed above, read it and evaluate the implementation against the spec/plan.\n`;
572
- query += `Use \`git diff main -- <file>\` if you need to see what specifically changed in a file.\n`;
768
+ query += `Do NOT rely on git diffs to determine the current state of code diffs miss uncommitted changes in worktrees.\n`;
573
769
  }
574
770
  else {
575
771
  query += `\n## Instructions\n\nRead the spec and plan files above, then explore the filesystem to find and review the implementation changes.\n`;
@@ -606,6 +802,11 @@ Please read and review this implementation plan:
606
802
  query += `- Spec file: ${specPath} (for context)\n`;
607
803
  }
608
804
  query += `
805
+ ## How to Review
806
+ **Read the files listed above directly from disk.** You have full filesystem access.
807
+ Do NOT rely on \`git diff\` or \`git log\` to review content — diffs may be truncated or miss uncommitted work.
808
+ Open the plan file (and spec if provided), read them in full, and evaluate the plan directly.
809
+
609
810
  Please review:
610
811
  1. Alignment with specification requirements
611
812
  2. Implementation approach and architecture
@@ -641,8 +842,20 @@ export async function consult(options) {
641
842
  if (reviewType && !VALID_REVIEW_TYPES.includes(reviewType)) {
642
843
  throw new Error(`Invalid review type: ${reviewType}\nValid types: ${VALID_REVIEW_TYPES.join(', ')}`);
643
844
  }
644
- const projectRoot = findProjectRoot();
645
- loadDotenv(projectRoot);
845
+ const workspaceRoot = findWorkspaceRoot();
846
+ loadDotenv(workspaceRoot);
847
+ // Capture timestamp at invocation start (before subprocess/SDK)
848
+ const timestamp = new Date().toISOString();
849
+ // Build metrics context with protocol/project defaults
850
+ const metricsCtx = {
851
+ timestamp,
852
+ model,
853
+ reviewType: reviewType ?? null,
854
+ subcommand,
855
+ protocol: options.protocol ?? 'manual',
856
+ projectId: options.projectId ?? null,
857
+ workspacePath: workspaceRoot,
858
+ };
646
859
  console.error(`[${subcommand} review]`);
647
860
  console.error(`Model: ${model}`);
648
861
  // Log custom role if specified
@@ -659,7 +872,7 @@ export async function consult(options) {
659
872
  if (isNaN(prNumber)) {
660
873
  throw new Error(`Invalid PR number: ${args[0]}`);
661
874
  }
662
- query = buildPRQuery(prNumber, projectRoot);
875
+ query = buildPRQuery(prNumber, workspaceRoot);
663
876
  break;
664
877
  }
665
878
  case 'spec': {
@@ -670,11 +883,11 @@ export async function consult(options) {
670
883
  if (isNaN(specNumber)) {
671
884
  throw new Error(`Invalid spec number: ${args[0]}`);
672
885
  }
673
- const specPath = findSpec(projectRoot, specNumber);
886
+ const specPath = findSpec(workspaceRoot, specNumber);
674
887
  if (!specPath) {
675
888
  throw new Error(`Spec ${specNumber} not found`);
676
889
  }
677
- const planPath = findPlan(projectRoot, specNumber);
890
+ const planPath = findPlan(workspaceRoot, specNumber);
678
891
  query = buildSpecQuery(specPath, planPath);
679
892
  console.error(`Spec: ${specPath}`);
680
893
  if (planPath)
@@ -689,11 +902,11 @@ export async function consult(options) {
689
902
  if (isNaN(planNumber)) {
690
903
  throw new Error(`Invalid plan number: ${args[0]}`);
691
904
  }
692
- const planPath = findPlan(projectRoot, planNumber);
905
+ const planPath = findPlan(workspaceRoot, planNumber);
693
906
  if (!planPath) {
694
907
  throw new Error(`Plan ${planNumber} not found`);
695
908
  }
696
- const specPath = findSpec(projectRoot, planNumber);
909
+ const specPath = findSpec(workspaceRoot, planNumber);
697
910
  query = buildPlanQuery(planPath, specPath);
698
911
  console.error(`Plan: ${planPath}`);
699
912
  if (specPath)
@@ -715,9 +928,9 @@ export async function consult(options) {
715
928
  if (isNaN(implNumber)) {
716
929
  throw new Error(`Invalid project number: ${args[0]}`);
717
930
  }
718
- const specPath = findSpec(projectRoot, implNumber);
719
- const planPath = findPlan(projectRoot, implNumber);
720
- query = buildImplQuery(implNumber, projectRoot, options.planPhase);
931
+ const specPath = findSpec(workspaceRoot, implNumber);
932
+ const planPath = findPlan(workspaceRoot, implNumber);
933
+ query = buildImplQuery(implNumber, workspaceRoot, options.planPhase);
721
934
  console.error(`Project: ${implNumber}`);
722
935
  if (specPath)
723
936
  console.error(`Spec: ${specPath}`);
@@ -752,8 +965,8 @@ export async function consult(options) {
752
965
  console.error(`[${model.toUpperCase()}] Starting consultation...`);
753
966
  console.error('='.repeat(60));
754
967
  console.error('');
755
- await runConsultation(model, query, projectRoot, dryRun, reviewType, customRole, outputPath);
968
+ await runConsultation(model, query, workspaceRoot, dryRun, reviewType, customRole, outputPath, metricsCtx);
756
969
  }
757
970
  // Exported for testing
758
- export { getDiffStat as _getDiffStat };
971
+ export { getDiffStat as _getDiffStat, buildSpecQuery as _buildSpecQuery, buildPlanQuery as _buildPlanQuery };
759
972
  //# sourceMappingURL=index.js.map