@cluesmith/codev 2.0.6 → 2.0.7

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 (181) hide show
  1. package/dashboard/dist/assets/{index-B-s8BA2l.js → index-BblS3DWL.js} +30 -30
  2. package/dashboard/dist/assets/index-BblS3DWL.js.map +1 -0
  3. package/dashboard/dist/assets/index-Cr9PyjqX.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 +23 -48
  7. package/dist/agent-farm/cli.js.map +1 -1
  8. package/dist/agent-farm/commands/architect.d.ts +5 -5
  9. package/dist/agent-farm/commands/architect.d.ts.map +1 -1
  10. package/dist/agent-farm/commands/architect.js +37 -20
  11. package/dist/agent-farm/commands/architect.js.map +1 -1
  12. package/dist/agent-farm/commands/attach.d.ts.map +1 -1
  13. package/dist/agent-farm/commands/attach.js +1 -21
  14. package/dist/agent-farm/commands/attach.js.map +1 -1
  15. package/dist/agent-farm/commands/cleanup.d.ts +12 -0
  16. package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
  17. package/dist/agent-farm/commands/cleanup.js +108 -7
  18. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  19. package/dist/agent-farm/commands/spawn-worktree.d.ts +1 -2
  20. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  21. package/dist/agent-farm/commands/spawn-worktree.js +12 -18
  22. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  23. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  24. package/dist/agent-farm/commands/spawn.js +30 -26
  25. package/dist/agent-farm/commands/spawn.js.map +1 -1
  26. package/dist/agent-farm/commands/start.d.ts.map +1 -1
  27. package/dist/agent-farm/commands/start.js +2 -6
  28. package/dist/agent-farm/commands/start.js.map +1 -1
  29. package/dist/agent-farm/commands/status.d.ts.map +1 -1
  30. package/dist/agent-farm/commands/status.js +4 -23
  31. package/dist/agent-farm/commands/status.js.map +1 -1
  32. package/dist/agent-farm/commands/stop.d.ts.map +1 -1
  33. package/dist/agent-farm/commands/stop.js +2 -6
  34. package/dist/agent-farm/commands/stop.js.map +1 -1
  35. package/dist/agent-farm/commands/tower-cloud.d.ts +2 -9
  36. package/dist/agent-farm/commands/tower-cloud.d.ts.map +1 -1
  37. package/dist/agent-farm/commands/tower-cloud.js +12 -47
  38. package/dist/agent-farm/commands/tower-cloud.js.map +1 -1
  39. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  40. package/dist/agent-farm/commands/tower.js +6 -23
  41. package/dist/agent-farm/commands/tower.js.map +1 -1
  42. package/dist/agent-farm/db/index.d.ts.map +1 -1
  43. package/dist/agent-farm/db/index.js +52 -2
  44. package/dist/agent-farm/db/index.js.map +1 -1
  45. package/dist/agent-farm/db/schema.d.ts +1 -1
  46. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  47. package/dist/agent-farm/db/schema.js +1 -1
  48. package/dist/agent-farm/lib/cloud-config.d.ts +1 -0
  49. package/dist/agent-farm/lib/cloud-config.d.ts.map +1 -1
  50. package/dist/agent-farm/lib/cloud-config.js +2 -2
  51. package/dist/agent-farm/lib/cloud-config.js.map +1 -1
  52. package/dist/agent-farm/lib/tower-client.d.ts +49 -0
  53. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
  54. package/dist/agent-farm/lib/tower-client.js +32 -2
  55. package/dist/agent-farm/lib/tower-client.js.map +1 -1
  56. package/dist/agent-farm/servers/overview.d.ts +47 -1
  57. package/dist/agent-farm/servers/overview.d.ts.map +1 -1
  58. package/dist/agent-farm/servers/overview.js +298 -58
  59. package/dist/agent-farm/servers/overview.js.map +1 -1
  60. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  61. package/dist/agent-farm/servers/tower-instances.js +2 -1
  62. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  63. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  64. package/dist/agent-farm/servers/tower-routes.js +21 -20
  65. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  66. package/dist/agent-farm/servers/tower-server.js +3 -4
  67. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  68. package/dist/agent-farm/servers/tower-terminals.d.ts +1 -1
  69. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  70. package/dist/agent-farm/servers/tower-terminals.js +4 -4
  71. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  72. package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
  73. package/dist/agent-farm/servers/tower-tunnel.js +3 -19
  74. package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
  75. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
  76. package/dist/agent-farm/servers/tower-websocket.js +2 -1
  77. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  78. package/dist/agent-farm/types.d.ts +1 -1
  79. package/dist/agent-farm/types.d.ts.map +1 -1
  80. package/dist/agent-farm/utils/display.d.ts +8 -0
  81. package/dist/agent-farm/utils/display.d.ts.map +1 -0
  82. package/dist/agent-farm/utils/display.js +26 -0
  83. package/dist/agent-farm/utils/display.js.map +1 -0
  84. package/dist/agent-farm/utils/notifications.d.ts.map +1 -1
  85. package/dist/agent-farm/utils/notifications.js +7 -16
  86. package/dist/agent-farm/utils/notifications.js.map +1 -1
  87. package/dist/agent-farm/utils/server-utils.d.ts +4 -0
  88. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  89. package/dist/agent-farm/utils/server-utils.js +20 -0
  90. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  91. package/dist/agent-farm/utils/shell.d.ts +5 -0
  92. package/dist/agent-farm/utils/shell.d.ts.map +1 -1
  93. package/dist/agent-farm/utils/shell.js +15 -11
  94. package/dist/agent-farm/utils/shell.js.map +1 -1
  95. package/dist/cli.d.ts.map +1 -1
  96. package/dist/cli.js +25 -19
  97. package/dist/cli.js.map +1 -1
  98. package/dist/commands/consult/index.d.ts +10 -9
  99. package/dist/commands/consult/index.d.ts.map +1 -1
  100. package/dist/commands/consult/index.js +401 -259
  101. package/dist/commands/consult/index.js.map +1 -1
  102. package/dist/commands/consult/usage-extractor.d.ts +3 -0
  103. package/dist/commands/consult/usage-extractor.d.ts.map +1 -1
  104. package/dist/commands/consult/usage-extractor.js +52 -29
  105. package/dist/commands/consult/usage-extractor.js.map +1 -1
  106. package/dist/commands/porch/index.d.ts.map +1 -1
  107. package/dist/commands/porch/index.js +13 -12
  108. package/dist/commands/porch/index.js.map +1 -1
  109. package/dist/commands/porch/next.d.ts.map +1 -1
  110. package/dist/commands/porch/next.js +7 -18
  111. package/dist/commands/porch/next.js.map +1 -1
  112. package/dist/commands/porch/plan.d.ts.map +1 -1
  113. package/dist/commands/porch/plan.js +17 -2
  114. package/dist/commands/porch/plan.js.map +1 -1
  115. package/dist/commands/porch/prompts.d.ts.map +1 -1
  116. package/dist/commands/porch/prompts.js +6 -3
  117. package/dist/commands/porch/prompts.js.map +1 -1
  118. package/dist/commands/porch/state.d.ts +13 -0
  119. package/dist/commands/porch/state.d.ts.map +1 -1
  120. package/dist/commands/porch/state.js +46 -1
  121. package/dist/commands/porch/state.js.map +1 -1
  122. package/dist/lib/github.d.ts +10 -9
  123. package/dist/lib/github.d.ts.map +1 -1
  124. package/dist/lib/github.js +49 -9
  125. package/dist/lib/github.js.map +1 -1
  126. package/dist/terminal/index.d.ts +2 -0
  127. package/dist/terminal/index.d.ts.map +1 -1
  128. package/dist/terminal/index.js +2 -0
  129. package/dist/terminal/index.js.map +1 -1
  130. package/dist/terminal/pty-manager.js +2 -2
  131. package/dist/terminal/pty-manager.js.map +1 -1
  132. package/dist/terminal/pty-session.js +1 -1
  133. package/dist/terminal/pty-session.js.map +1 -1
  134. package/package.json +1 -1
  135. package/skeleton/.claude/skills/af/SKILL.md +9 -9
  136. package/skeleton/.claude/skills/consult/SKILL.md +55 -38
  137. package/skeleton/.claude/skills/porch/SKILL.md +53 -0
  138. package/skeleton/DEPENDENCIES.md +2 -2
  139. package/skeleton/builders.md +8 -19
  140. package/skeleton/protocol-schema.json +1 -1
  141. package/skeleton/protocols/bugfix/prompts/pr.md +3 -3
  142. package/skeleton/protocols/bugfix/protocol.json +1 -1
  143. package/skeleton/protocols/maintain/consult-types/impl-review.md +72 -0
  144. package/skeleton/protocols/maintain/consult-types/pr-review.md +72 -0
  145. package/skeleton/protocols/maintain/protocol.json +4 -4
  146. package/skeleton/protocols/maintain/protocol.md +3 -3
  147. package/skeleton/protocols/protocol-schema.json +1 -1
  148. package/skeleton/protocols/spir/consult-types/impl-review.md +72 -0
  149. package/skeleton/protocols/spir/consult-types/phase-review.md +72 -0
  150. package/skeleton/protocols/spir/consult-types/pr-review.md +72 -0
  151. package/skeleton/protocols/spir/prompts/plan.md +4 -4
  152. package/skeleton/protocols/spir/prompts/review.md +8 -8
  153. package/skeleton/protocols/spir/prompts/specify.md +6 -6
  154. package/skeleton/protocols/spir/protocol.json +11 -11
  155. package/skeleton/protocols/spir/templates/review.md +2 -2
  156. package/skeleton/protocols/tick/consult-types/impl-review.md +72 -0
  157. package/skeleton/protocols/tick/consult-types/plan-review.md +59 -0
  158. package/skeleton/protocols/tick/consult-types/pr-review.md +72 -0
  159. package/skeleton/protocols/tick/consult-types/spec-review.md +55 -0
  160. package/skeleton/protocols/tick/protocol.json +2 -7
  161. package/skeleton/resources/commands/agent-farm.md +13 -11
  162. package/skeleton/resources/commands/codev.md +0 -35
  163. package/skeleton/resources/commands/consult.md +88 -234
  164. package/skeleton/resources/commands/overview.md +6 -7
  165. package/skeleton/resources/workflow-reference.md +24 -24
  166. package/skeleton/roles/architect.md +17 -21
  167. package/skeleton/roles/builder.md +13 -13
  168. package/skeleton/templates/AGENTS.md +1 -1
  169. package/skeleton/templates/CLAUDE.md +1 -1
  170. package/skeleton/templates/cheatsheet.md +22 -18
  171. package/skeleton/templates/pr-overview.md +5 -5
  172. package/dashboard/dist/assets/index-B-s8BA2l.js.map +0 -1
  173. package/dashboard/dist/assets/index-DB2AxRP7.css +0 -32
  174. package/dist/agent-farm/commands/consult.d.ts +0 -15
  175. package/dist/agent-farm/commands/consult.d.ts.map +0 -1
  176. package/dist/agent-farm/commands/consult.js +0 -39
  177. package/dist/agent-farm/commands/consult.js.map +0 -1
  178. /package/skeleton/{consult-types → protocols/bugfix/consult-types}/impl-review.md +0 -0
  179. /package/skeleton/{consult-types/pr-ready.md → protocols/bugfix/consult-types/pr-review.md} +0 -0
  180. /package/skeleton/{consult-types → protocols/spir/consult-types}/plan-review.md +0 -0
  181. /package/skeleton/{consult-types → protocols/spir/consult-types}/spec-review.md +0 -0
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * consult - AI consultation with external models
3
3
  *
