@cluesmith/codev 2.0.3 → 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-BblS3DWL.js +135 -0
- 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 +54 -61
- 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 +19 -0
- package/dist/agent-farm/commands/attach.d.ts.map +1 -1
- package/dist/agent-farm/commands/attach.js +169 -29
- 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/send.d.ts +22 -2
- package/dist/agent-farm/commands/send.d.ts.map +1 -1
- package/dist/agent-farm/commands/send.js +97 -178
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/spawn-roles.d.ts +3 -9
- package/dist/agent-farm/commands/spawn-roles.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-roles.js +14 -53
- package/dist/agent-farm/commands/spawn-roles.js.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.d.ts +11 -18
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +35 -22
- package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
- package/dist/agent-farm/commands/spawn.d.ts +8 -6
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +207 -89
- 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 +5 -35
- 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 +65 -6
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tower-client.js +57 -2
- package/dist/agent-farm/lib/tower-client.js.map +1 -1
- package/dist/agent-farm/servers/overview.d.ts +157 -0
- package/dist/agent-farm/servers/overview.d.ts.map +1 -0
- package/dist/agent-farm/servers/overview.js +625 -0
- package/dist/agent-farm/servers/overview.js.map +1 -0
- package/dist/agent-farm/servers/tower-instances.d.ts +1 -3
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-instances.js +12 -14
- package/dist/agent-farm/servers/tower-instances.js.map +1 -1
- package/dist/agent-farm/servers/tower-messages.d.ts +87 -0
- package/dist/agent-farm/servers/tower-messages.d.ts.map +1 -0
- package/dist/agent-farm/servers/tower-messages.js +202 -0
- package/dist/agent-farm/servers/tower-messages.js.map +1 -0
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +182 -34
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +30 -6
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts +9 -3
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.js +129 -84
- 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-types.d.ts +0 -2
- package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.js +27 -5
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
- package/dist/agent-farm/types.d.ts +4 -5
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/agent-names.d.ts +85 -0
- package/dist/agent-farm/utils/agent-names.d.ts.map +1 -0
- package/dist/agent-farm/utils/agent-names.js +140 -0
- package/dist/agent-farm/utils/agent-names.js.map +1 -0
- 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/message-format.d.ts +17 -0
- package/dist/agent-farm/utils/message-format.d.ts.map +1 -0
- package/dist/agent-farm/utils/message-format.js +41 -0
- package/dist/agent-farm/utils/message-format.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 +46 -15
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +1 -13
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts +34 -9
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +617 -263
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/consult/metrics.d.ts +90 -0
- package/dist/commands/consult/metrics.d.ts.map +1 -0
- package/dist/commands/consult/metrics.js +203 -0
- package/dist/commands/consult/metrics.js.map +1 -0
- package/dist/commands/consult/stats.d.ts +18 -0
- package/dist/commands/consult/stats.d.ts.map +1 -0
- package/dist/commands/consult/stats.js +150 -0
- package/dist/commands/consult/stats.js.map +1 -0
- package/dist/commands/consult/usage-extractor.d.ts +41 -0
- package/dist/commands/consult/usage-extractor.d.ts.map +1 -0
- package/dist/commands/consult/usage-extractor.js +122 -0
- package/dist/commands/consult/usage-extractor.js.map +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +5 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +1 -13
- package/dist/commands/init.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 +57 -77
- 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 +10 -1
- package/dist/commands/porch/prompts.d.ts.map +1 -1
- package/dist/commands/porch/prompts.js +56 -29
- package/dist/commands/porch/prompts.js.map +1 -1
- package/dist/commands/porch/protocol.js +2 -2
- 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 +49 -2
- package/dist/commands/porch/state.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +0 -10
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/github.d.ts +82 -0
- package/dist/lib/github.d.ts.map +1 -0
- package/dist/lib/github.js +181 -0
- package/dist/lib/github.js.map +1 -0
- package/dist/lib/scaffold.d.ts +0 -21
- package/dist/lib/scaffold.d.ts.map +1 -1
- package/dist/lib/scaffold.js +0 -57
- package/dist/lib/scaffold.js.map +1 -1
- package/dist/terminal/index.d.ts +16 -0
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +14 -0
- package/dist/terminal/index.js.map +1 -1
- package/dist/terminal/pty-manager.d.ts.map +1 -1
- package/dist/terminal/pty-manager.js +8 -5
- package/dist/terminal/pty-manager.js.map +1 -1
- package/dist/terminal/pty-session.js +4 -4
- package/dist/terminal/pty-session.js.map +1 -1
- package/dist/terminal/session-manager.d.ts +64 -0
- package/dist/terminal/session-manager.d.ts.map +1 -1
- package/dist/terminal/session-manager.js +299 -10
- package/dist/terminal/session-manager.js.map +1 -1
- package/dist/terminal/shellper-client.d.ts +2 -1
- package/dist/terminal/shellper-client.d.ts.map +1 -1
- package/dist/terminal/shellper-client.js +4 -2
- package/dist/terminal/shellper-client.js.map +1 -1
- package/dist/terminal/shellper-main.js +33 -4
- package/dist/terminal/shellper-main.js.map +1 -1
- package/dist/terminal/shellper-process.d.ts +24 -7
- package/dist/terminal/shellper-process.d.ts.map +1 -1
- package/dist/terminal/shellper-process.js +139 -36
- package/dist/terminal/shellper-process.js.map +1 -1
- package/dist/terminal/shellper-protocol.d.ts +1 -0
- package/dist/terminal/shellper-protocol.d.ts.map +1 -1
- package/dist/terminal/shellper-protocol.js.map +1 -1
- package/package.json +4 -1
- package/skeleton/.claude/skills/af/SKILL.md +10 -10
- 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/maintain/.gitkeep +1 -1
- package/skeleton/porch/prompts/specify.md +1 -1
- package/skeleton/protocol-schema.json +1 -1
- package/skeleton/protocols/bugfix/prompts/pr.md +18 -7
- package/skeleton/protocols/bugfix/protocol.json +1 -1
- package/skeleton/protocols/experiment/protocol.md +17 -17
- 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/prompts/audit.md +2 -2
- package/skeleton/protocols/maintain/prompts/sync.md +1 -1
- package/skeleton/protocols/maintain/prompts/verify.md +1 -1
- package/skeleton/protocols/maintain/protocol.json +4 -4
- package/skeleton/protocols/maintain/protocol.md +11 -12
- package/skeleton/protocols/maintain/templates/maintenance-run.md +2 -2
- 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 +16 -16
- package/skeleton/protocols/spir/protocol.md +8 -8
- 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/protocols/tick/protocol.md +31 -31
- package/skeleton/resources/commands/agent-farm.md +21 -19
- package/skeleton/resources/commands/codev.md +0 -36
- package/skeleton/resources/commands/consult.md +88 -234
- package/skeleton/resources/commands/overview.md +6 -7
- package/skeleton/resources/spikes.md +3 -3
- package/skeleton/resources/workflow-reference.md +28 -28
- package/skeleton/roles/architect.md +34 -38
- package/skeleton/roles/builder.md +14 -14
- package/skeleton/roles/consultant.md +6 -0
- package/skeleton/templates/AGENTS.md +6 -6
- package/skeleton/templates/CLAUDE.md +6 -6
- package/skeleton/templates/cheatsheet.md +22 -18
- package/skeleton/templates/lifecycle.md +9 -9
- package/skeleton/templates/pr-overview.md +5 -5
- package/templates/open.html +6 -3
- package/templates/tower.html +1 -41
- package/dashboard/dist/assets/index-4n9zpWLY.css +0 -32
- package/dashboard/dist/assets/index-UsH9ixz1.js +0 -136
- package/dashboard/dist/assets/index-UsH9ixz1.js.map +0 -1
- 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/dist/agent-farm/utils/gate-status.d.ts +0 -16
- package/dist/agent-farm/utils/gate-status.d.ts.map +0 -1
- package/dist/agent-farm/utils/gate-status.js +0 -79
- package/dist/agent-farm/utils/gate-status.js.map +0 -1
- package/skeleton/templates/projectlist-archive.md +0 -21
- package/skeleton/templates/projectlist.md +0 -147
- /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';
|
|
@@ -9,15 +12,15 @@ import { spawn, execSync } from 'node:child_process';
|
|
|
9
12
|
import { tmpdir } from 'node:os';
|
|
10
13
|
import chalk from 'chalk';
|
|
11
14
|
import { query as claudeQuery } from '@anthropic-ai/claude-agent-sdk';
|
|
12
|
-
import {
|
|
15
|
+
import { Codex } from '@openai/codex-sdk';
|
|
16
|
+
import { readCodevFile, findWorkspaceRoot } from '../../lib/skeleton.js';
|
|
17
|
+
import { MetricsDB } from './metrics.js';
|
|
18
|
+
import { extractUsage, extractReviewText } from './usage-extractor.js';
|
|
13
19
|
const MODEL_CONFIGS = {
|
|
14
|
-
gemini: { cli: 'gemini', args: [
|
|
15
|
-
// Codex uses experimental_instructions_file config flag (not env var)
|
|
16
|
-
// See: https://github.com/openai/codex/discussions/3896
|
|
17
|
-
codex: { cli: 'codex', args: ['exec', '-m', 'gpt-5.2-codex', '--full-auto'], envVar: null },
|
|
20
|
+
gemini: { cli: 'gemini', args: [], envVar: 'GEMINI_SYSTEM_MD' },
|
|
18
21
|
};
|
|
19
|
-
// Models that use
|
|
20
|
-
const SDK_MODELS = ['claude'];
|
|
22
|
+
// Models that use an Agent SDK instead of CLI subprocess
|
|
23
|
+
const SDK_MODELS = ['claude', 'codex'];
|
|
21
24
|
// Claude Agent SDK turn limit. Claude explores the codebase with Read/Glob/Grep
|
|
22
25
|
// tools before producing its verdict, so it needs a generous turn budget.
|
|
23
26
|
const CLAUDE_MAX_TURNS = 200;
|
|
@@ -27,60 +30,42 @@ const MODEL_ALIASES = {
|
|
|
27
30
|
gpt: 'codex',
|
|
28
31
|
opus: 'claude',
|
|
29
32
|
};
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return !excludePatterns.some(pattern => basename.includes(pattern));
|
|
60
|
-
})
|
|
61
|
-
.map(f => f.replace('.md', ''));
|
|
33
|
+
// Helper to record a metrics entry, opening and closing the DB
|
|
34
|
+
function recordMetrics(ctx, extra) {
|
|
35
|
+
try {
|
|
36
|
+
const db = new MetricsDB();
|
|
37
|
+
try {
|
|
38
|
+
db.record({
|
|
39
|
+
timestamp: ctx.timestamp,
|
|
40
|
+
model: ctx.model,
|
|
41
|
+
reviewType: ctx.reviewType,
|
|
42
|
+
subcommand: ctx.subcommand,
|
|
43
|
+
protocol: ctx.protocol,
|
|
44
|
+
projectId: ctx.projectId,
|
|
45
|
+
durationSeconds: extra.durationSeconds,
|
|
46
|
+
inputTokens: extra.inputTokens,
|
|
47
|
+
cachedInputTokens: extra.cachedInputTokens,
|
|
48
|
+
outputTokens: extra.outputTokens,
|
|
49
|
+
costUsd: extra.costUsd,
|
|
50
|
+
exitCode: extra.exitCode,
|
|
51
|
+
workspacePath: ctx.workspacePath,
|
|
52
|
+
errorMessage: extra.errorMessage,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
db.close();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.error(`[warn] Failed to record metrics: ${err instanceof Error ? err.message : String(err)}`);
|
|
61
|
+
}
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
64
|
+
* Validate name to prevent directory traversal attacks.
|
|
65
|
+
* Only allows alphanumeric, hyphen, and underscore characters.
|
|
66
66
|
*/
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
if (!isValidRoleName(roleName)) {
|
|
70
|
-
throw new Error(`Invalid role name: '${roleName}'\n` +
|
|
71
|
-
'Role names can only contain letters, numbers, hyphens, and underscores.');
|
|
72
|
-
}
|
|
73
|
-
// Use readCodevFile which handles local-first with skeleton fallback
|
|
74
|
-
const rolePath = `roles/${roleName}.md`;
|
|
75
|
-
const roleContent = readCodevFile(rolePath, workspaceRoot);
|
|
76
|
-
if (!roleContent) {
|
|
77
|
-
const available = listAvailableRoles(workspaceRoot);
|
|
78
|
-
const availableStr = available.length > 0
|
|
79
|
-
? `\n\nAvailable roles:\n${available.map(r => ` - ${r}`).join('\n')}`
|
|
80
|
-
: '\n\nNo custom roles found in codev/roles/';
|
|
81
|
-
throw new Error(`Role '${roleName}' not found.${availableStr}`);
|
|
82
|
-
}
|
|
83
|
-
return roleContent;
|
|
67
|
+
function isValidRoleName(name) {
|
|
68
|
+
return /^[a-zA-Z0-9_-]+$/.test(name);
|
|
84
69
|
}
|
|
85
70
|
/**
|
|
86
71
|
* Load the consultant role.
|
|
@@ -96,29 +81,24 @@ function loadRole(workspaceRoot) {
|
|
|
96
81
|
return role;
|
|
97
82
|
}
|
|
98
83
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
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
|
|
102
88
|
*/
|
|
103
|
-
function
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return readCodevFile(fallbackPath, workspaceRoot);
|
|
115
|
-
}
|
|
116
|
-
// 3. Fall back to embedded skeleton consult-types/ (default)
|
|
117
|
-
const skeletonPrompt = readCodevFile(primaryPath, workspaceRoot);
|
|
118
|
-
if (skeletonPrompt) {
|
|
119
|
-
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}`);
|
|
120
100
|
}
|
|
121
|
-
return
|
|
101
|
+
return content;
|
|
122
102
|
}
|
|
123
103
|
/**
|
|
124
104
|
* Load .env file if it exists
|
|
@@ -149,37 +129,83 @@ function loadDotenv(workspaceRoot) {
|
|
|
149
129
|
}
|
|
150
130
|
}
|
|
151
131
|
/**
|
|
152
|
-
* Find a spec file by number
|
|
132
|
+
* Find a spec file by number. Returns null if not found.
|
|
133
|
+
* Errors if multiple matches found.
|
|
153
134
|
*/
|
|
154
135
|
function findSpec(workspaceRoot, number) {
|
|
155
136
|
const specsDir = path.join(workspaceRoot, 'codev', 'specs');
|
|
156
137
|
const pattern = String(number).padStart(4, '0');
|
|
157
138
|
if (fs.existsSync(specsDir)) {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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]);
|
|
163
146
|
}
|
|
164
147
|
}
|
|
165
148
|
return null;
|
|
166
149
|
}
|
|
167
150
|
/**
|
|
168
|
-
* Find a plan file by number
|
|
151
|
+
* Find a plan file by number. Returns null if not found.
|
|
152
|
+
* Errors if multiple matches found.
|
|
169
153
|
*/
|
|
170
154
|
function findPlan(workspaceRoot, number) {
|
|
171
155
|
const plansDir = path.join(workspaceRoot, 'codev', 'plans');
|
|
172
156
|
const pattern = String(number).padStart(4, '0');
|
|
173
157
|
if (fs.existsSync(plansDir)) {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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]);
|
|
179
165
|
}
|
|
180
166
|
}
|
|
181
167
|
return null;
|
|
182
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
|
+
}
|
|
183
209
|
/**
|
|
184
210
|
* Log query to history file
|
|
185
211
|
*/
|
|
@@ -211,13 +237,109 @@ function commandExists(cmd) {
|
|
|
211
237
|
return false;
|
|
212
238
|
}
|
|
213
239
|
}
|
|
240
|
+
// Codex pricing for cost computation (matches values from old SUBPROCESS_MODEL_PRICING)
|
|
241
|
+
const CODEX_PRICING = { inputPer1M: 2.00, cachedInputPer1M: 1.00, outputPer1M: 8.00 };
|
|
242
|
+
/**
|
|
243
|
+
* Run Codex consultation via @openai/codex-sdk.
|
|
244
|
+
* Mirrors runClaudeConsultation() — streams events, captures usage, records metrics.
|
|
245
|
+
*/
|
|
246
|
+
export async function runCodexConsultation(queryText, role, workspaceRoot, outputPath, metricsCtx) {
|
|
247
|
+
const chunks = [];
|
|
248
|
+
const startTime = Date.now();
|
|
249
|
+
let usageData = null;
|
|
250
|
+
let errorMessage = null;
|
|
251
|
+
let exitCode = 0;
|
|
252
|
+
// Write role to temp file — SDK requires file path for instructions
|
|
253
|
+
const tempFile = path.join(tmpdir(), `codev-role-${Date.now()}.md`);
|
|
254
|
+
fs.writeFileSync(tempFile, role);
|
|
255
|
+
try {
|
|
256
|
+
const codex = new Codex({
|
|
257
|
+
config: {
|
|
258
|
+
experimental_instructions_file: tempFile,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
const thread = codex.startThread({
|
|
262
|
+
model: 'gpt-5.2-codex',
|
|
263
|
+
sandboxMode: 'read-only',
|
|
264
|
+
modelReasoningEffort: 'medium',
|
|
265
|
+
workingDirectory: workspaceRoot,
|
|
266
|
+
});
|
|
267
|
+
const { events } = await thread.runStreamed(queryText);
|
|
268
|
+
for await (const event of events) {
|
|
269
|
+
if (event.type === 'item.completed') {
|
|
270
|
+
const item = event.item;
|
|
271
|
+
if (item.type === 'agent_message') {
|
|
272
|
+
process.stdout.write(item.text);
|
|
273
|
+
chunks.push(item.text);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (event.type === 'turn.completed') {
|
|
277
|
+
const input = event.usage.input_tokens;
|
|
278
|
+
const cached = event.usage.cached_input_tokens;
|
|
279
|
+
const output = event.usage.output_tokens;
|
|
280
|
+
const uncached = input - cached;
|
|
281
|
+
const cost = (uncached / 1_000_000) * CODEX_PRICING.inputPer1M
|
|
282
|
+
+ (cached / 1_000_000) * CODEX_PRICING.cachedInputPer1M
|
|
283
|
+
+ (output / 1_000_000) * CODEX_PRICING.outputPer1M;
|
|
284
|
+
usageData = { inputTokens: input, cachedInputTokens: cached, outputTokens: output, costUsd: cost };
|
|
285
|
+
}
|
|
286
|
+
if (event.type === 'turn.failed') {
|
|
287
|
+
errorMessage = event.error.message ?? 'Codex turn failed';
|
|
288
|
+
exitCode = 1;
|
|
289
|
+
throw new Error(errorMessage);
|
|
290
|
+
}
|
|
291
|
+
if (event.type === 'error') {
|
|
292
|
+
errorMessage = event.message ?? 'Codex stream error';
|
|
293
|
+
exitCode = 1;
|
|
294
|
+
throw new Error(errorMessage);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Write output file
|
|
298
|
+
if (outputPath) {
|
|
299
|
+
const outputDir = path.dirname(outputPath);
|
|
300
|
+
if (!fs.existsSync(outputDir))
|
|
301
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
302
|
+
fs.writeFileSync(outputPath, chunks.join(''));
|
|
303
|
+
console.error(`\nOutput written to: ${outputPath}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
if (!errorMessage) {
|
|
308
|
+
errorMessage = (err instanceof Error ? err.message : String(err)).substring(0, 500);
|
|
309
|
+
exitCode = 1;
|
|
310
|
+
}
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
// Clean up temp file
|
|
315
|
+
if (fs.existsSync(tempFile))
|
|
316
|
+
fs.unlinkSync(tempFile);
|
|
317
|
+
// Record metrics (always, even on error)
|
|
318
|
+
if (metricsCtx) {
|
|
319
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
320
|
+
recordMetrics(metricsCtx, {
|
|
321
|
+
durationSeconds: duration,
|
|
322
|
+
inputTokens: usageData?.inputTokens ?? null,
|
|
323
|
+
cachedInputTokens: usageData?.cachedInputTokens ?? null,
|
|
324
|
+
outputTokens: usageData?.outputTokens ?? null,
|
|
325
|
+
costUsd: usageData?.costUsd ?? null,
|
|
326
|
+
exitCode,
|
|
327
|
+
errorMessage,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
214
332
|
/**
|
|
215
333
|
* Run Claude consultation via Agent SDK.
|
|
216
334
|
* Uses the SDK's query() function instead of CLI subprocess.
|
|
217
335
|
* This avoids the CLAUDECODE nesting guard and enables tool use during reviews.
|
|
218
336
|
*/
|
|
219
|
-
async function runClaudeConsultation(queryText, role, workspaceRoot, outputPath) {
|
|
337
|
+
async function runClaudeConsultation(queryText, role, workspaceRoot, outputPath, metricsCtx) {
|
|
220
338
|
const chunks = [];
|
|
339
|
+
const startTime = Date.now();
|
|
340
|
+
let sdkResult;
|
|
341
|
+
let errorMessage = null;
|
|
342
|
+
let exitCode = 0;
|
|
221
343
|
// The SDK spawns a Claude Code subprocess that checks process.env.CLAUDECODE.
|
|
222
344
|
// We must remove it from process.env (not just the options env) to avoid
|
|
223
345
|
// the nesting guard. Restore it after the SDK call.
|
|
@@ -254,9 +376,14 @@ async function runClaudeConsultation(queryText, role, workspaceRoot, outputPath)
|
|
|
254
376
|
}
|
|
255
377
|
}
|
|
256
378
|
if (message.type === 'result') {
|
|
257
|
-
if (message.subtype
|
|
379
|
+
if (message.subtype === 'success') {
|
|
380
|
+
sdkResult = message;
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
258
383
|
const errors = 'errors' in message ? message.errors : [];
|
|
259
|
-
|
|
384
|
+
errorMessage = `Claude SDK error (${message.subtype}): ${errors.join(', ')}`.substring(0, 500);
|
|
385
|
+
exitCode = 1;
|
|
386
|
+
throw new Error(errorMessage);
|
|
260
387
|
}
|
|
261
388
|
}
|
|
262
389
|
}
|
|
@@ -268,43 +395,49 @@ async function runClaudeConsultation(queryText, role, workspaceRoot, outputPath)
|
|
|
268
395
|
console.error(`\nOutput written to: ${outputPath}`);
|
|
269
396
|
}
|
|
270
397
|
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
if (!errorMessage) {
|
|
400
|
+
errorMessage = (err instanceof Error ? err.message : String(err)).substring(0, 500);
|
|
401
|
+
exitCode = 1;
|
|
402
|
+
}
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
271
405
|
finally {
|
|
272
406
|
if (savedClaudeCode !== undefined) {
|
|
273
407
|
process.env.CLAUDECODE = savedClaudeCode;
|
|
274
408
|
}
|
|
409
|
+
// Record metrics (always, even on error)
|
|
410
|
+
if (metricsCtx) {
|
|
411
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
412
|
+
const usage = sdkResult ? extractUsage('claude', '', sdkResult) : null;
|
|
413
|
+
recordMetrics(metricsCtx, {
|
|
414
|
+
durationSeconds: duration,
|
|
415
|
+
inputTokens: usage?.inputTokens ?? null,
|
|
416
|
+
cachedInputTokens: usage?.cachedInputTokens ?? null,
|
|
417
|
+
outputTokens: usage?.outputTokens ?? null,
|
|
418
|
+
costUsd: usage?.costUsd ?? null,
|
|
419
|
+
exitCode,
|
|
420
|
+
errorMessage,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
275
423
|
}
|
|
276
424
|
}
|
|
277
425
|
/**
|
|
278
|
-
* Run the consultation
|
|
426
|
+
* Run the consultation — dispatches to the correct model runner.
|
|
279
427
|
*/
|
|
280
|
-
async function runConsultation(model, query, workspaceRoot,
|
|
281
|
-
//
|
|
282
|
-
let role = customRole ? loadCustomRole(workspaceRoot, customRole) : loadRole(workspaceRoot);
|
|
283
|
-
// Append review type prompt if specified
|
|
284
|
-
if (reviewType) {
|
|
285
|
-
const typePrompt = loadReviewTypePrompt(workspaceRoot, reviewType);
|
|
286
|
-
if (typePrompt) {
|
|
287
|
-
role = role + '\n\n---\n\n' + typePrompt;
|
|
288
|
-
console.error(`Review type: ${reviewType}`);
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
console.error(chalk.yellow(`Warning: Review type prompt not found: ${reviewType}`));
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
// Claude uses the Agent SDK — handle separately from CLI-based models
|
|
428
|
+
async function runConsultation(model, query, workspaceRoot, role, outputPath, metricsCtx, generalMode) {
|
|
429
|
+
// SDK-based models
|
|
295
430
|
if (model === 'claude') {
|
|
296
|
-
if (dryRun) {
|
|
297
|
-
console.log(chalk.yellow(`[claude] Would invoke Agent SDK:`));
|
|
298
|
-
console.log(` Model: claude-opus-4-6`);
|
|
299
|
-
console.log(` Tools: Read, Glob, Grep`);
|
|
300
|
-
console.log(` Max turns: ${CLAUDE_MAX_TURNS}`);
|
|
301
|
-
console.log(` Max budget: $25.00`);
|
|
302
|
-
const promptPreview = query.substring(0, 200) + (query.length > 200 ? '...' : '');
|
|
303
|
-
console.log(` Prompt: ${promptPreview}`);
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
431
|
const startTime = Date.now();
|
|
307
|
-
await runClaudeConsultation(query, role, workspaceRoot, outputPath);
|
|
432
|
+
await runClaudeConsultation(query, role, workspaceRoot, outputPath, metricsCtx);
|
|
433
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
434
|
+
logQuery(workspaceRoot, model, query, duration);
|
|
435
|
+
console.error(`\n[${model} completed in ${duration.toFixed(1)}s]`);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (model === 'codex') {
|
|
439
|
+
const startTime = Date.now();
|
|
440
|
+
await runCodexConsultation(query, role, workspaceRoot, outputPath, metricsCtx);
|
|
308
441
|
const duration = (Date.now() - startTime) / 1000;
|
|
309
442
|
logQuery(workspaceRoot, model, query, duration);
|
|
310
443
|
console.error(`\n[${model} completed in ${duration.toFixed(1)}s]`);
|
|
@@ -314,74 +447,41 @@ async function runConsultation(model, query, workspaceRoot, dryRun, reviewType,
|
|
|
314
447
|
if (!config) {
|
|
315
448
|
throw new Error(`Unknown model: ${model}`);
|
|
316
449
|
}
|
|
317
|
-
// Check if CLI exists
|
|
318
|
-
if (!
|
|
450
|
+
// Check if CLI exists
|
|
451
|
+
if (!commandExists(config.cli)) {
|
|
319
452
|
throw new Error(`${config.cli} not found. Please install it first.`);
|
|
320
453
|
}
|
|
321
454
|
let tempFile = null;
|
|
322
455
|
const env = {};
|
|
323
|
-
// Prepare command and environment based on model
|
|
324
456
|
let cmd;
|
|
325
457
|
if (model === 'gemini') {
|
|
326
458
|
// Gemini uses GEMINI_SYSTEM_MD env var for role
|
|
327
459
|
tempFile = path.join(tmpdir(), `codev-role-${Date.now()}.md`);
|
|
328
460
|
fs.writeFileSync(tempFile, role);
|
|
329
461
|
env['GEMINI_SYSTEM_MD'] = tempFile;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
tempFile = path.join(tmpdir(), `codev-role-${Date.now()}.md`);
|
|
336
|
-
fs.writeFileSync(tempFile, role);
|
|
337
|
-
cmd = [
|
|
338
|
-
config.cli,
|
|
339
|
-
'exec',
|
|
340
|
-
'-c', `experimental_instructions_file=${tempFile}`,
|
|
341
|
-
'-c', 'model_reasoning_effort=medium', // Balance speed vs review quality
|
|
342
|
-
'-c', 'sandbox=read-only', // Consult is read-only — no test execution
|
|
343
|
-
'--full-auto',
|
|
344
|
-
query,
|
|
345
|
-
];
|
|
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];
|
|
346
467
|
}
|
|
347
468
|
else {
|
|
348
469
|
throw new Error(`Unknown model: ${model}`);
|
|
349
470
|
}
|
|
350
|
-
if (dryRun) {
|
|
351
|
-
console.log(chalk.yellow(`[${model}] Would execute:`));
|
|
352
|
-
console.log(` Command: ${cmd.join(' ')}`);
|
|
353
|
-
if (Object.keys(env).length > 0) {
|
|
354
|
-
for (const [key, value] of Object.entries(env)) {
|
|
355
|
-
if (key === 'GEMINI_SYSTEM_MD') {
|
|
356
|
-
console.log(` Env: ${key}=<temp file with consultant role>`);
|
|
357
|
-
}
|
|
358
|
-
else {
|
|
359
|
-
const preview = value.substring(0, 50) + (value.length > 50 ? '...' : '');
|
|
360
|
-
console.log(` Env: ${key}=${preview}`);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
if (tempFile)
|
|
365
|
-
fs.unlinkSync(tempFile);
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
471
|
// Execute with passthrough stdio
|
|
369
472
|
// Use 'ignore' for stdin to prevent blocking when spawned as subprocess
|
|
370
|
-
// When outputPath is set, capture stdout to write to file (used by porch)
|
|
371
473
|
const fullEnv = { ...process.env, ...env };
|
|
372
474
|
const startTime = Date.now();
|
|
373
|
-
const stdoutMode = outputPath ? 'pipe' : 'inherit';
|
|
374
475
|
return new Promise((resolve, reject) => {
|
|
375
476
|
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
477
|
+
cwd: workspaceRoot,
|
|
376
478
|
env: fullEnv,
|
|
377
|
-
stdio: ['ignore',
|
|
479
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
378
480
|
});
|
|
379
481
|
const chunks = [];
|
|
380
|
-
if (
|
|
482
|
+
if (proc.stdout) {
|
|
381
483
|
proc.stdout.on('data', (chunk) => {
|
|
382
484
|
chunks.push(chunk);
|
|
383
|
-
// Also write to stdout so the user can still see output
|
|
384
|
-
process.stdout.write(chunk);
|
|
385
485
|
});
|
|
386
486
|
}
|
|
387
487
|
proc.on('close', (code) => {
|
|
@@ -390,15 +490,34 @@ async function runConsultation(model, query, workspaceRoot, dryRun, reviewType,
|
|
|
390
490
|
if (tempFile && fs.existsSync(tempFile)) {
|
|
391
491
|
fs.unlinkSync(tempFile);
|
|
392
492
|
}
|
|
393
|
-
|
|
394
|
-
|
|
493
|
+
const rawOutput = Buffer.concat(chunks).toString('utf-8');
|
|
494
|
+
// Extract review text from structured output (JSON/JSONL → plain text)
|
|
495
|
+
const reviewText = extractReviewText(model, rawOutput);
|
|
496
|
+
const outputContent = reviewText ?? rawOutput; // Fallback to raw on parse failure
|
|
497
|
+
// Write text to stdout (was fully buffered)
|
|
498
|
+
process.stdout.write(outputContent);
|
|
499
|
+
// Write to output file
|
|
500
|
+
if (outputPath && outputContent.length > 0) {
|
|
395
501
|
const outputDir = path.dirname(outputPath);
|
|
396
502
|
if (!fs.existsSync(outputDir)) {
|
|
397
503
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
398
504
|
}
|
|
399
|
-
fs.writeFileSync(outputPath,
|
|
505
|
+
fs.writeFileSync(outputPath, outputContent);
|
|
400
506
|
console.error(`\nOutput written to: ${outputPath}`);
|
|
401
507
|
}
|
|
508
|
+
// Record metrics
|
|
509
|
+
if (metricsCtx) {
|
|
510
|
+
const usage = extractUsage(model, rawOutput);
|
|
511
|
+
recordMetrics(metricsCtx, {
|
|
512
|
+
durationSeconds: duration,
|
|
513
|
+
inputTokens: usage?.inputTokens ?? null,
|
|
514
|
+
cachedInputTokens: usage?.cachedInputTokens ?? null,
|
|
515
|
+
outputTokens: usage?.outputTokens ?? null,
|
|
516
|
+
costUsd: usage?.costUsd ?? null,
|
|
517
|
+
exitCode: code ?? 1,
|
|
518
|
+
errorMessage: code !== 0 ? `Process exited with code ${code}` : null,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
402
521
|
console.error(`\n[${model} completed in ${duration.toFixed(1)}s]`);
|
|
403
522
|
if (code !== 0) {
|
|
404
523
|
reject(new Error(`Process exited with code ${code}`));
|
|
@@ -411,13 +530,25 @@ async function runConsultation(model, query, workspaceRoot, dryRun, reviewType,
|
|
|
411
530
|
if (tempFile && fs.existsSync(tempFile)) {
|
|
412
531
|
fs.unlinkSync(tempFile);
|
|
413
532
|
}
|
|
533
|
+
// Record metrics for spawn failures
|
|
534
|
+
if (metricsCtx) {
|
|
535
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
536
|
+
recordMetrics(metricsCtx, {
|
|
537
|
+
durationSeconds: duration,
|
|
538
|
+
inputTokens: null,
|
|
539
|
+
cachedInputTokens: null,
|
|
540
|
+
outputTokens: null,
|
|
541
|
+
costUsd: null,
|
|
542
|
+
exitCode: 1,
|
|
543
|
+
errorMessage: (error.message || String(error)).substring(0, 500),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
414
546
|
reject(error);
|
|
415
547
|
});
|
|
416
548
|
});
|
|
417
549
|
}
|
|
418
550
|
/**
|
|
419
551
|
* Get a compact diff stat summary and list of changed files.
|
|
420
|
-
* Returns { stat, files } where stat is the `--stat` output and files is the list of paths.
|
|
421
552
|
*/
|
|
422
553
|
function getDiffStat(workspaceRoot, ref) {
|
|
423
554
|
const stat = execSync(`git diff --stat ${ref}`, { cwd: workspaceRoot, encoding: 'utf-8' }).toString();
|
|
@@ -426,7 +557,7 @@ function getDiffStat(workspaceRoot, ref) {
|
|
|
426
557
|
return { stat, files };
|
|
427
558
|
}
|
|
428
559
|
/**
|
|
429
|
-
* Fetch PR metadata (no diff —
|
|
560
|
+
* Fetch PR metadata (no diff — that's fetched separately)
|
|
430
561
|
*/
|
|
431
562
|
function fetchPRData(prNumber) {
|
|
432
563
|
console.error(`Fetching PR #${prNumber} data...`);
|
|
@@ -447,12 +578,24 @@ function fetchPRData(prNumber) {
|
|
|
447
578
|
throw new Error(`Failed to fetch PR data: ${err}`);
|
|
448
579
|
}
|
|
449
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
|
+
}
|
|
450
592
|
/**
|
|
451
593
|
* Build query for PR review.
|
|
452
|
-
*
|
|
594
|
+
* Includes full PR diff + file list; model reads surrounding context from disk.
|
|
453
595
|
*/
|
|
454
|
-
function buildPRQuery(prNumber
|
|
596
|
+
function buildPRQuery(prNumber) {
|
|
455
597
|
const prData = fetchPRData(prNumber);
|
|
598
|
+
const diff = fetchPRDiff(prNumber);
|
|
456
599
|
const fileList = prData.changedFiles.map(f => `- ${f}`).join('\n');
|
|
457
600
|
return `Review Pull Request #${prNumber}
|
|
458
601
|
|
|
@@ -464,10 +607,13 @@ ${prData.info}
|
|
|
464
607
|
## Changed Files
|
|
465
608
|
${fileList}
|
|
466
609
|
|
|
610
|
+
## PR Diff
|
|
611
|
+
\`\`\`diff
|
|
612
|
+
${diff}
|
|
613
|
+
\`\`\`
|
|
614
|
+
|
|
467
615
|
## How to Review
|
|
468
|
-
|
|
469
|
-
For each changed file listed above, read it and evaluate the code quality, correctness, and test coverage.
|
|
470
|
-
Use \`git diff HEAD~1 -- <file>\` or \`git log -p -- <file>\` if you need to see what specifically changed.
|
|
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.
|
|
471
617
|
|
|
472
618
|
## Comments
|
|
473
619
|
${prData.comments}
|
|
@@ -505,6 +651,11 @@ Please read and review this specification:
|
|
|
505
651
|
query += `- Plan file: ${planPath}\n`;
|
|
506
652
|
}
|
|
507
653
|
query += `
|
|
654
|
+
## How to Review
|
|
655
|
+
**Read the files listed above directly from disk.** You have full filesystem access.
|
|
656
|
+
Do NOT rely on \`git diff\` or \`git log\` to review content — diffs may be truncated or miss uncommitted work.
|
|
657
|
+
Open the spec file, read it in full, and evaluate it directly.
|
|
658
|
+
|
|
508
659
|
Please review:
|
|
509
660
|
1. Clarity and completeness of requirements
|
|
510
661
|
2. Technical feasibility
|
|
@@ -526,24 +677,22 @@ KEY_ISSUES: [List of critical issues if any, or "None"]`;
|
|
|
526
677
|
}
|
|
527
678
|
/**
|
|
528
679
|
* Build query for implementation review.
|
|
529
|
-
*
|
|
680
|
+
* Accepts spec/plan paths and optional diff reference override.
|
|
530
681
|
*/
|
|
531
|
-
function buildImplQuery(
|
|
532
|
-
|
|
533
|
-
const planPath = findPlan(workspaceRoot, projectNumber);
|
|
534
|
-
// Get compact diff summary against base branch
|
|
682
|
+
function buildImplQuery(workspaceRoot, specPath, planPath, planPhase, diffRef) {
|
|
683
|
+
// Get compact diff summary
|
|
535
684
|
let diffStat = '';
|
|
536
685
|
let changedFiles = [];
|
|
537
686
|
try {
|
|
538
|
-
const
|
|
539
|
-
const result = getDiffStat(workspaceRoot,
|
|
687
|
+
const ref = diffRef ?? execSync('git merge-base HEAD main', { cwd: workspaceRoot, encoding: 'utf-8' }).trim();
|
|
688
|
+
const result = getDiffStat(workspaceRoot, ref);
|
|
540
689
|
diffStat = result.stat;
|
|
541
690
|
changedFiles = result.files;
|
|
542
691
|
}
|
|
543
692
|
catch {
|
|
544
693
|
// If git diff fails, reviewer will explore filesystem
|
|
545
694
|
}
|
|
546
|
-
let query = `Review Implementation
|
|
695
|
+
let query = `Review Implementation`;
|
|
547
696
|
if (planPhase) {
|
|
548
697
|
query += ` — Phase: ${planPhase}`;
|
|
549
698
|
}
|
|
@@ -570,7 +719,7 @@ function buildImplQuery(projectNumber, workspaceRoot, planPhase) {
|
|
|
570
719
|
query += `\n\n## How to Review\n`;
|
|
571
720
|
query += `**Read the changed files from disk** to review their actual content. You have full filesystem access.\n`;
|
|
572
721
|
query += `For each file listed above, read it and evaluate the implementation against the spec/plan.\n`;
|
|
573
|
-
query += `
|
|
722
|
+
query += `Do NOT rely on git diffs to determine the current state of code — diffs miss uncommitted changes in worktrees.\n`;
|
|
574
723
|
}
|
|
575
724
|
else {
|
|
576
725
|
query += `\n## Instructions\n\nRead the spec and plan files above, then explore the filesystem to find and review the implementation changes.\n`;
|
|
@@ -607,6 +756,11 @@ Please read and review this implementation plan:
|
|
|
607
756
|
query += `- Spec file: ${specPath} (for context)\n`;
|
|
608
757
|
}
|
|
609
758
|
query += `
|
|
759
|
+
## How to Review
|
|
760
|
+
**Read the files listed above directly from disk.** You have full filesystem access.
|
|
761
|
+
Do NOT rely on \`git diff\` or \`git log\` to review content — diffs may be truncated or miss uncommitted work.
|
|
762
|
+
Open the plan file (and spec if provided), read them in full, and evaluate the plan directly.
|
|
763
|
+
|
|
610
764
|
Please review:
|
|
611
765
|
1. Alignment with specification requirements
|
|
612
766
|
2. Implementation approach and architecture
|
|
@@ -627,109 +781,304 @@ KEY_ISSUES: [List of critical issues if any, or "None"]`;
|
|
|
627
781
|
return query;
|
|
628
782
|
}
|
|
629
783
|
/**
|
|
630
|
-
*
|
|
784
|
+
* Build query for phase-scoped review.
|
|
785
|
+
* Uses git show HEAD for the phase's atomic commit diff.
|
|
631
786
|
*/
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
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 });
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
// If git show fails, reviewer explores filesystem
|
|
640
794
|
}
|
|
641
|
-
|
|
642
|
-
if (
|
|
643
|
-
|
|
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}`);
|
|
644
843
|
}
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
console.error(`Model: ${model}`);
|
|
649
|
-
// Log custom role if specified
|
|
650
|
-
if (customRole) {
|
|
651
|
-
console.error(`Role: ${customRole}`);
|
|
844
|
+
const prNumber = parseInt(prJson, 10);
|
|
845
|
+
if (isNaN(prNumber)) {
|
|
846
|
+
throw new Error(`No PR found for branch: ${branchName}`);
|
|
652
847
|
}
|
|
653
|
-
|
|
654
|
-
|
|
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
|
+
}
|
|
655
899
|
case 'pr': {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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.');
|
|
662
908
|
}
|
|
663
|
-
|
|
664
|
-
|
|
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);
|
|
665
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) {
|
|
666
943
|
case 'spec': {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const
|
|
671
|
-
if (isNaN(specNumber)) {
|
|
672
|
-
throw new Error(`Invalid spec number: ${args[0]}`);
|
|
673
|
-
}
|
|
674
|
-
const specPath = findSpec(workspaceRoot, specNumber);
|
|
675
|
-
if (!specPath) {
|
|
676
|
-
throw new Error(`Spec ${specNumber} not found`);
|
|
677
|
-
}
|
|
678
|
-
const planPath = findPlan(workspaceRoot, specNumber);
|
|
679
|
-
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);
|
|
680
948
|
console.error(`Spec: ${specPath}`);
|
|
681
949
|
if (planPath)
|
|
682
950
|
console.error(`Plan: ${planPath}`);
|
|
683
|
-
|
|
951
|
+
return buildSpecQuery(specPath, planPath);
|
|
684
952
|
}
|
|
685
953
|
case 'plan': {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const
|
|
690
|
-
if (isNaN(planNumber)) {
|
|
691
|
-
throw new Error(`Invalid plan number: ${args[0]}`);
|
|
692
|
-
}
|
|
693
|
-
const planPath = findPlan(workspaceRoot, planNumber);
|
|
694
|
-
if (!planPath) {
|
|
695
|
-
throw new Error(`Plan ${planNumber} not found`);
|
|
696
|
-
}
|
|
697
|
-
const specPath = findSpec(workspaceRoot, planNumber);
|
|
698
|
-
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);
|
|
699
958
|
console.error(`Plan: ${planPath}`);
|
|
700
959
|
if (specPath)
|
|
701
960
|
console.error(`Spec: ${specPath}`);
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
case 'general': {
|
|
705
|
-
if (args.length === 0) {
|
|
706
|
-
throw new Error('Query required\nUsage: consult -m <model> general "<query>"');
|
|
707
|
-
}
|
|
708
|
-
query = args.join(' ');
|
|
709
|
-
break;
|
|
961
|
+
return buildPlanQuery(planPath, specPath);
|
|
710
962
|
}
|
|
711
963
|
case 'impl': {
|
|
712
|
-
|
|
713
|
-
|
|
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' });
|
|
714
968
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
throw new Error(`Invalid project number: ${args[0]}`);
|
|
969
|
+
catch {
|
|
970
|
+
// May already be fetched
|
|
718
971
|
}
|
|
719
|
-
const
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
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})`);
|
|
723
976
|
if (specPath)
|
|
724
977
|
console.error(`Spec: ${specPath}`);
|
|
725
978
|
if (planPath)
|
|
726
979
|
console.error(`Plan: ${planPath}`);
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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);
|
|
730
991
|
}
|
|
731
992
|
default:
|
|
732
|
-
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
|
+
}
|
|
733
1082
|
}
|
|
734
1083
|
// Prepend iteration context if provided (for stateful reviews)
|
|
735
1084
|
if (options.context) {
|
|
@@ -742,6 +1091,10 @@ export async function consult(options) {
|
|
|
742
1091
|
console.error(chalk.yellow(`Warning: Could not read context file: ${options.context}`));
|
|
743
1092
|
}
|
|
744
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
|
+
}
|
|
745
1098
|
// Show the query/prompt being sent
|
|
746
1099
|
console.error('');
|
|
747
1100
|
console.error('='.repeat(60));
|
|
@@ -753,8 +1106,9 @@ export async function consult(options) {
|
|
|
753
1106
|
console.error(`[${model.toUpperCase()}] Starting consultation...`);
|
|
754
1107
|
console.error('='.repeat(60));
|
|
755
1108
|
console.error('');
|
|
756
|
-
|
|
1109
|
+
const isGeneralMode = !hasType;
|
|
1110
|
+
await runConsultation(model, query, workspaceRoot, role, options.output, metricsCtx, isGeneralMode);
|
|
757
1111
|
}
|
|
758
1112
|
// Exported for testing
|
|
759
|
-
export { getDiffStat as _getDiffStat };
|
|
1113
|
+
export { getDiffStat as _getDiffStat, buildSpecQuery as _buildSpecQuery, buildPlanQuery as _buildPlanQuery };
|
|
760
1114
|
//# sourceMappingURL=index.js.map
|