@cluesmith/codev 2.0.6 → 2.0.8

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 +407 -261
  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 +29 -29
  167. package/skeleton/roles/builder.md +22 -23
  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,87 @@ 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
- const pattern = String(number).padStart(4, '0');
137
+ const unpadded = String(number);
138
+ const padded = unpadded.padStart(4, '0');
187
139
  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
- }
140
+ const files = fs.readdirSync(specsDir).filter(f => f.endsWith('.md'));
141
+ const matches = files.filter(f => f.startsWith(`${unpadded}-`) || f.startsWith(`${padded}-`));
142
+ if (matches.length > 1) {
143
+ const list = matches.map(f => ` - codev/specs/${f}`).join('\n');
144
+ throw new Error(`Multiple spec files match '${unpadded}' or '${padded}':\n${list}`);
145
+ }
146
+ if (matches.length === 1) {
147
+ return path.join(specsDir, matches[0]);
193
148
  }
194
149
  }
195
150
  return null;
196
151
  }
197
152
  /**
198
- * Find a plan file by number
153
+ * Find a plan file by number. Returns null if not found.
154
+ * Errors if multiple matches found.
199
155
  */
200
156
  function findPlan(workspaceRoot, number) {
201
157
  const plansDir = path.join(workspaceRoot, 'codev', 'plans');
202
- const pattern = String(number).padStart(4, '0');
158
+ const unpadded = String(number);
159
+ const padded = unpadded.padStart(4, '0');
203
160
  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
- }
161
+ const files = fs.readdirSync(plansDir).filter(f => f.endsWith('.md'));
162
+ const matches = files.filter(f => f.startsWith(`${unpadded}-`) || f.startsWith(`${padded}-`));
163
+ if (matches.length > 1) {
164
+ const list = matches.map(f => ` - codev/plans/${f}`).join('\n');
165
+ throw new Error(`Multiple plan files match '${unpadded}' or '${padded}':\n${list}`);
166
+ }
167
+ if (matches.length === 1) {
168
+ return path.join(plansDir, matches[0]);
209
169
  }
210
170
  }
211
171
  return null;
212
172
  }
173
+ /**
174
+ * Check if running in a builder worktree
175
+ */
176
+ function isBuilderContext() {
177
+ return process.cwd().includes('/.builders/');
178
+ }
179
+ /**
180
+ * Get builder project state from status.yaml
181
+ */
182
+ function getBuilderProjectState(workspaceRoot) {
183
+ const projectsDir = path.join(workspaceRoot, 'codev', 'projects');
184
+ if (!fs.existsSync(projectsDir)) {
185
+ throw new Error('No project state found. Are you in a builder worktree?');
186
+ }
187
+ const entries = fs.readdirSync(projectsDir);
188
+ const projectDirs = entries.filter(e => {
189
+ return fs.statSync(path.join(projectsDir, e)).isDirectory();
190
+ });
191
+ if (projectDirs.length === 0) {
192
+ throw new Error('No project found in codev/projects/');
193
+ }
194
+ if (projectDirs.length > 1) {
195
+ throw new Error(`Multiple projects found: ${projectDirs.join(', ')}`);
196
+ }
197
+ const dir = projectDirs[0];
198
+ const statusPath = path.join(projectsDir, dir, 'status.yaml');
199
+ if (!fs.existsSync(statusPath)) {
200
+ throw new Error(`status.yaml not found in ${dir}`);
201
+ }
202
+ const content = fs.readFileSync(statusPath, 'utf-8');
203
+ // Simple YAML parsing for the fields we need
204
+ const idMatch = content.match(/^id:\s*'?(\d+)'?\s*$/m);
205
+ const titleMatch = content.match(/^title:\s*(.+)$/m);
206
+ const phaseMatch = content.match(/^current_plan_phase:\s*(.+)$/m);
207
+ const id = idMatch?.[1] ?? '';
208
+ const title = titleMatch?.[1]?.trim() ?? '';
209
+ const rawPhase = phaseMatch?.[1]?.trim() ?? 'null';
210
+ const currentPlanPhase = rawPhase === 'null' ? null : rawPhase;
211
+ return { id, title, currentPlanPhase };
212
+ }
213
213
  /**
214
214
  * Log query to history file
215
215
  */
