@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.
- package/dashboard/dist/assets/{index-B-s8BA2l.js → index-BblS3DWL.js} +30 -30
- package/dashboard/dist/assets/index-BblS3DWL.js.map +1 -0
- package/dashboard/dist/assets/index-Cr9PyjqX.css +32 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +23 -48
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/architect.d.ts +5 -5
- package/dist/agent-farm/commands/architect.d.ts.map +1 -1
- package/dist/agent-farm/commands/architect.js +37 -20
- package/dist/agent-farm/commands/architect.js.map +1 -1
- package/dist/agent-farm/commands/attach.d.ts.map +1 -1
- package/dist/agent-farm/commands/attach.js +1 -21
- package/dist/agent-farm/commands/attach.js.map +1 -1
- package/dist/agent-farm/commands/cleanup.d.ts +12 -0
- package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
- package/dist/agent-farm/commands/cleanup.js +108 -7
- package/dist/agent-farm/commands/cleanup.js.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.d.ts +1 -2
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +12 -18
- package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +30 -26
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts.map +1 -1
- package/dist/agent-farm/commands/start.js +2 -6
- package/dist/agent-farm/commands/start.js.map +1 -1
- package/dist/agent-farm/commands/status.d.ts.map +1 -1
- package/dist/agent-farm/commands/status.js +4 -23
- package/dist/agent-farm/commands/status.js.map +1 -1
- package/dist/agent-farm/commands/stop.d.ts.map +1 -1
- package/dist/agent-farm/commands/stop.js +2 -6
- package/dist/agent-farm/commands/stop.js.map +1 -1
- package/dist/agent-farm/commands/tower-cloud.d.ts +2 -9
- package/dist/agent-farm/commands/tower-cloud.d.ts.map +1 -1
- package/dist/agent-farm/commands/tower-cloud.js +12 -47
- package/dist/agent-farm/commands/tower-cloud.js.map +1 -1
- package/dist/agent-farm/commands/tower.d.ts.map +1 -1
- package/dist/agent-farm/commands/tower.js +6 -23
- package/dist/agent-farm/commands/tower.js.map +1 -1
- package/dist/agent-farm/db/index.d.ts.map +1 -1
- package/dist/agent-farm/db/index.js +52 -2
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/db/schema.d.ts +1 -1
- package/dist/agent-farm/db/schema.d.ts.map +1 -1
- package/dist/agent-farm/db/schema.js +1 -1
- package/dist/agent-farm/lib/cloud-config.d.ts +1 -0
- package/dist/agent-farm/lib/cloud-config.d.ts.map +1 -1
- package/dist/agent-farm/lib/cloud-config.js +2 -2
- package/dist/agent-farm/lib/cloud-config.js.map +1 -1
- package/dist/agent-farm/lib/tower-client.d.ts +49 -0
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tower-client.js +32 -2
- package/dist/agent-farm/lib/tower-client.js.map +1 -1
- package/dist/agent-farm/servers/overview.d.ts +47 -1
- package/dist/agent-farm/servers/overview.d.ts.map +1 -1
- package/dist/agent-farm/servers/overview.js +298 -58
- package/dist/agent-farm/servers/overview.js.map +1 -1
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-instances.js +2 -1
- package/dist/agent-farm/servers/tower-instances.js.map +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +21 -20
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +3 -4
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.js +4 -4
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.js +3 -19
- package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.js +2 -1
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
- package/dist/agent-farm/types.d.ts +1 -1
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/display.d.ts +8 -0
- package/dist/agent-farm/utils/display.d.ts.map +1 -0
- package/dist/agent-farm/utils/display.js +26 -0
- package/dist/agent-farm/utils/display.js.map +1 -0
- package/dist/agent-farm/utils/notifications.d.ts.map +1 -1
- package/dist/agent-farm/utils/notifications.js +7 -16
- package/dist/agent-farm/utils/notifications.js.map +1 -1
- package/dist/agent-farm/utils/server-utils.d.ts +4 -0
- package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
- package/dist/agent-farm/utils/server-utils.js +20 -0
- package/dist/agent-farm/utils/server-utils.js.map +1 -1
- package/dist/agent-farm/utils/shell.d.ts +5 -0
- package/dist/agent-farm/utils/shell.d.ts.map +1 -1
- package/dist/agent-farm/utils/shell.js +15 -11
- package/dist/agent-farm/utils/shell.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +25 -19
- package/dist/cli.js.map +1 -1
- package/dist/commands/consult/index.d.ts +10 -9
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +407 -261
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/consult/usage-extractor.d.ts +3 -0
- package/dist/commands/consult/usage-extractor.d.ts.map +1 -1
- package/dist/commands/consult/usage-extractor.js +52 -29
- package/dist/commands/consult/usage-extractor.js.map +1 -1
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +13 -12
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/next.d.ts.map +1 -1
- package/dist/commands/porch/next.js +7 -18
- package/dist/commands/porch/next.js.map +1 -1
- package/dist/commands/porch/plan.d.ts.map +1 -1
- package/dist/commands/porch/plan.js +17 -2
- package/dist/commands/porch/plan.js.map +1 -1
- package/dist/commands/porch/prompts.d.ts.map +1 -1
- package/dist/commands/porch/prompts.js +6 -3
- package/dist/commands/porch/prompts.js.map +1 -1
- package/dist/commands/porch/state.d.ts +13 -0
- package/dist/commands/porch/state.d.ts.map +1 -1
- package/dist/commands/porch/state.js +46 -1
- package/dist/commands/porch/state.js.map +1 -1
- package/dist/lib/github.d.ts +10 -9
- package/dist/lib/github.d.ts.map +1 -1
- package/dist/lib/github.js +49 -9
- package/dist/lib/github.js.map +1 -1
- package/dist/terminal/index.d.ts +2 -0
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +2 -0
- package/dist/terminal/index.js.map +1 -1
- package/dist/terminal/pty-manager.js +2 -2
- package/dist/terminal/pty-manager.js.map +1 -1
- package/dist/terminal/pty-session.js +1 -1
- package/dist/terminal/pty-session.js.map +1 -1
- package/package.json +1 -1
- package/skeleton/.claude/skills/af/SKILL.md +9 -9
- package/skeleton/.claude/skills/consult/SKILL.md +55 -38
- package/skeleton/.claude/skills/porch/SKILL.md +53 -0
- package/skeleton/DEPENDENCIES.md +2 -2
- package/skeleton/builders.md +8 -19
- package/skeleton/protocol-schema.json +1 -1
- package/skeleton/protocols/bugfix/prompts/pr.md +3 -3
- package/skeleton/protocols/bugfix/protocol.json +1 -1
- package/skeleton/protocols/maintain/consult-types/impl-review.md +72 -0
- package/skeleton/protocols/maintain/consult-types/pr-review.md +72 -0
- package/skeleton/protocols/maintain/protocol.json +4 -4
- package/skeleton/protocols/maintain/protocol.md +3 -3
- package/skeleton/protocols/protocol-schema.json +1 -1
- package/skeleton/protocols/spir/consult-types/impl-review.md +72 -0
- package/skeleton/protocols/spir/consult-types/phase-review.md +72 -0
- package/skeleton/protocols/spir/consult-types/pr-review.md +72 -0
- package/skeleton/protocols/spir/prompts/plan.md +4 -4
- package/skeleton/protocols/spir/prompts/review.md +8 -8
- package/skeleton/protocols/spir/prompts/specify.md +6 -6
- package/skeleton/protocols/spir/protocol.json +11 -11
- package/skeleton/protocols/spir/templates/review.md +2 -2
- package/skeleton/protocols/tick/consult-types/impl-review.md +72 -0
- package/skeleton/protocols/tick/consult-types/plan-review.md +59 -0
- package/skeleton/protocols/tick/consult-types/pr-review.md +72 -0
- package/skeleton/protocols/tick/consult-types/spec-review.md +55 -0
- package/skeleton/protocols/tick/protocol.json +2 -7
- package/skeleton/resources/commands/agent-farm.md +13 -11
- package/skeleton/resources/commands/codev.md +0 -35
- package/skeleton/resources/commands/consult.md +88 -234
- package/skeleton/resources/commands/overview.md +6 -7
- package/skeleton/resources/workflow-reference.md +24 -24
- package/skeleton/roles/architect.md +29 -29
- package/skeleton/roles/builder.md +22 -23
- package/skeleton/templates/AGENTS.md +1 -1
- package/skeleton/templates/CLAUDE.md +1 -1
- package/skeleton/templates/cheatsheet.md +22 -18
- package/skeleton/templates/pr-overview.md +5 -5
- package/dashboard/dist/assets/index-B-s8BA2l.js.map +0 -1
- package/dashboard/dist/assets/index-DB2AxRP7.css +0 -32
- package/dist/agent-farm/commands/consult.d.ts +0 -15
- package/dist/agent-farm/commands/consult.d.ts.map +0 -1
- package/dist/agent-farm/commands/consult.js +0 -39
- package/dist/agent-farm/commands/consult.js.map +0 -1
- /package/skeleton/{consult-types → protocols/bugfix/consult-types}/impl-review.md +0 -0
- /package/skeleton/{consult-types/pr-ready.md → protocols/bugfix/consult-types/pr-review.md} +0 -0
- /package/skeleton/{consult-types → protocols/spir/consult-types}/plan-review.md +0 -0
- /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
|
-
*
|
|
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
|
|
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: [
|
|
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
|
|
64
|
+
* Validate name to prevent directory traversal attacks.
|
|
70
65
|
* Only allows alphanumeric, hyphen, and underscore characters.
|
|
71
66
|
*/
|
|
72
|
-
function isValidRoleName(
|
|
73
|
-
return /^[a-zA-Z0-9_-]+$/.test(
|
|
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
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
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
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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,
|
|
433
|
-
//
|
|
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
|
|
487
|
-
if (!
|
|
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
|
-
|
|
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',
|
|
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
|
|
552
|
-
|
|
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 —
|
|
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
|
-
*
|
|
598
|
+
* Includes full PR diff + file list; model reads surrounding context from disk.
|
|
641
599
|
*/
|
|
642
|
-
function buildPRQuery(prNumber
|
|
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
|
-
|
|
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
|
-
*
|
|
684
|
+
* Accepts spec/plan paths and optional diff reference override.
|
|
723
685
|
*/
|
|
724
|
-
function buildImplQuery(
|
|
725
|
-
|
|
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
|
|
732
|
-
|
|
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
|
|
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
|
-
*
|
|
788
|
+
* Build query for phase-scoped review.
|
|
789
|
+
* Uses git show HEAD for the phase's atomic commit diff.
|
|
831
790
|
*/
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
|
|
842
|
-
|
|
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
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
866
|
-
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
-
|
|
876
|
-
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
const
|
|
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
|
-
|
|
955
|
+
return buildSpecQuery(specPath, planPath);
|
|
896
956
|
}
|
|
897
957
|
case 'plan': {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
925
|
-
|
|
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
|
-
|
|
928
|
-
|
|
929
|
-
throw new Error(`Invalid project number: ${args[0]}`);
|
|
973
|
+
catch {
|
|
974
|
+
// May already be fetched
|
|
930
975
|
}
|
|
931
|
-
const
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
console.error(`Project: ${
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
|
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
|
-
|
|
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 };
|