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