@@ -427,34 +427,11 @@ async function runClaudeConsultation(queryText, role, workspaceRoot, outputPath,
427
427
  }
428
428
  }
429
429
  /**
430
- * Run the consultation
430
+ * Run the consultation — dispatches to the correct model runner.
431
431
  */
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
432
+ async function runConsultation(model, query, workspaceRoot, role, outputPath, metricsCtx, generalMode) {
433
+ // SDK-based models
447
434
  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
435
  const startTime = Date.now();
459
436
  await runClaudeConsultation(query, role, workspaceRoot, outputPath, metricsCtx);
460
437
  const duration = (Date.now() - startTime) / 1000;
@@ -463,15 +440,6 @@ async function runConsultation(model, query, workspaceRoot, dryRun, reviewType,
463
440
  return;
464
441
  }
465
442
  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
443
  const startTime = Date.now();
476
444
  await runCodexConsultation(query, role, workspaceRoot, outputPath, metricsCtx);
477
445
  const duration = (Date.now() - startTime) / 1000;
@@ -483,59 +451,41 @@ async function runConsultation(model, query, workspaceRoot, dryRun, reviewType,
483
451
  if (!config) {
484
452
  throw new Error(`Unknown model: ${model}`);
485
453
  }
486
- // Check if CLI exists (skip for dry-run mode)
487
- if (!dryRun && !commandExists(config.cli)) {
454
+ // Check if CLI exists
455
+ if (!commandExists(config.cli)) {
488
456
  throw new Error(`${config.cli} not found. Please install it first.`);
489
457
  }
490
458
  let tempFile = null;
491
459
  const env = {};
492
- // Prepare command and environment based on model
493
460
  let cmd;
494
461
  if (model === 'gemini') {
495
462
  // Gemini uses GEMINI_SYSTEM_MD env var for role
496
463
  tempFile = path.join(tmpdir(), `codev-role-${Date.now()}.md`);
497
464
  fs.writeFileSync(tempFile, role);
498
465
  env['GEMINI_SYSTEM_MD'] = tempFile;
499
- cmd = [config.cli, ...config.args, '--output-format', 'json', query];
466
+ // Use --output-format json to capture token usage/cost in structured output.
467
+ // Only use --yolo in protocol mode (structured reviews).
468
+ // General mode must NOT use --yolo to prevent unintended file writes (#370).
469
+ const yoloArgs = generalMode ? [] : ['--yolo'];
470
+ cmd = [config.cli, ...yoloArgs, '--output-format', 'json', ...config.args, query];
500
471
  }
501
472
  else {
502
473
  throw new Error(`Unknown model: ${model}`);
503
474
  }
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
475
  // Execute with passthrough stdio
523
476
  // 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
477
  const fullEnv = { ...process.env, ...env };
526
478
  const startTime = Date.now();
527
- const stdoutMode = 'pipe'; // Always pipe to capture structured output for metrics
528
479
  return new Promise((resolve, reject) => {
529
480
  const proc = spawn(cmd[0], cmd.slice(1), {
530
481
  cwd: workspaceRoot,
531
482
  env: fullEnv,
532
- stdio: ['ignore', stdoutMode, 'inherit'],
483
+ stdio: ['ignore', 'pipe', 'inherit'],
533
484
  });
534
485
  const chunks = [];
535
486
  if (proc.stdout) {
536
487
  proc.stdout.on('data', (chunk) => {
537
488
  chunks.push(chunk);
538
- // Gemini: buffer only (JSON is one blob, text emitted on close)
539
489
  });
540
490
  }
541
491
  proc.on('close', (code) => {
@@ -548,10 +498,8 @@ async function runConsultation(model, query, workspaceRoot, dryRun, reviewType,
548
498
  // Extract review text from structured output (JSON/JSONL → plain text)
549
499
  const reviewText = extractReviewText(model, rawOutput);
550
500
  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
- }
501
+ // Write text to stdout (was fully buffered)
502
+ process.stdout.write(outputContent);
555
503
  // Write to output file
556
504
  if (outputPath && outputContent.length > 0) {
557
505
  const outputDir = path.dirname(outputPath);
@@ -605,7 +553,6 @@ async function runConsultation(model, query, workspaceRoot, dryRun, reviewType,
605
553
  }
606
554
  /**
607
555
  * 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
556
  */
610
557
  function getDiffStat(workspaceRoot, ref) {
611
558
  const stat = execSync(`git diff --stat ${ref}`, { cwd: workspaceRoot, encoding: 'utf-8' }).toString();
@@ -614,7 +561,7 @@ function getDiffStat(workspaceRoot, ref) {
614
561
  return { stat, files };
615
562
  }
616
563
  /**
617
- * Fetch PR metadata (no diff — reviewers read files from disk)
564
+ * Fetch PR metadata (no diff — that's fetched separately)
618
565
  */
619
566
  function fetchPRData(prNumber) {
620
567
  console.error(`Fetching PR #${prNumber} data...`);
@@ -635,12 +582,24 @@ function fetchPRData(prNumber) {
635
582
  throw new Error(`Failed to fetch PR data: ${err}`);
636
583
  }
637
584
  }
585
+ /**
586
+ * Fetch the full PR diff via gh pr diff
587
+ */
588
+ function fetchPRDiff(prNumber) {
589
+ try {
590
+ return execSync(`gh pr diff ${prNumber}`, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
591
+ }
592
+ catch (err) {
593
+ throw new Error(`Failed to fetch PR diff for #${prNumber}: ${err}`);
594
+ }
595
+ }
638
596
  /**
639
597
  * Build query for PR review.
640
- * Provides file list and instructs reviewers to read files from disk.
598
+ * Includes full PR diff + file list; model reads surrounding context from disk.
641
599
  */
642
- function buildPRQuery(prNumber, _workspaceRoot) {
600
+ function buildPRQuery(prNumber) {
643
601
  const prData = fetchPRData(prNumber);
602
+ const diff = fetchPRDiff(prNumber);
644
603
  const fileList = prData.changedFiles.map(f => `- ${f}`).join('\n');
645
604
  return `Review Pull Request #${prNumber}
646
605
 
@@ -652,10 +611,13 @@ ${prData.info}
652
611
  ## Changed Files
653
612
  ${fileList}
654
613
 
614
+ ## PR Diff
615
+ \`\`\`diff
616
+ ${diff}
617
+ \`\`\`
618
+
655
619
  ## 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.
620
+ 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
621
 
660
622
  ## Comments
661
623
  ${prData.comments}
@@ -719,26 +681,22 @@ KEY_ISSUES: [List of critical issues if any, or "None"]`;
719
681
  }
720
682
  /**
721
683
  * Build query for implementation review.
722
- * Provides diff stat + file list and instructs reviewers to read files from disk.
684
+ * Accepts spec/plan paths and optional diff reference override.
723
685
  */
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
686
+ function buildImplQuery(workspaceRoot, specPath, planPath, planPhase, diffRef) {
687
+ // Get compact diff summary
728
688
  let diffStat = '';
729
689
  let changedFiles = [];
730
690
  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);
691
+ const ref = diffRef ?? execSync('git merge-base HEAD main', { cwd: workspaceRoot, encoding: 'utf-8' }).trim();
692
+ const result = getDiffStat(workspaceRoot, ref);
735
693
  diffStat = result.stat;
736
694
  changedFiles = result.files;
737
695
  }
738
696
  catch {
739
697
  // If git diff fails, reviewer will explore filesystem
740
698
  }
741
- let query = `Review Implementation for Project ${projectNumber}`;
699
+ let query = `Review Implementation`;
742
700
  if (planPhase) {
743
701
  query += ` — Phase: ${planPhase}`;
744
702
  }
@@ -827,121 +785,304 @@ KEY_ISSUES: [List of critical issues if any, or "None"]`;
827
785
  return query;
828
786
  }
829
787
  /**
830
- * Main consult entry point
788
+ * Build query for phase-scoped review.
789
+ * Uses git show HEAD for the phase's atomic commit diff.
831
790
  */
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(', ')}`);
791
+ function buildPhaseQuery(workspaceRoot, planPhase, specPath, planPath) {
792
+ let phaseDiff = '';
793
+ try {
794
+ phaseDiff = execSync('git show HEAD', { cwd: workspaceRoot, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
840
795
  }
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(', ')}`);
796
+ catch {
797
+ // If git show fails, reviewer explores filesystem
844
798
  }
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}`);
799
+ let query = `Review Phase Implementation: "${planPhase}"\n\n## Context Files\n`;
800
+ if (specPath)
801
+ query += `- Spec: ${specPath}\n`;
802
+ if (planPath)
803
+ query += `- Plan: ${planPath}\n`;
804
+ query += `
805
+ ## REVIEW SCOPE — CURRENT PLAN PHASE ONLY
806
+ You are reviewing **plan phase "${planPhase}" ONLY**.
807
+ Read the plan, find the section for "${planPhase}", and scope your review to ONLY the work described in that phase.
808
+
809
+ **DO NOT** request changes for work that belongs to other plan phases.
810
+ **DO NOT** flag missing functionality that is scheduled for a later phase.
811
+ **DO** verify that this phase's deliverables are complete and correct.
812
+
813
+ ## Phase Commit Diff
814
+ \`\`\`
815
+ ${phaseDiff}
816
+ \`\`\`
817
+
818
+ ## How to Review
819
+ The diff above shows the atomic commit for this phase. You also have **full filesystem access** — read files from disk to understand surrounding code.
820
+
821
+ Please review:
822
+ 1. **Spec Adherence**: Does the code fulfill the spec requirements for this phase?
823
+ 2. **Code Quality**: Is the code readable, maintainable, and bug-free?
824
+ 3. **Test Coverage**: Are there adequate tests for the changes in this phase?
825
+ 4. **Error Handling**: Are edge cases and errors handled properly?
826
+ 5. **Plan Alignment**: Does the implementation follow the plan for phase "${planPhase}"?
827
+
828
+ End your review with a verdict in this EXACT format:
829
+
830
+ ---
831
+ VERDICT: [APPROVE | REQUEST_CHANGES | COMMENT]
832
+ SUMMARY: [One-line summary of your review]
833
+ CONFIDENCE: [HIGH | MEDIUM | LOW]
834
+ ---
835
+
836
+ KEY_ISSUES: [List of critical issues if any, or "None"]`;
837
+ return query;
838
+ }
839
+ /**
840
+ * Find PR number for the current branch
841
+ */
842
+ function findPRForCurrentBranch(workspaceRoot) {
843
+ const branchName = execSync('git branch --show-current', { cwd: workspaceRoot, encoding: 'utf-8' }).trim();
844
+ const prJson = execSync(`gh pr list --head "${branchName}" --json number --jq '.[0].number'`, { cwd: workspaceRoot, encoding: 'utf-8' }).trim();
845
+ if (!prJson) {
846
+ throw new Error(`No PR found for branch: ${branchName}`);
864
847
  }
865
- let query;
866
- switch (subcommand.toLowerCase()) {
848
+ const prNumber = parseInt(prJson, 10);
849
+ if (isNaN(prNumber)) {
850
+ throw new Error(`No PR found for branch: ${branchName}`);
851
+ }
852
+ return prNumber;
853
+ }
854
+ /**
855
+ * Find PR number for a given issue number (architect mode)
856
+ */
857
+ function findPRForIssue(workspaceRoot, issueNumber) {
858
+ const prJson = execSync(`gh pr list --search "${issueNumber}" --json number,headRefName --jq '.[0]'`, { cwd: workspaceRoot, encoding: 'utf-8' }).trim();
859
+ if (!prJson || prJson === 'null') {
860
+ throw new Error(`No PR found for issue #${issueNumber}`);
861
+ }
862
+ return JSON.parse(prJson);
863
+ }
864
+ /**
865
+ * Resolve query for builder context (auto-detected from porch state)
866
+ */
867
+ function resolveBuilderQuery(workspaceRoot, type, options) {
868
+ const projectState = getBuilderProjectState(workspaceRoot);
869
+ const projectNumber = parseInt(projectState.id, 10);
870
+ switch (type) {
871
+ case 'spec': {
872
+ const specPath = findSpec(workspaceRoot, projectNumber);
873
+ if (!specPath)
874
+ throw new Error(`Spec ${projectState.id} not found in codev/specs/`);
875
+ const planPath = findPlan(workspaceRoot, projectNumber);
876
+ console.error(`Spec: ${specPath}`);
877
+ if (planPath)
878
+ console.error(`Plan: ${planPath}`);
879
+ return buildSpecQuery(specPath, planPath);
880
+ }
881
+ case 'plan': {
882
+ const planPath = findPlan(workspaceRoot, projectNumber);
883
+ if (!planPath)
884
+ throw new Error(`Plan ${projectState.id} not found in codev/plans/`);
885
+ const specPath = findSpec(workspaceRoot, projectNumber);
886
+ console.error(`Plan: ${planPath}`);
887
+ if (specPath)
888
+ console.error(`Spec: ${specPath}`);
889
+ return buildPlanQuery(planPath, specPath);
890
+ }
891
+ case 'impl': {
892
+ const specPath = findSpec(workspaceRoot, projectNumber);
893
+ const planPath = findPlan(workspaceRoot, projectNumber);
894
+ console.error(`Project: ${projectState.id}`);
895
+ if (specPath)
896
+ console.error(`Spec: ${specPath}`);
897
+ if (planPath)
898
+ console.error(`Plan: ${planPath}`);
899
+ if (options.planPhase)
900
+ console.error(`Plan phase: ${options.planPhase}`);
901
+ return buildImplQuery(workspaceRoot, specPath, planPath, options.planPhase);
902
+ }
867
903
  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]}`);
904
+ const prNumber = findPRForCurrentBranch(workspaceRoot);
905
+ console.error(`PR: #${prNumber}`);
906
+ return buildPRQuery(prNumber);
907
+ }
908
+ case 'phase': {
909
+ const currentPhase = options.planPhase ?? projectState.currentPlanPhase;
910
+ if (!currentPhase) {
911
+ throw new Error('No current plan phase detected. Use --plan-phase to specify.');
874
912
  }
875
- query = buildPRQuery(prNumber, workspaceRoot);
876
- break;
913
+ const specPath = findSpec(workspaceRoot, projectNumber);
914
+ const planPath = findPlan(workspaceRoot, projectNumber);
915
+ console.error(`Phase: ${currentPhase}`);
916
+ if (specPath)
917
+ console.error(`Spec: ${specPath}`);
918
+ if (planPath)
919
+ console.error(`Plan: ${planPath}`);
920
+ return buildPhaseQuery(workspaceRoot, currentPhase, specPath, planPath);
921
+ }
922
+ case 'integration': {
923
+ const prNumber = findPRForCurrentBranch(workspaceRoot);
924
+ console.error(`PR: #${prNumber} (integration review)`);
925
+ return buildPRQuery(prNumber);
877
926
  }
927
+ default:
928
+ throw new Error(`Unknown review type: ${type}\nValid types: spec, plan, impl, pr, phase, integration`);
929
+ }
930
+ }
931
+ /**
932
+ * Resolve query for architect context (requires --issue)
933
+ */
934
+ function resolveArchitectQuery(workspaceRoot, type, options) {
935
+ if (type === 'phase') {
936
+ throw new Error('--type phase requires a builder worktree. Phases only exist in builders and require the phase commit to exist.');
937
+ }
938
+ if (!options.issue) {
939
+ throw new Error(`--issue is required from architect context for --type ${type}.\n` +
940
+ `Example: consult -m gemini --protocol spir --type ${type} --issue 42`);
941
+ }
942
+ const issueNumber = parseInt(options.issue, 10);
943
+ if (isNaN(issueNumber)) {
944
+ throw new Error(`Invalid issue number: ${options.issue}`);
945
+ }
946
+ switch (type) {
878
947
  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);
948
+ const specPath = findSpec(workspaceRoot, issueNumber);
949
+ if (!specPath)
950
+ throw new Error(`Spec ${issueNumber} not found in codev/specs/`);
951
+ const planPath = findPlan(workspaceRoot, issueNumber);
892
952
  console.error(`Spec: ${specPath}`);
893
953
  if (planPath)
894
954
  console.error(`Plan: ${planPath}`);
895
- break;
955
+ return buildSpecQuery(specPath, planPath);
896
956
  }
897
957
  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);
958
+ const planPath = findPlan(workspaceRoot, issueNumber);
959
+ if (!planPath)
960
+ throw new Error(`Plan ${issueNumber} not found in codev/plans/`);
961
+ const specPath = findSpec(workspaceRoot, issueNumber);
911
962
  console.error(`Plan: ${planPath}`);
912
963
  if (specPath)
913
964
  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;
965
+ return buildPlanQuery(planPath, specPath);
922
966
  }
923
967
  case 'impl': {
924
- if (args.length === 0) {
925
- throw new Error('Project number required\nUsage: consult -m <model> impl <number>');
968
+ const pr = findPRForIssue(workspaceRoot, issueNumber);
969
+ // Fetch the branch and diff from merge-base
970
+ try {
971
+ execSync(`git fetch origin ${pr.headRefName}`, { cwd: workspaceRoot, stdio: 'pipe' });
926
972
  }
927
- const implNumber = parseInt(args[0], 10);
928
- if (isNaN(implNumber)) {
929
- throw new Error(`Invalid project number: ${args[0]}`);
973
+ catch {
974
+ // May already be fetched
930
975
  }
931
- const specPath = findSpec(workspaceRoot, implNumber);
932
- const planPath = findPlan(workspaceRoot, implNumber);
933
- query = buildImplQuery(implNumber, workspaceRoot, options.planPhase);
934
- console.error(`Project: ${implNumber}`);
976
+ const mergeBase = execSync(`git merge-base main origin/${pr.headRefName}`, { cwd: workspaceRoot, encoding: 'utf-8' }).trim();
977
+ const specPath = findSpec(workspaceRoot, issueNumber);
978
+ const planPath = findPlan(workspaceRoot, issueNumber);
979
+ console.error(`Project: ${issueNumber} (PR #${pr.number}, branch: ${pr.headRefName})`);
935
980
  if (specPath)
936
981
  console.error(`Spec: ${specPath}`);
937
982
  if (planPath)
938
983
  console.error(`Plan: ${planPath}`);
939
- if (options.planPhase)
940
- console.error(`Plan phase: ${options.planPhase}`);
941
- break;
984
+ return buildImplQuery(workspaceRoot, specPath, planPath, options.planPhase, `${mergeBase}..origin/${pr.headRefName}`);
985
+ }
986
+ case 'pr': {
987
+ const pr = findPRForIssue(workspaceRoot, issueNumber);
988
+ console.error(`PR: #${pr.number}`);
989
+ return buildPRQuery(pr.number);
990
+ }
991
+ case 'integration': {
992
+ const pr = findPRForIssue(workspaceRoot, issueNumber);
993
+ console.error(`PR: #${pr.number} (integration review)`);
994
+ return buildPRQuery(pr.number);
942
995
  }
943
996
  default:
944
- throw new Error(`Unknown subcommand: ${subcommand}\nValid subcommands: pr, spec, plan, impl, general`);
997
+ throw new Error(`Unknown review type: ${type}\nValid types: spec, plan, impl, pr, phase, integration`);
998
+ }
999
+ }
1000
+ /**
1001
+ * Main consult entry point
1002
+ */
1003
+ export async function consult(options) {
1004
+ const hasPrompt = !!options.prompt || !!options.promptFile;
1005
+ const hasType = !!options.type;
1006
+ // --- Input validation ---
1007
+ // Mode conflict: --prompt/--prompt-file + --type
1008
+ if (hasPrompt && hasType) {
1009
+ throw new Error('Mode conflict: cannot use --prompt/--prompt-file with --type.\n' +
1010
+ 'Use --prompt or --prompt-file for general queries.\n' +
1011
+ 'Use --type (with optional --protocol) for protocol reviews.');
1012
+ }
1013
+ // --prompt + --prompt-file together
1014
+ if (options.prompt && options.promptFile) {
1015
+ throw new Error('Cannot use both --prompt and --prompt-file. Choose one.');
1016
+ }
1017
+ // --protocol without --type
1018
+ if (options.protocol && !options.type) {
1019
+ throw new Error('--protocol requires --type. Example: consult -m gemini --protocol spir --type spec');
1020
+ }
1021
+ // Neither mode specified
1022
+ if (!hasPrompt && !hasType) {
1023
+ throw new Error('No mode specified.\n' +
1024
+ 'General mode: consult -m <model> --prompt "question"\n' +
1025
+ 'Protocol mode: consult -m <model> --protocol <name> --type <type>\n' +
1026
+ 'Stats mode: consult stats');
1027
+ }
1028
+ // Validate --protocol and --type for path traversal
1029
+ if (options.protocol && !isValidRoleName(options.protocol)) {
1030
+ throw new Error(`Invalid protocol name: '${options.protocol}'. Only alphanumeric characters, hyphens, and underscores allowed.`);
1031
+ }
1032
+ if (options.type && !isValidRoleName(options.type)) {
1033
+ throw new Error(`Invalid type name: '${options.type}'. Only alphanumeric characters, hyphens, and underscores allowed.`);
1034
+ }
1035
+ // --- Resolve model ---
1036
+ const model = MODEL_ALIASES[options.model.toLowerCase()] || options.model.toLowerCase();
1037
+ if (!MODEL_CONFIGS[model] && !SDK_MODELS.includes(model)) {
1038
+ const validModels = [...Object.keys(MODEL_CONFIGS), ...SDK_MODELS, ...Object.keys(MODEL_ALIASES)];
1039
+ throw new Error(`Unknown model: ${options.model}\nValid models: ${validModels.join(', ')}`);
1040
+ }
1041
+ // --- Setup ---
1042
+ const workspaceRoot = findWorkspaceRoot();
1043
+ loadDotenv(workspaceRoot);
1044
+ const timestamp = new Date().toISOString();
1045
+ const metricsCtx = {
1046
+ timestamp,
1047
+ model,
1048
+ reviewType: options.type ?? null,
1049
+ subcommand: options.type ?? 'general',
1050
+ protocol: options.protocol ?? 'manual',
1051
+ projectId: options.projectId ?? null,
1052
+ workspacePath: workspaceRoot,
1053
+ };
1054
+ console.error(`Model: ${model}`);
1055
+ let query;
1056
+ let role = loadRole(workspaceRoot);
1057
+ // --- Build query based on mode ---
1058
+ if (hasType) {
1059
+ // Protocol mode
1060
+ const type = options.type;
1061
+ // Load and append protocol prompt template
1062
+ const promptTemplate = resolveProtocolPrompt(workspaceRoot, options.protocol, type);
1063
+ role = role + '\n\n---\n\n' + promptTemplate;
1064
+ console.error(`Review type: ${type}${options.protocol ? ` (protocol: ${options.protocol})` : ''}`);
1065
+ // Determine context: builder (auto-detect) vs architect (--issue or not in builder)
1066
+ const inBuilder = isBuilderContext() && !options.issue;
1067
+ if (inBuilder) {
1068
+ query = resolveBuilderQuery(workspaceRoot, type, options);
1069
+ }
1070
+ else {
1071
+ query = resolveArchitectQuery(workspaceRoot, type, options);
1072
+ }
1073
+ }
1074
+ else {
1075
+ // General mode
1076
+ if (options.prompt) {
1077
+ query = options.prompt;
1078
+ }
1079
+ else {
1080
+ const filePath = options.promptFile;
1081
+ if (!fs.existsSync(filePath)) {
1082
+ throw new Error(`Prompt file not found: ${filePath}`);
1083
+ }
1084
+ query = fs.readFileSync(filePath, 'utf-8');
1085
+ }
945
1086
  }
946
1087
  // Prepend iteration context if provided (for stateful reviews)
947
1088
  if (options.context) {
@@ -954,6 +1095,10 @@ export async function consult(options) {
954
1095
  console.error(chalk.yellow(`Warning: Could not read context file: ${options.context}`));
955
1096
  }
956
1097
  }
1098
+ // Add file access instruction for Gemini
1099
+ if (model === 'gemini') {
1100
+ query += '\n\nYou have file access. Read files directly from disk to review code.';
1101
+ }
957
1102
  // Show the query/prompt being sent
958
1103
  console.error('');
959
1104
  console.error('='.repeat(60));
@@ -965,7 +1110,8 @@ export async function consult(options) {
965
1110
  console.error(`[${model.toUpperCase()}] Starting consultation...`);
966
1111
  console.error('='.repeat(60));
967
1112
  console.error('');
968
- await runConsultation(model, query, workspaceRoot, dryRun, reviewType, customRole, outputPath, metricsCtx);
1113
+ const isGeneralMode = !hasType;
1114
+ await runConsultation(model, query, workspaceRoot, role, options.output, metricsCtx, isGeneralMode);
969
1115
  }
970
1116
  // Exported for testing
971
1117
  export { getDiffStat as _getDiffStat, buildSpecQuery as _buildSpecQuery, buildPlanQuery as _buildPlanQuery };