4
- * Provides unified interface to gemini-cli, codex, and Claude Agent SDK.
4
+ * Three modes:
5
+ * 1. General — ad-hoc prompts via --prompt or --prompt-file
6
+ * 2. Protocol — structured reviews via --protocol + --type
7
+ * 3. Stats — consultation metrics (delegated to stats.ts, handled in cli.ts)
5
8
  */
6
9
  import * as fs from 'node:fs';
7
10
  import * as path from 'node:path';
@@ -10,11 +13,11 @@ import { tmpdir } from 'node:os';
10
13
  import chalk from 'chalk';
11
14
  import { query as claudeQuery } from '@anthropic-ai/claude-agent-sdk';
12
15
  import { Codex } from '@openai/codex-sdk';
13
- import { readCodevFile, findWorkspaceRoot, hasLocalOverride } from '../../lib/skeleton.js';
16
+ import { readCodevFile, findWorkspaceRoot } from '../../lib/skeleton.js';
14
17
  import { MetricsDB } from './metrics.js';
15
18
  import { extractUsage, extractReviewText } from './usage-extractor.js';
16
19
  const MODEL_CONFIGS = {
17
- gemini: { cli: 'gemini', args: ['--yolo'], envVar: 'GEMINI_SYSTEM_MD' },
20
+ gemini: { cli: 'gemini', args: [], envVar: 'GEMINI_SYSTEM_MD' },
18
21
  };
19
22
  // Models that use an Agent SDK instead of CLI subprocess
20
23
  const SDK_MODELS = ['claude', 'codex'];
@@ -57,60 +60,12 @@ function recordMetrics(ctx, extra) {
57
60
  console.error(`[warn] Failed to record metrics: ${err instanceof Error ? err.message : String(err)}`);
58
61
  }
59
62
  }
60
- // Valid review types
61
- const VALID_REVIEW_TYPES = [
62
- 'spec-review',
63
- 'plan-review',
64
- 'impl-review',
65
- 'pr-ready',
66
- 'integration-review',
67
- ];
68
63
  /**
69
- * Validate role name to prevent directory traversal attacks.
64
+ * Validate name to prevent directory traversal attacks.
70
65
  * Only allows alphanumeric, hyphen, and underscore characters.
71
66
  */
72
- function isValidRoleName(roleName) {
73
- return /^[a-zA-Z0-9_-]+$/.test(roleName);
74
- }
75
- /**
76
- * List available roles in codev/roles/
77
- * Excludes non-role files like README.md, review-types/, etc.
78
- */
79
- function listAvailableRoles(workspaceRoot) {
80
- const rolesDir = path.join(workspaceRoot, 'codev', 'roles');
81
- if (!fs.existsSync(rolesDir))
82
- return [];
83
- const excludePatterns = ['readme', 'review-types', 'overview', 'index'];
84
- return fs.readdirSync(rolesDir)
85
- .filter(f => {
86
- if (!f.endsWith('.md'))
87
- return false;
88
- const basename = f.replace('.md', '').toLowerCase();
89
- return !excludePatterns.some(pattern => basename.includes(pattern));
90
- })
91
- .map(f => f.replace('.md', ''));
92
- }
93
- /**
94
- * Load a custom role from codev/roles/<name>.md
95
- * Falls back to embedded skeleton if not found locally.
96
- */
97
- function loadCustomRole(workspaceRoot, roleName) {
98
- // Validate role name to prevent directory traversal
99
- if (!isValidRoleName(roleName)) {
100
- throw new Error(`Invalid role name: '${roleName}'\n` +
101
- 'Role names can only contain letters, numbers, hyphens, and underscores.');
102
- }
103
- // Use readCodevFile which handles local-first with skeleton fallback
104
- const rolePath = `roles/${roleName}.md`;
105
- const roleContent = readCodevFile(rolePath, workspaceRoot);
106
- if (!roleContent) {
107
- const available = listAvailableRoles(workspaceRoot);
108
- const availableStr = available.length > 0
109
- ? `\n\nAvailable roles:\n${available.map(r => ` - ${r}`).join('\n')}`
110
- : '\n\nNo custom roles found in codev/roles/';
111
- throw new Error(`Role '${roleName}' not found.${availableStr}`);
112
- }
113
- return roleContent;
67
+ function isValidRoleName(name) {
68
+ return /^[a-zA-Z0-9_-]+$/.test(name);
114
69
  }
115
70
  /**
116
71
  * Load the consultant role.
@@ -126,29 +81,24 @@ function loadRole(workspaceRoot) {
126
81
  return role;
127
82
  }
128
83
  /**
129
- * Load a review type prompt.
130
- * Checks consult-types/{type}.md first (new location),
131
- * then falls back to roles/review-types/{type}.md (deprecated) with a warning.
84
+ * Resolve protocol prompt template.
85
+ * 1. If --protocol given → codev/protocols/<protocol>/consult-types/<type>-review.md
86
+ * 2. If --type alone → codev/consult-types/<type>-review.md
87
+ * 3. Error if file not found
132
88
  */
133
- function loadReviewTypePrompt(workspaceRoot, reviewType) {
134
- const primaryPath = `consult-types/${reviewType}.md`;
135
- const fallbackPath = `roles/review-types/${reviewType}.md`;
136
- // 1. Check LOCAL consult-types/ first (preferred location)
137
- if (hasLocalOverride(primaryPath, workspaceRoot)) {
138
- return readCodevFile(primaryPath, workspaceRoot);
139
- }
140
- // 2. Check LOCAL roles/review-types/ (deprecated location with warning)
141
- if (hasLocalOverride(fallbackPath, workspaceRoot)) {
142
- console.error(chalk.yellow('Warning: Review types in roles/review-types/ are deprecated.'));
143
- console.error(chalk.yellow('Move your custom types to consult-types/ for future compatibility.'));
144
- return readCodevFile(fallbackPath, workspaceRoot);
145
- }
146
- // 3. Fall back to embedded skeleton consult-types/ (default)
147
- const skeletonPrompt = readCodevFile(primaryPath, workspaceRoot);
148
- if (skeletonPrompt) {
149
- return skeletonPrompt;
89
+ function resolveProtocolPrompt(workspaceRoot, protocol, type) {
90
+ const templateName = `${type}-review.md`;
91
+ const relativePath = protocol
92
+ ? `protocols/${protocol}/consult-types/${templateName}`
93
+ : `consult-types/${templateName}`;
94
+ const content = readCodevFile(relativePath, workspaceRoot);
95
+ if (!content) {
96
+ const location = protocol
97
+ ? `codev/protocols/${protocol}/consult-types/${templateName}`
98
+ : `codev/consult-types/${templateName}`;
99
+ throw new Error(`Prompt template not found: ${location}`);
150
100
  }
151
- return null;
101
+ return content;
152
102
  }
153
103
  /**
154
104
  * Load .env file if it exists
@@ -179,37 +129,83 @@ function loadDotenv(workspaceRoot) {
179
129
  }
180
130
  }
181
131
  /**
182
- * Find a spec file by number
132
+ * Find a spec file by number. Returns null if not found.
133
+ * Errors if multiple matches found.
183
134
  */
184
135
  function findSpec(workspaceRoot, number) {
185
136
  const specsDir = path.join(workspaceRoot, 'codev', 'specs');
186
137
  const pattern = String(number).padStart(4, '0');
187
138
  if (fs.existsSync(specsDir)) {
188
- const files = fs.readdirSync(specsDir);
189
- for (const file of files) {
190
- if (file.startsWith(pattern) && file.endsWith('.md')) {
191
- return path.join(specsDir, file);
192
- }
139
+ const matches = fs.readdirSync(specsDir).filter(f => f.startsWith(pattern) && f.endsWith('.md'));
140
+ if (matches.length > 1) {
141
+ const list = matches.map(f => ` - codev/specs/${f}`).join('\n');
142
+ throw new Error(`Multiple spec files match '${pattern}*':\n${list}`);
143
+ }
144
+ if (matches.length === 1) {
145
+ return path.join(specsDir, matches[0]);
193
146
  }
194
147
  }
195
148
  return null;
196
149
  }
197
150
  /**
198
- * Find a plan file by number
151
+ * Find a plan file by number. Returns null if not found.
152
+ * Errors if multiple matches found.
199
153
  */
200
154
  function findPlan(workspaceRoot, number) {
201
155
  const plansDir = path.join(workspaceRoot, 'codev', 'plans');
202
156
  const pattern = String(number).padStart(4, '0');
203
157
  if (fs.existsSync(plansDir)) {
204
- const files = fs.readdirSync(plansDir);
205
- for (const file of files) {
206
- if (file.startsWith(pattern) && file.endsWith('.md')) {
207
- return path.join(plansDir, file);
208
- }
158
+ const matches = fs.readdirSync(plansDir).filter(f => f.startsWith(pattern) && f.endsWith('.md'));
159
+ if (matches.length > 1) {
160
+ const list = matches.map(f => ` - codev/plans/${f}`).join('\n');
161
+ throw new Error(`Multiple plan files match '${pattern}*':\n${list}`);
162
+ }
163
+ if (matches.length === 1) {
164
+ return path.join(plansDir, matches[0]);
209
165
  }
210
166
  }
211
167
  return null;
212
168
  }
169
+ /**
170
+ * Check if running in a builder worktree
171
+ */
172
+ function isBuilderContext() {
173
+ return process.cwd().includes('/.builders/');
174
+ }
175
+ /**
176
+ * Get builder project state from status.yaml
177
+ */
178
+ function getBuilderProjectState(workspaceRoot) {
179
+ const projectsDir = path.join(workspaceRoot, 'codev', 'projects');
180
+ if (!fs.existsSync(projectsDir)) {
181
+ throw new Error('No project state found. Are you in a builder worktree?');
182
+ }
183
+ const entries = fs.readdirSync(projectsDir);
184
+ const projectDirs = entries.filter(e => {
185
+ return fs.statSync(path.join(projectsDir, e)).isDirectory();
186
+ });
187
+ if (projectDirs.length === 0) {
188
+ throw new Error('No project found in codev/projects/');
189
+ }
190
+ if (projectDirs.length > 1) {
191
+ throw new Error(`Multiple projects found: ${projectDirs.join(', ')}`);
192
+ }
193
+ const dir = projectDirs[0];
194
+ const statusPath = path.join(projectsDir, dir, 'status.yaml');
195
+ if (!fs.existsSync(statusPath)) {
196
+ throw new Error(`status.yaml not found in ${dir}`);
197
+ }
198
+ const content = fs.readFileSync(statusPath, 'utf-8');
199
+ // Simple YAML parsing for the fields we need
200
+ const idMatch = content.match(/^id:\s*'?(\d+)'?\s*$/m);
201
+ const titleMatch = content.match(/^title:\s*(.+)$/m);
202
+ const phaseMatch = content.match(/^current_plan_phase:\s*(.+)$/m);
203
+ const id = idMatch?.[1] ?? '';
204
+ const title = titleMatch?.[1]?.trim() ?? '';
205
+ const rawPhase = phaseMatch?.[1]?.trim() ?? 'null';
206
+ const currentPlanPhase = rawPhase === 'null' ? null : rawPhase;
207
+ return { id, title, currentPlanPhase };
208
+ }
213
209
  /**
214
210
  * Log query to history file
215
211
  */
@@ -427,34 +423,11 @@ async function runClaudeConsultation(queryText, role, workspaceRoot, outputPath,
427
423
  }
428
424
  }
429
425
  /**
430
- * Run the consultation
426
+ * Run the consultation — dispatches to the correct model runner.
431
427
  */
432
- async function runConsultation(model, query, workspaceRoot, dryRun, reviewType, customRole, outputPath, metricsCtx) {
433
- // Use custom role if specified, otherwise use default consultant role
434
- let role = customRole ? loadCustomRole(workspaceRoot, customRole) : loadRole(workspaceRoot);
435
- // Append review type prompt if specified
436
- if (reviewType) {
437
- const typePrompt = loadReviewTypePrompt(workspaceRoot, reviewType);
438
- if (typePrompt) {
439
- role = role + '\n\n---\n\n' + typePrompt;
440
- console.error(`Review type: ${reviewType}`);
441
- }
442
- else {
443
- console.error(chalk.yellow(`Warning: Review type prompt not found: ${reviewType}`));
444
- }
445
- }
446
- // SDK-based models — handle separately from CLI subprocess models
428
+ async function runConsultation(model, query, workspaceRoot, role, outputPath, metricsCtx, generalMode) {
429
+ // SDK-based models
447
430
  if (model === 'claude') {
448
- if (dryRun) {
449
- console.log(chalk.yellow(`[claude] Would invoke Agent SDK:`));
450
- console.log(` Model: claude-opus-4-6`);
451
- console.log(` Tools: Read, Glob, Grep`);
452
- console.log(` Max turns: ${CLAUDE_MAX_TURNS}`);
453
- console.log(` Max budget: $25.00`);
454
- const promptPreview = query.substring(0, 200) + (query.length > 200 ? '...' : '');
455
- console.log(` Prompt: ${promptPreview}`);
456
- return;
457
- }
458
431
  const startTime = Date.now();
459
432
  await runClaudeConsultation(query, role, workspaceRoot, outputPath, metricsCtx);
460
433
  const duration = (Date.now() - startTime) / 1000;
@@ -463,15 +436,6 @@ async function runConsultation(model, query, workspaceRoot, dryRun, reviewType,
463
436
  return;
464
437
  }
465
438
  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
439
  const startTime = Date.now();
476
440
  await runCodexConsultation(query, role, workspaceRoot, outputPath, metricsCtx);
477
441
  const duration = (Date.now() - startTime) / 1000;
@@ -483,59 +447,41 @@ async function runConsultation(model, query, workspaceRoot, dryRun, reviewType,
483
447
  if (!config) {
484
448
  throw new Error(`Unknown model: ${model}`);
485
449
  }
486
- // Check if CLI exists (skip for dry-run mode)
487
- if (!dryRun && !commandExists(config.cli)) {
450
+ // Check if CLI exists
451
+ if (!commandExists(config.cli)) {
488
452
  throw new Error(`${config.cli} not found. Please install it first.`);
489
453
  }
490
454
  let tempFile = null;
491
455
  const env = {};
492
- // Prepare command and environment based on model
493
456
  let cmd;
494
457
  if (model === 'gemini') {
495
458
  // Gemini uses GEMINI_SYSTEM_MD env var for role
496
459
  tempFile = path.join(tmpdir(), `codev-role-${Date.now()}.md`);
497
460
  fs.writeFileSync(tempFile, role);
498
461
  env['GEMINI_SYSTEM_MD'] = tempFile;
499
- cmd = [config.cli, ...config.args, '--output-format', 'json', query];
462
+ // Use --output-format json to capture token usage/cost in structured output.
463
+ // Only use --yolo in protocol mode (structured reviews).
464
+ // General mode must NOT use --yolo to prevent unintended file writes (#370).
465
+ const yoloArgs = generalMode ? [] : ['--yolo'];
466
+ cmd = [config.cli, ...yoloArgs, '--output-format', 'json', ...config.args, query];
500
467
  }
501
468
  else {
502
469
  throw new Error(`Unknown model: ${model}`);
503
470
  }
504
- if (dryRun) {
505
- console.log(chalk.yellow(`[${model}] Would execute:`));
506
- console.log(` Command: ${cmd.join(' ')}`);
507
- if (Object.keys(env).length > 0) {
508
- for (const [key, value] of Object.entries(env)) {
509
- if (key === 'GEMINI_SYSTEM_MD') {
510
- console.log(` Env: ${key}=<temp file with consultant role>`);
511
- }
512
- else {
513
- const preview = value.substring(0, 50) + (value.length > 50 ? '...' : '');
514
- console.log(` Env: ${key}=${preview}`);
515
- }
516
- }
517
- }
518
- if (tempFile)
519
- fs.unlinkSync(tempFile);
520
- return;
521
- }
522
471
  // Execute with passthrough stdio
523
472
  // Use 'ignore' for stdin to prevent blocking when spawned as subprocess
524
- // When outputPath is set, capture stdout to write to file (used by porch)
525
473
  const fullEnv = { ...process.env, ...env };
526
474
  const startTime = Date.now();
527
- const stdoutMode = 'pipe'; // Always pipe to capture structured output for metrics
528
475
  return new Promise((resolve, reject) => {
529
476
  const proc = spawn(cmd[0], cmd.slice(1), {
530
477
  cwd: workspaceRoot,
531
478
  env: fullEnv,
532
- stdio: ['ignore', stdoutMode, 'inherit'],
479
+ stdio: ['ignore', 'pipe', 'inherit'],
533
480
  });
534
481
  const chunks = [];
535
482
  if (proc.stdout) {
536
483
  proc.stdout.on('data', (chunk) => {
537
484
  chunks.push(chunk);
538
- // Gemini: buffer only (JSON is one blob, text emitted on close)
539
485
  });
540
486
  }
541
487
  proc.on('close', (code) => {
@@ -548,10 +494,8 @@ async function runConsultation(model, query, workspaceRoot, dryRun, reviewType,
548
494
  // Extract review text from structured output (JSON/JSONL → plain text)
549
495
  const reviewText = extractReviewText(model, rawOutput);
550
496
  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
- }
497
+ // Write text to stdout (was fully buffered)
498
+ process.stdout.write(outputContent);
555
499
  // Write to output file
556
500
  if (outputPath && outputContent.length > 0) {
557
501
  const outputDir = path.dirname(outputPath);
@@ -605,7 +549,6 @@ async function runConsultation(model, query, workspaceRoot, dryRun, reviewType,
605
549
  }
606
550
  /**
607
551
  * Get a compact diff stat summary and list of changed files.
608
- * Returns { stat, files } where stat is the `--stat` output and files is the list of paths.
609
552
  */
610
553
  function getDiffStat(workspaceRoot, ref) {
611
554
  const stat = execSync(`git diff --stat ${ref}`, { cwd: workspaceRoot, encoding: 'utf-8' }).toString();
@@ -614,7 +557,7 @@ function getDiffStat(workspaceRoot, ref) {
614
557
  return { stat, files };
615
558
  }
616
559
  /**
617
- * Fetch PR metadata (no diff — reviewers read files from disk)
560
+ * Fetch PR metadata (no diff — that's fetched separately)
618
561
  */
619
562
  function fetchPRData(prNumber) {
620
563
  console.error(`Fetching PR #${prNumber} data...`);
@@ -635,12 +578,24 @@ function fetchPRData(prNumber) {
635
578
  throw new Error(`Failed to fetch PR data: ${err}`);
636
579
  }
637
580
  }
581
+ /**
582
+ * Fetch the full PR diff via gh pr diff
583
+ */
584
+ function fetchPRDiff(prNumber) {
585
+ try {
586
+ return execSync(`gh pr diff ${prNumber}`, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
587
+ }
588
+ catch (err) {
589
+ throw new Error(`Failed to fetch PR diff for #${prNumber}: ${err}`);
590
+ }
591
+ }
638
592
  /**
639
593
  * Build query for PR review.
640
- * Provides file list and instructs reviewers to read files from disk.
594
+ * Includes full PR diff + file list; model reads surrounding context from disk.
641
595
  */
642
- function buildPRQuery(prNumber, _workspaceRoot) {
596
+ function buildPRQuery(prNumber) {
643
597
  const prData = fetchPRData(prNumber);
598
+ const diff = fetchPRDiff(prNumber);
644
599
  const fileList = prData.changedFiles.map(f => `- ${f}`).join('\n');
645
600
  return `Review Pull Request #${prNumber}
646
601
 
@@ -652,10 +607,13 @@ ${prData.info}
652
607
  ## Changed Files
653
608
  ${fileList}
654
609
 
610
+ ## PR Diff
611
+ \`\`\`diff
612
+ ${diff}
613
+ \`\`\`
614
+
655
615
  ## How to Review
656
- **Read the changed files from disk** to review their current content. You have full filesystem access.
657
- For each changed file listed above, read it and evaluate the code quality, correctness, and test coverage.
658
- Do NOT rely on git diffs to determine the current state of code — diffs miss uncommitted changes in worktrees.
616
+ Review the PR diff above for the changes. You also have **full filesystem access** — read files from disk for surrounding context beyond what the diff shows.
659
617
 
660
618
  ## Comments
661
619
  ${prData.comments}
@@ -719,26 +677,22 @@ KEY_ISSUES: [List of critical issues if any, or "None"]`;
719
677
  }
720
678
  /**
721
679
  * Build query for implementation review.
722
- * Provides diff stat + file list and instructs reviewers to read files from disk.
680
+ * Accepts spec/plan paths and optional diff reference override.
723
681
  */
724
- function buildImplQuery(projectNumber, workspaceRoot, planPhase) {
725
- const specPath = findSpec(workspaceRoot, projectNumber);
726
- const planPath = findPlan(workspaceRoot, projectNumber);
727
- // Get compact diff summary against base branch
682
+ function buildImplQuery(workspaceRoot, specPath, planPath, planPhase, diffRef) {
683
+ // Get compact diff summary
728
684
  let diffStat = '';
729
685
  let changedFiles = [];
730
686
  try {
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);
687
+ const ref = diffRef ?? execSync('git merge-base HEAD main', { cwd: workspaceRoot, encoding: 'utf-8' }).trim();
688
+ const result = getDiffStat(workspaceRoot, ref);
735
689
  diffStat = result.stat;
736
690
  changedFiles = result.files;
737
691
  }
738
692
  catch {
739
693
  // If git diff fails, reviewer will explore filesystem
740
694
  }
741
- let query = `Review Implementation for Project ${projectNumber}`;
695
+ let query = `Review Implementation`;
742
696
  if (planPhase) {
743
697
  query += ` — Phase: ${planPhase}`;
744
698
  }
@@ -827,121 +781,304 @@ KEY_ISSUES: [List of critical issues if any, or "None"]`;
827
781
  return query;
828
782
  }
829
783
  /**
830
- * Main consult entry point
784
+ * Build query for phase-scoped review.
785
+ * Uses git show HEAD for the phase's atomic commit diff.
831
786
  */
832
- export async function consult(options) {
833
- const { model: modelInput, subcommand, args, dryRun = false, reviewType, role: customRole, output: outputPath } = options;
834
- // Resolve model alias
835
- const model = MODEL_ALIASES[modelInput.toLowerCase()] || modelInput.toLowerCase();
836
- // Validate model
837
- if (!MODEL_CONFIGS[model] && !SDK_MODELS.includes(model)) {
838
- const validModels = [...Object.keys(MODEL_CONFIGS), ...SDK_MODELS, ...Object.keys(MODEL_ALIASES)];
839
- throw new Error(`Unknown model: ${modelInput}\nValid models: ${validModels.join(', ')}`);
787
+ function buildPhaseQuery(workspaceRoot, planPhase, specPath, planPath) {
788
+ let phaseDiff = '';
789
+ try {
790
+ phaseDiff = execSync('git show HEAD', { cwd: workspaceRoot, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
840
791
  }
841
- // Validate review type if provided
842
- if (reviewType && !VALID_REVIEW_TYPES.includes(reviewType)) {
843
- throw new Error(`Invalid review type: ${reviewType}\nValid types: ${VALID_REVIEW_TYPES.join(', ')}`);
792
+ catch {
793
+ // If git show fails, reviewer explores filesystem
844
794
  }
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
- };
859
- console.error(`[${subcommand} review]`);
860
- console.error(`Model: ${model}`);
861
- // Log custom role if specified
862
- if (customRole) {
863
- console.error(`Role: ${customRole}`);
795
+ let query = `Review Phase Implementation: "${planPhase}"\n\n## Context Files\n`;
796
+ if (specPath)
797
+ query += `- Spec: ${specPath}\n`;
798
+ if (planPath)
799
+ query += `- Plan: ${planPath}\n`;
800
+ query += `
801
+ ## REVIEW SCOPE — CURRENT PLAN PHASE ONLY
802
+ You are reviewing **plan phase "${planPhase}" ONLY**.
803
+ Read the plan, find the section for "${planPhase}", and scope your review to ONLY the work described in that phase.
804
+
805
+ **DO NOT** request changes for work that belongs to other plan phases.
806
+ **DO NOT** flag missing functionality that is scheduled for a later phase.
807
+ **DO** verify that this phase's deliverables are complete and correct.
808
+
809
+ ## Phase Commit Diff
810
+ \`\`\`
811
+ ${phaseDiff}
812
+ \`\`\`
813
+
814
+ ## How to Review
815
+ The diff above shows the atomic commit for this phase. You also have **full filesystem access** — read files from disk to understand surrounding code.
816
+
817
+ Please review:
818
+ 1. **Spec Adherence**: Does the code fulfill the spec requirements for this phase?
819
+ 2. **Code Quality**: Is the code readable, maintainable, and bug-free?
820
+ 3. **Test Coverage**: Are there adequate tests for the changes in this phase?
821
+ 4. **Error Handling**: Are edge cases and errors handled properly?
822
+ 5. **Plan Alignment**: Does the implementation follow the plan for phase "${planPhase}"?
823
+
824
+ End your review with a verdict in this EXACT format:
825
+
826
+ ---
827
+ VERDICT: [APPROVE | REQUEST_CHANGES | COMMENT]
828
+ SUMMARY: [One-line summary of your review]
829
+ CONFIDENCE: [HIGH | MEDIUM | LOW]
830
+ ---
831
+
832
+ KEY_ISSUES: [List of critical issues if any, or "None"]`;
833
+ return query;
834
+ }
835
+ /**
836
+ * Find PR number for the current branch
837
+ */
838
+ function findPRForCurrentBranch(workspaceRoot) {
839
+ const branchName = execSync('git branch --show-current', { cwd: workspaceRoot, encoding: 'utf-8' }).trim();
840
+ const prJson = execSync(`gh pr list --head "${branchName}" --json number --jq '.[0].number'`, { cwd: workspaceRoot, encoding: 'utf-8' }).trim();
841
+ if (!prJson) {
842
+ throw new Error(`No PR found for branch: ${branchName}`);
864
843
  }
865
- let query;
866
- switch (subcommand.toLowerCase()) {
844
+ const prNumber = parseInt(prJson, 10);
845
+ if (isNaN(prNumber)) {
846
+ throw new Error(`No PR found for branch: ${branchName}`);
847
+ }
848
+ return prNumber;
849
+ }
850
+ /**
851
+ * Find PR number for a given issue number (architect mode)
852
+ */
853
+ function findPRForIssue(workspaceRoot, issueNumber) {
854
+ const prJson = execSync(`gh pr list --search "${issueNumber}" --json number,headRefName --jq '.[0]'`, { cwd: workspaceRoot, encoding: 'utf-8' }).trim();
855
+ if (!prJson || prJson === 'null') {
856
+ throw new Error(`No PR found for issue #${issueNumber}`);
857
+ }
858
+ return JSON.parse(prJson);
859
+ }
860
+ /**
861
+ * Resolve query for builder context (auto-detected from porch state)
862
+ */
863
+ function resolveBuilderQuery(workspaceRoot, type, options) {
864
+ const projectState = getBuilderProjectState(workspaceRoot);
865
+ const projectNumber = parseInt(projectState.id, 10);
866
+ switch (type) {
867
+ case 'spec': {
868
+ const specPath = findSpec(workspaceRoot, projectNumber);
869
+ if (!specPath)
870
+ throw new Error(`Spec ${projectState.id} not found in codev/specs/`);
871
+ const planPath = findPlan(workspaceRoot, projectNumber);
872
+ console.error(`Spec: ${specPath}`);
873
+ if (planPath)
874
+ console.error(`Plan: ${planPath}`);
875
+ return buildSpecQuery(specPath, planPath);
876
+ }
877
+ case 'plan': {
878
+ const planPath = findPlan(workspaceRoot, projectNumber);
879
+ if (!planPath)
880
+ throw new Error(`Plan ${projectState.id} not found in codev/plans/`);
881
+ const specPath = findSpec(workspaceRoot, projectNumber);
882
+ console.error(`Plan: ${planPath}`);
883
+ if (specPath)
884
+ console.error(`Spec: ${specPath}`);
885
+ return buildPlanQuery(planPath, specPath);
886
+ }
887
+ case 'impl': {
888
+ const specPath = findSpec(workspaceRoot, projectNumber);
889
+ const planPath = findPlan(workspaceRoot, projectNumber);
890
+ console.error(`Project: ${projectState.id}`);
891
+ if (specPath)
892
+ console.error(`Spec: ${specPath}`);
893
+ if (planPath)
894
+ console.error(`Plan: ${planPath}`);
895
+ if (options.planPhase)
896
+ console.error(`Plan phase: ${options.planPhase}`);
897
+ return buildImplQuery(workspaceRoot, specPath, planPath, options.planPhase);
898
+ }
867
899
  case 'pr': {
868
- if (args.length === 0) {
869
- throw new Error('PR number required\nUsage: consult -m <model> pr <number>');
870
- }
871
- const prNumber = parseInt(args[0], 10);
872
- if (isNaN(prNumber)) {
873
- throw new Error(`Invalid PR number: ${args[0]}`);
900
+ const prNumber = findPRForCurrentBranch(workspaceRoot);
901
+ console.error(`PR: #${prNumber}`);
902
+ return buildPRQuery(prNumber);
903
+ }
904
+ case 'phase': {
905
+ const currentPhase = options.planPhase ?? projectState.currentPlanPhase;
906
+ if (!currentPhase) {
907
+ throw new Error('No current plan phase detected. Use --plan-phase to specify.');
874
908
  }
875
- query = buildPRQuery(prNumber, workspaceRoot);
876
- break;
909
+ const specPath = findSpec(workspaceRoot, projectNumber);
910
+ const planPath = findPlan(workspaceRoot, projectNumber);
911
+ console.error(`Phase: ${currentPhase}`);
912
+ if (specPath)
913
+ console.error(`Spec: ${specPath}`);
914
+ if (planPath)
915
+ console.error(`Plan: ${planPath}`);
916
+ return buildPhaseQuery(workspaceRoot, currentPhase, specPath, planPath);
917
+ }
918
+ case 'integration': {
919
+ const prNumber = findPRForCurrentBranch(workspaceRoot);
920
+ console.error(`PR: #${prNumber} (integration review)`);
921
+ return buildPRQuery(prNumber);
877
922
  }
923
+ default:
924
+ throw new Error(`Unknown review type: ${type}\nValid types: spec, plan, impl, pr, phase, integration`);
925
+ }
926
+ }
927
+ /**
928
+ * Resolve query for architect context (requires --issue)
929
+ */
930
+ function resolveArchitectQuery(workspaceRoot, type, options) {
931
+ if (type === 'phase') {
932
+ throw new Error('--type phase requires a builder worktree. Phases only exist in builders and require the phase commit to exist.');
933
+ }
934
+ if (!options.issue) {
935
+ throw new Error(`--issue is required from architect context for --type ${type}.\n` +
936
+ `Example: consult -m gemini --protocol spir --type ${type} --issue 42`);
937
+ }
938
+ const issueNumber = parseInt(options.issue, 10);
939
+ if (isNaN(issueNumber)) {
940
+ throw new Error(`Invalid issue number: ${options.issue}`);
941
+ }
942
+ switch (type) {
878
943
  case 'spec': {
879
- if (args.length === 0) {
880
- throw new Error('Spec number required\nUsage: consult -m <model> spec <number>');
881
- }
882
- const specNumber = parseInt(args[0], 10);
883
- if (isNaN(specNumber)) {
884
- throw new Error(`Invalid spec number: ${args[0]}`);
885
- }
886
- const specPath = findSpec(workspaceRoot, specNumber);
887
- if (!specPath) {
888
- throw new Error(`Spec ${specNumber} not found`);
889
- }
890
- const planPath = findPlan(workspaceRoot, specNumber);
891
- query = buildSpecQuery(specPath, planPath);
944
+ const specPath = findSpec(workspaceRoot, issueNumber);
945
+ if (!specPath)
946
+ throw new Error(`Spec ${issueNumber} not found in codev/specs/`);
947
+ const planPath = findPlan(workspaceRoot, issueNumber);
892
948
  console.error(`Spec: ${specPath}`);
893
949
  if (planPath)
894
950
  console.error(`Plan: ${planPath}`);
895
- break;
951
+ return buildSpecQuery(specPath, planPath);
896
952
  }
897
953
  case 'plan': {
898
- if (args.length === 0) {
899
- throw new Error('Plan number required\nUsage: consult -m <model> plan <number>');
900
- }
901
- const planNumber = parseInt(args[0], 10);
902
- if (isNaN(planNumber)) {
903
- throw new Error(`Invalid plan number: ${args[0]}`);
904
- }
905
- const planPath = findPlan(workspaceRoot, planNumber);
906
- if (!planPath) {
907
- throw new Error(`Plan ${planNumber} not found`);
908
- }
909
- const specPath = findSpec(workspaceRoot, planNumber);
910
- query = buildPlanQuery(planPath, specPath);
954
+ const planPath = findPlan(workspaceRoot, issueNumber);
955
+ if (!planPath)
956
+ throw new Error(`Plan ${issueNumber} not found in codev/plans/`);
957
+ const specPath = findSpec(workspaceRoot, issueNumber);
911
958
  console.error(`Plan: ${planPath}`);
912
959
  if (specPath)
913
960
  console.error(`Spec: ${specPath}`);
914
- break;
915
- }
916
- case 'general': {
917
- if (args.length === 0) {
918
- throw new Error('Query required\nUsage: consult -m <model> general "<query>"');
919
- }
920
- query = args.join(' ');
921
- break;
961
+ return buildPlanQuery(planPath, specPath);
922
962
  }
923
963
  case 'impl': {
924
- if (args.length === 0) {
925
- throw new Error('Project number required\nUsage: consult -m <model> impl <number>');
964
+ const pr = findPRForIssue(workspaceRoot, issueNumber);
965
+ // Fetch the branch and diff from merge-base
966
+ try {
967
+ execSync(`git fetch origin ${pr.headRefName}`, { cwd: workspaceRoot, stdio: 'pipe' });
926
968
  }
927
- const implNumber = parseInt(args[0], 10);
928
- if (isNaN(implNumber)) {
929
- throw new Error(`Invalid project number: ${args[0]}`);
969
+ catch {
970
+ // May already be fetched
930
971
  }
931
- const specPath = findSpec(workspaceRoot, implNumber);
932
- const planPath = findPlan(workspaceRoot, implNumber);
933
- query = buildImplQuery(implNumber, workspaceRoot, options.planPhase);
934
- console.error(`Project: ${implNumber}`);
972
+ const mergeBase = execSync(`git merge-base main origin/${pr.headRefName}`, { cwd: workspaceRoot, encoding: 'utf-8' }).trim();
973
+ const specPath = findSpec(workspaceRoot, issueNumber);
974
+ const planPath = findPlan(workspaceRoot, issueNumber);
975
+ console.error(`Project: ${issueNumber} (PR #${pr.number}, branch: ${pr.headRefName})`);
935
976
  if (specPath)
936
977
  console.error(`Spec: ${specPath}`);
937
978
  if (planPath)
938
979
  console.error(`Plan: ${planPath}`);
939
- if (options.planPhase)
940
- console.error(`Plan phase: ${options.planPhase}`);
941
- break;
980
+ return buildImplQuery(workspaceRoot, specPath, planPath, options.planPhase, `${mergeBase}..origin/${pr.headRefName}`);
981
+ }
982
+ case 'pr': {
983
+ const pr = findPRForIssue(workspaceRoot, issueNumber);
984
+ console.error(`PR: #${pr.number}`);
985
+ return buildPRQuery(pr.number);
986
+ }
987
+ case 'integration': {
988
+ const pr = findPRForIssue(workspaceRoot, issueNumber);
989
+ console.error(`PR: #${pr.number} (integration review)`);
990
+ return buildPRQuery(pr.number);
942
991
  }
943
992
  default:
944
- throw new Error(`Unknown subcommand: ${subcommand}\nValid subcommands: pr, spec, plan, impl, general`);
993
+ throw new Error(`Unknown review type: ${type}\nValid types: spec, plan, impl, pr, phase, integration`);
994
+ }
995
+ }
996
+ /**
997
+ * Main consult entry point
998
+ */
999
+ export async function consult(options) {
1000
+ const hasPrompt = !!options.prompt || !!options.promptFile;
1001
+ const hasType = !!options.type;
1002
+ // --- Input validation ---
1003
+ // Mode conflict: --prompt/--prompt-file + --type
1004
+ if (hasPrompt && hasType) {
1005
+ throw new Error('Mode conflict: cannot use --prompt/--prompt-file with --type.\n' +
1006
+ 'Use --prompt or --prompt-file for general queries.\n' +
1007
+ 'Use --type (with optional --protocol) for protocol reviews.');
1008
+ }
1009
+ // --prompt + --prompt-file together
1010
+ if (options.prompt && options.promptFile) {
1011
+ throw new Error('Cannot use both --prompt and --prompt-file. Choose one.');
1012
+ }
1013
+ // --protocol without --type
1014
+ if (options.protocol && !options.type) {
1015
+ throw new Error('--protocol requires --type. Example: consult -m gemini --protocol spir --type spec');
1016
+ }
1017
+ // Neither mode specified
1018
+ if (!hasPrompt && !hasType) {
1019
+ throw new Error('No mode specified.\n' +
1020
+ 'General mode: consult -m <model> --prompt "question"\n' +
1021
+ 'Protocol mode: consult -m <model> --protocol <name> --type <type>\n' +
1022
+ 'Stats mode: consult stats');
1023
+ }
1024
+ // Validate --protocol and --type for path traversal
1025
+ if (options.protocol && !isValidRoleName(options.protocol)) {
1026
+ throw new Error(`Invalid protocol name: '${options.protocol}'. Only alphanumeric characters, hyphens, and underscores allowed.`);
1027
+ }
1028
+ if (options.type && !isValidRoleName(options.type)) {
1029
+ throw new Error(`Invalid type name: '${options.type}'. Only alphanumeric characters, hyphens, and underscores allowed.`);
1030
+ }
1031
+ // --- Resolve model ---
1032
+ const model = MODEL_ALIASES[options.model.toLowerCase()] || options.model.toLowerCase();
1033
+ if (!MODEL_CONFIGS[model] && !SDK_MODELS.includes(model)) {
1034
+ const validModels = [...Object.keys(MODEL_CONFIGS), ...SDK_MODELS, ...Object.keys(MODEL_ALIASES)];
1035
+ throw new Error(`Unknown model: ${options.model}\nValid models: ${validModels.join(', ')}`);
1036
+ }
1037
+ // --- Setup ---
1038
+ const workspaceRoot = findWorkspaceRoot();
1039
+ loadDotenv(workspaceRoot);
1040
+ const timestamp = new Date().toISOString();
1041
+ const metricsCtx = {
1042
+ timestamp,
1043
+ model,
1044
+ reviewType: options.type ?? null,
1045
+ subcommand: options.type ?? 'general',
1046
+ protocol: options.protocol ?? 'manual',
1047
+ projectId: options.projectId ?? null,
1048
+ workspacePath: workspaceRoot,
1049
+ };
1050
+ console.error(`Model: ${model}`);
1051
+ let query;
1052
+ let role = loadRole(workspaceRoot);
1053
+ // --- Build query based on mode ---
1054
+ if (hasType) {
1055
+ // Protocol mode
1056
+ const type = options.type;
1057
+ // Load and append protocol prompt template
1058
+ const promptTemplate = resolveProtocolPrompt(workspaceRoot, options.protocol, type);
1059
+ role = role + '\n\n---\n\n' + promptTemplate;
1060
+ console.error(`Review type: ${type}${options.protocol ? ` (protocol: ${options.protocol})` : ''}`);
1061
+ // Determine context: builder (auto-detect) vs architect (--issue or not in builder)
1062
+ const inBuilder = isBuilderContext() && !options.issue;
1063
+ if (inBuilder) {
1064
+ query = resolveBuilderQuery(workspaceRoot, type, options);
1065
+ }
1066
+ else {
1067
+ query = resolveArchitectQuery(workspaceRoot, type, options);
1068
+ }
1069
+ }
1070
+ else {
1071
+ // General mode
1072
+ if (options.prompt) {
1073
+ query = options.prompt;
1074
+ }
1075
+ else {
1076
+ const filePath = options.promptFile;
1077
+ if (!fs.existsSync(filePath)) {
1078
+ throw new Error(`Prompt file not found: ${filePath}`);
1079
+ }
1080
+ query = fs.readFileSync(filePath, 'utf-8');
1081
+ }
945
1082
  }
946
1083
  // Prepend iteration context if provided (for stateful reviews)
947
1084
  if (options.context) {
@@ -954,6 +1091,10 @@ export async function consult(options) {
954
1091
  console.error(chalk.yellow(`Warning: Could not read context file: ${options.context}`));
955
1092
  }
956
1093
  }
1094
+ // Add file access instruction for Gemini
1095
+ if (model === 'gemini') {
1096
+ query += '\n\nYou have file access. Read files directly from disk to review code.';
1097
+ }
957
1098
  // Show the query/prompt being sent
958
1099
  console.error('');
959
1100
  console.error('='.repeat(60));
@@ -965,7 +1106,8 @@ export async function consult(options) {
965
1106
  console.error(`[${model.toUpperCase()}] Starting consultation...`);
966
1107
  console.error('='.repeat(60));
967
1108
  console.error('');
968
- await runConsultation(model, query, workspaceRoot, dryRun, reviewType, customRole, outputPath, metricsCtx);
1109
+ const isGeneralMode = !hasType;
1110
+ await runConsultation(model, query, workspaceRoot, role, options.output, metricsCtx, isGeneralMode);
969
1111
  }
970
1112
  // Exported for testing
971
1113
  export { getDiffStat as _getDiffStat, buildSpecQuery as _buildSpecQuery, buildPlanQuery as _buildPlanQuery };