@cluesmith/codev 2.0.2 → 2.0.6
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 +135 -0
- package/dashboard/dist/assets/index-B-s8BA2l.js.map +1 -0
- package/dashboard/dist/assets/index-DB2AxRP7.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 +32 -14
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/architect.d.ts +1 -1
- package/dist/agent-farm/commands/architect.js +3 -3
- 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 +172 -12
- package/dist/agent-farm/commands/attach.js.map +1 -1
- package/dist/agent-farm/commands/cleanup.js +6 -6
- package/dist/agent-farm/commands/cleanup.js.map +1 -1
- package/dist/agent-farm/commands/open.js +5 -5
- package/dist/agent-farm/commands/open.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 +100 -181
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/shell.js +5 -5
- package/dist/agent-farm/commands/shell.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 -17
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +32 -13
- 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 +183 -69
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts +4 -4
- package/dist/agent-farm/commands/start.js +16 -16
- package/dist/agent-farm/commands/start.js.map +1 -1
- package/dist/agent-farm/commands/status.d.ts +1 -1
- package/dist/agent-farm/commands/status.d.ts.map +1 -1
- package/dist/agent-farm/commands/status.js +15 -26
- package/dist/agent-farm/commands/status.js.map +1 -1
- package/dist/agent-farm/commands/stop.d.ts +4 -4
- package/dist/agent-farm/commands/stop.js +9 -9
- package/dist/agent-farm/commands/stop.js.map +1 -1
- package/dist/agent-farm/db/index.d.ts.map +1 -1
- package/dist/agent-farm/db/index.js +82 -7
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/db/schema.d.ts +2 -2
- package/dist/agent-farm/db/schema.d.ts.map +1 -1
- package/dist/agent-farm/db/schema.js +21 -4
- package/dist/agent-farm/db/schema.js.map +1 -1
- package/dist/agent-farm/lib/tower-client.d.ts +36 -26
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tower-client.js +50 -25
- package/dist/agent-farm/lib/tower-client.js.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.d.ts +12 -2
- package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.js +59 -1
- package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
- package/dist/agent-farm/servers/overview.d.ts +111 -0
- package/dist/agent-farm/servers/overview.d.ts.map +1 -0
- package/dist/agent-farm/servers/overview.js +385 -0
- package/dist/agent-farm/servers/overview.js.map +1 -0
- package/dist/agent-farm/servers/tower-instances.d.ts +18 -20
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-instances.js +97 -100
- 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 +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +343 -174
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +50 -21
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts +35 -31
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.js +208 -184
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.d.ts +2 -2
- package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.js +12 -12
- package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
- package/dist/agent-farm/servers/tower-types.d.ts +8 -12
- package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-utils.d.ts +9 -9
- package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-utils.js +18 -18
- package/dist/agent-farm/servers/tower-utils.js.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.d.ts +2 -2
- package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.js +39 -18
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
- package/dist/agent-farm/types.d.ts +5 -6
- 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/config.d.ts +1 -1
- package/dist/agent-farm/utils/config.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.js +16 -16
- package/dist/agent-farm/utils/config.js.map +1 -1
- package/dist/agent-farm/utils/file-tabs.d.ts +3 -3
- package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -1
- package/dist/agent-farm/utils/file-tabs.js +9 -9
- package/dist/agent-farm/utils/file-tabs.js.map +1 -1
- package/dist/agent-farm/utils/index.d.ts +0 -1
- package/dist/agent-farm/utils/index.d.ts.map +1 -1
- package/dist/agent-farm/utils/index.js +0 -1
- package/dist/agent-farm/utils/index.js.map +1 -1
- 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 +4 -4
- package/dist/agent-farm/utils/notifications.d.ts.map +1 -1
- package/dist/agent-farm/utils/notifications.js +18 -18
- package/dist/agent-farm/utils/notifications.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts +2 -2
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +13 -15
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts +26 -2
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +296 -83
- 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 +38 -0
- package/dist/commands/consult/usage-extractor.d.ts.map +1 -0
- package/dist/commands/consult/usage-extractor.js +99 -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 +11 -9
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/import.js +4 -4
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +13 -15
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/porch/index.d.ts +6 -6
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +37 -37
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/next.d.ts +1 -1
- package/dist/commands/porch/next.d.ts.map +1 -1
- package/dist/commands/porch/next.js +86 -92
- package/dist/commands/porch/next.js.map +1 -1
- package/dist/commands/porch/notify.d.ts +11 -0
- package/dist/commands/porch/notify.d.ts.map +1 -0
- package/dist/commands/porch/notify.js +30 -0
- package/dist/commands/porch/notify.js.map +1 -0
- package/dist/commands/porch/plan.d.ts +1 -1
- package/dist/commands/porch/plan.d.ts.map +1 -1
- package/dist/commands/porch/plan.js +3 -3
- 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 +59 -35
- package/dist/commands/porch/prompts.js.map +1 -1
- package/dist/commands/porch/protocol.d.ts +1 -1
- package/dist/commands/porch/protocol.d.ts.map +1 -1
- package/dist/commands/porch/protocol.js +8 -8
- package/dist/commands/porch/protocol.js.map +1 -1
- package/dist/commands/porch/state.d.ts +6 -6
- package/dist/commands/porch/state.d.ts.map +1 -1
- package/dist/commands/porch/state.js +14 -12
- package/dist/commands/porch/state.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +10 -11
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/github.d.ts +81 -0
- package/dist/lib/github.d.ts.map +1 -0
- package/dist/lib/github.js +141 -0
- package/dist/lib/github.js.map +1 -0
- package/dist/lib/scaffold.d.ts +13 -21
- package/dist/lib/scaffold.d.ts.map +1 -1
- package/dist/lib/scaffold.js +34 -57
- package/dist/lib/scaffold.js.map +1 -1
- package/dist/lib/skeleton.d.ts +7 -7
- package/dist/lib/skeleton.d.ts.map +1 -1
- package/dist/lib/skeleton.js +10 -10
- package/dist/lib/skeleton.js.map +1 -1
- package/dist/terminal/index.d.ts +14 -0
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +12 -0
- package/dist/terminal/index.js.map +1 -1
- package/dist/terminal/pty-manager.d.ts +1 -1
- package/dist/terminal/pty-manager.d.ts.map +1 -1
- package/dist/terminal/pty-manager.js +10 -7
- package/dist/terminal/pty-manager.js.map +1 -1
- package/dist/terminal/pty-session.js +3 -3
- 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 +7 -7
- package/skeleton/.claude/skills/consult/SKILL.md +1 -1
- package/skeleton/builders.md +2 -2
- package/skeleton/maintain/.gitkeep +1 -1
- package/skeleton/porch/prompts/specify.md +1 -1
- package/skeleton/protocols/bugfix/prompts/pr.md +15 -4
- package/skeleton/protocols/experiment/protocol.md +17 -17
- 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.md +8 -9
- package/skeleton/protocols/maintain/templates/maintenance-run.md +2 -2
- package/skeleton/protocols/spir/protocol.json +5 -5
- package/skeleton/protocols/spir/protocol.md +8 -8
- package/skeleton/protocols/tick/protocol.md +31 -31
- package/skeleton/resources/commands/agent-farm.md +14 -14
- package/skeleton/resources/commands/codev.md +0 -1
- package/skeleton/resources/commands/consult.md +3 -3
- package/skeleton/resources/spikes.md +3 -3
- package/skeleton/resources/workflow-reference.md +14 -14
- package/skeleton/roles/architect.md +25 -25
- package/skeleton/roles/builder.md +1 -1
- package/skeleton/roles/consultant.md +6 -0
- package/skeleton/templates/AGENTS.md +5 -5
- package/skeleton/templates/CLAUDE.md +5 -5
- package/skeleton/templates/lifecycle.md +9 -9
- package/templates/open.html +19 -16
- package/templates/tower.html +54 -94
- package/templates/vendor/marked.min.js +6 -0
- package/templates/vendor/prism-bash.min.js +1 -0
- package/templates/vendor/prism-css.min.js +1 -0
- package/templates/vendor/prism-javascript.min.js +1 -0
- package/templates/vendor/prism-json.min.js +1 -0
- package/templates/vendor/prism-markdown.min.js +1 -0
- package/templates/vendor/prism-markup.min.js +1 -0
- package/templates/vendor/prism-python.min.js +1 -0
- package/templates/vendor/prism-tomorrow.min.css +1 -0
- package/templates/vendor/prism-typescript.min.js +1 -0
- package/templates/vendor/prism-yaml.min.js +1 -0
- package/templates/vendor/prism.min.js +1 -0
- package/templates/vendor/purify.min.js +3 -0
- package/dashboard/dist/assets/index-4n9zpWLY.css +0 -32
- package/dashboard/dist/assets/index-b38SaXk5.js +0 -136
- package/dashboard/dist/assets/index-b38SaXk5.js.map +0 -1
- package/dist/agent-farm/hq-connector.d.ts +0 -19
- package/dist/agent-farm/hq-connector.d.ts.map +0 -1
- package/dist/agent-farm/hq-connector.js +0 -351
- package/dist/agent-farm/hq-connector.js.map +0 -1
- package/dist/agent-farm/utils/deps.d.ts +0 -51
- package/dist/agent-farm/utils/deps.d.ts.map +0 -1
- package/dist/agent-farm/utils/deps.js +0 -162
- package/dist/agent-farm/utils/deps.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/dist/agent-farm/utils/gate-watcher.d.ts +0 -38
- package/dist/agent-farm/utils/gate-watcher.d.ts.map +0 -1
- package/dist/agent-farm/utils/gate-watcher.js +0 -122
- package/dist/agent-farm/utils/gate-watcher.js.map +0 -1
- package/dist/agent-farm/utils/session.d.ts +0 -32
- package/dist/agent-farm/utils/session.d.ts.map +0 -1
- package/dist/agent-farm/utils/session.js +0 -57
- package/dist/agent-farm/utils/session.js.map +0 -1
- package/dist/lib/projectlist-parser.d.ts +0 -70
- package/dist/lib/projectlist-parser.d.ts.map +0 -1
- package/dist/lib/projectlist-parser.js +0 -200
- package/dist/lib/projectlist-parser.js.map +0 -1
- package/skeleton/templates/projectlist-archive.md +0 -21
- package/skeleton/templates/projectlist.md +0 -147
- package/templates/dashboard/css/dialogs.css +0 -149
- package/templates/dashboard/css/files.css +0 -558
- package/templates/dashboard/css/layout.css +0 -133
- package/templates/dashboard/css/projects.css +0 -501
- package/templates/dashboard/css/statusbar.css +0 -23
- package/templates/dashboard/css/tabs.css +0 -314
- package/templates/dashboard/css/utilities.css +0 -50
- package/templates/dashboard/css/variables.css +0 -45
- package/templates/dashboard/index.html +0 -149
- package/templates/dashboard/js/dialogs.js +0 -368
- package/templates/dashboard/js/files.js +0 -448
- package/templates/dashboard/js/main.js +0 -476
- package/templates/dashboard/js/projects.js +0 -544
- package/templates/dashboard/js/state.js +0 -91
- package/templates/dashboard/js/tabs.js +0 -518
- package/templates/dashboard/js/utils.js +0 -191
|
@@ -9,15 +9,15 @@ import { spawn, execSync } from 'node:child_process';
|
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import { query as claudeQuery } from '@anthropic-ai/claude-agent-sdk';
|
|
12
|
-
import {
|
|
12
|
+
import { Codex } from '@openai/codex-sdk';
|
|
13
|
+
import { readCodevFile, findWorkspaceRoot, hasLocalOverride } from '../../lib/skeleton.js';
|
|
14
|
+
import { MetricsDB } from './metrics.js';
|
|
15
|
+
import { extractUsage, extractReviewText } from './usage-extractor.js';
|
|
13
16
|
const MODEL_CONFIGS = {
|
|
14
17
|
gemini: { cli: 'gemini', args: ['--yolo'], envVar: 'GEMINI_SYSTEM_MD' },
|
|
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 },
|
|
18
18
|
};
|
|
19
|
-
// Models that use
|
|
20
|
-
const SDK_MODELS = ['claude'];
|
|
19
|
+
// Models that use an Agent SDK instead of CLI subprocess
|
|
20
|
+
const SDK_MODELS = ['claude', 'codex'];
|
|
21
21
|
// Claude Agent SDK turn limit. Claude explores the codebase with Read/Glob/Grep
|
|
22
22
|
// tools before producing its verdict, so it needs a generous turn budget.
|
|
23
23
|
const CLAUDE_MAX_TURNS = 200;
|
|
@@ -27,6 +27,36 @@ const MODEL_ALIASES = {
|
|
|
27
27
|
gpt: 'codex',
|
|
28
28
|
opus: 'claude',
|
|
29
29
|
};
|
|
30
|
+
// Helper to record a metrics entry, opening and closing the DB
|
|
31
|
+
function recordMetrics(ctx, extra) {
|
|
32
|
+
try {
|
|
33
|
+
const db = new MetricsDB();
|
|
34
|
+
try {
|
|
35
|
+
db.record({
|
|
36
|
+
timestamp: ctx.timestamp,
|
|
37
|
+
model: ctx.model,
|
|
38
|
+
reviewType: ctx.reviewType,
|
|
39
|
+
subcommand: ctx.subcommand,
|
|
40
|
+
protocol: ctx.protocol,
|
|
41
|
+
projectId: ctx.projectId,
|
|
42
|
+
durationSeconds: extra.durationSeconds,
|
|
43
|
+
inputTokens: extra.inputTokens,
|
|
44
|
+
cachedInputTokens: extra.cachedInputTokens,
|
|
45
|
+
outputTokens: extra.outputTokens,
|
|
46
|
+
costUsd: extra.costUsd,
|
|
47
|
+
exitCode: extra.exitCode,
|
|
48
|
+
workspacePath: ctx.workspacePath,
|
|
49
|
+
errorMessage: extra.errorMessage,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
db.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
console.error(`[warn] Failed to record metrics: ${err instanceof Error ? err.message : String(err)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
30
60
|
// Valid review types
|
|
31
61
|
const VALID_REVIEW_TYPES = [
|
|
32
62
|
'spec-review',
|
|
@@ -46,8 +76,8 @@ function isValidRoleName(roleName) {
|
|
|
46
76
|
* List available roles in codev/roles/
|
|
47
77
|
* Excludes non-role files like README.md, review-types/, etc.
|
|
48
78
|
*/
|
|
49
|
-
function listAvailableRoles(
|
|
50
|
-
const rolesDir = path.join(
|
|
79
|
+
function listAvailableRoles(workspaceRoot) {
|
|
80
|
+
const rolesDir = path.join(workspaceRoot, 'codev', 'roles');
|
|
51
81
|
if (!fs.existsSync(rolesDir))
|
|
52
82
|
return [];
|
|
53
83
|
const excludePatterns = ['readme', 'review-types', 'overview', 'index'];
|
|
@@ -64,7 +94,7 @@ function listAvailableRoles(projectRoot) {
|
|
|
64
94
|
* Load a custom role from codev/roles/<name>.md
|
|
65
95
|
* Falls back to embedded skeleton if not found locally.
|
|
66
96
|
*/
|
|
67
|
-
function loadCustomRole(
|
|
97
|
+
function loadCustomRole(workspaceRoot, roleName) {
|
|
68
98
|
// Validate role name to prevent directory traversal
|
|
69
99
|
if (!isValidRoleName(roleName)) {
|
|
70
100
|
throw new Error(`Invalid role name: '${roleName}'\n` +
|
|
@@ -72,9 +102,9 @@ function loadCustomRole(projectRoot, roleName) {
|
|
|
72
102
|
}
|
|
73
103
|
// Use readCodevFile which handles local-first with skeleton fallback
|
|
74
104
|
const rolePath = `roles/${roleName}.md`;
|
|
75
|
-
const roleContent = readCodevFile(rolePath,
|
|
105
|
+
const roleContent = readCodevFile(rolePath, workspaceRoot);
|
|
76
106
|
if (!roleContent) {
|
|
77
|
-
const available = listAvailableRoles(
|
|
107
|
+
const available = listAvailableRoles(workspaceRoot);
|
|
78
108
|
const availableStr = available.length > 0
|
|
79
109
|
? `\n\nAvailable roles:\n${available.map(r => ` - ${r}`).join('\n')}`
|
|
80
110
|
: '\n\nNo custom roles found in codev/roles/';
|
|
@@ -86,8 +116,8 @@ function loadCustomRole(projectRoot, roleName) {
|
|
|
86
116
|
* Load the consultant role.
|
|
87
117
|
* Checks local codev/roles/consultant.md first, then falls back to embedded skeleton.
|
|
88
118
|
*/
|
|
89
|
-
function loadRole(
|
|
90
|
-
const role = readCodevFile('roles/consultant.md',
|
|
119
|
+
function loadRole(workspaceRoot) {
|
|
120
|
+
const role = readCodevFile('roles/consultant.md', workspaceRoot);
|
|
91
121
|
if (!role) {
|
|
92
122
|
throw new Error('consultant.md not found.\n' +
|
|
93
123
|
'Checked: local codev/roles/consultant.md and embedded skeleton.\n' +
|
|
@@ -100,21 +130,21 @@ function loadRole(projectRoot) {
|
|
|
100
130
|
* Checks consult-types/{type}.md first (new location),
|
|
101
131
|
* then falls back to roles/review-types/{type}.md (deprecated) with a warning.
|
|
102
132
|
*/
|
|
103
|
-
function loadReviewTypePrompt(
|
|
133
|
+
function loadReviewTypePrompt(workspaceRoot, reviewType) {
|
|
104
134
|
const primaryPath = `consult-types/${reviewType}.md`;
|
|
105
135
|
const fallbackPath = `roles/review-types/${reviewType}.md`;
|
|
106
136
|
// 1. Check LOCAL consult-types/ first (preferred location)
|
|
107
|
-
if (hasLocalOverride(primaryPath,
|
|
108
|
-
return readCodevFile(primaryPath,
|
|
137
|
+
if (hasLocalOverride(primaryPath, workspaceRoot)) {
|
|
138
|
+
return readCodevFile(primaryPath, workspaceRoot);
|
|
109
139
|
}
|
|
110
140
|
// 2. Check LOCAL roles/review-types/ (deprecated location with warning)
|
|
111
|
-
if (hasLocalOverride(fallbackPath,
|
|
141
|
+
if (hasLocalOverride(fallbackPath, workspaceRoot)) {
|
|
112
142
|
console.error(chalk.yellow('Warning: Review types in roles/review-types/ are deprecated.'));
|
|
113
143
|
console.error(chalk.yellow('Move your custom types to consult-types/ for future compatibility.'));
|
|
114
|
-
return readCodevFile(fallbackPath,
|
|
144
|
+
return readCodevFile(fallbackPath, workspaceRoot);
|
|
115
145
|
}
|
|
116
146
|
// 3. Fall back to embedded skeleton consult-types/ (default)
|
|
117
|
-
const skeletonPrompt = readCodevFile(primaryPath,
|
|
147
|
+
const skeletonPrompt = readCodevFile(primaryPath, workspaceRoot);
|
|
118
148
|
if (skeletonPrompt) {
|
|
119
149
|
return skeletonPrompt;
|
|
120
150
|
}
|
|
@@ -123,8 +153,8 @@ function loadReviewTypePrompt(projectRoot, reviewType) {
|
|
|
123
153
|
/**
|
|
124
154
|
* Load .env file if it exists
|
|
125
155
|
*/
|
|
126
|
-
function loadDotenv(
|
|
127
|
-
const envFile = path.join(
|
|
156
|
+
function loadDotenv(workspaceRoot) {
|
|
157
|
+
const envFile = path.join(workspaceRoot, '.env');
|
|
128
158
|
if (!fs.existsSync(envFile))
|
|
129
159
|
return;
|
|
130
160
|
const content = fs.readFileSync(envFile, 'utf-8');
|
|
@@ -151,8 +181,8 @@ function loadDotenv(projectRoot) {
|
|
|
151
181
|
/**
|
|
152
182
|
* Find a spec file by number
|
|
153
183
|
*/
|
|
154
|
-
function findSpec(
|
|
155
|
-
const specsDir = path.join(
|
|
184
|
+
function findSpec(workspaceRoot, number) {
|
|
185
|
+
const specsDir = path.join(workspaceRoot, 'codev', 'specs');
|
|
156
186
|
const pattern = String(number).padStart(4, '0');
|
|
157
187
|
if (fs.existsSync(specsDir)) {
|
|
158
188
|
const files = fs.readdirSync(specsDir);
|
|
@@ -167,8 +197,8 @@ function findSpec(projectRoot, number) {
|
|
|
167
197
|
/**
|
|
168
198
|
* Find a plan file by number
|
|
169
199
|
*/
|
|
170
|
-
function findPlan(
|
|
171
|
-
const plansDir = path.join(
|
|
200
|
+
function findPlan(workspaceRoot, number) {
|
|
201
|
+
const plansDir = path.join(workspaceRoot, 'codev', 'plans');
|
|
172
202
|
const pattern = String(number).padStart(4, '0');
|
|
173
203
|
if (fs.existsSync(plansDir)) {
|
|
174
204
|
const files = fs.readdirSync(plansDir);
|
|
@@ -183,9 +213,9 @@ function findPlan(projectRoot, number) {
|
|
|
183
213
|
/**
|
|
184
214
|
* Log query to history file
|
|
185
215
|
*/
|
|
186
|
-
function logQuery(
|
|
216
|
+
function logQuery(workspaceRoot, model, query, duration) {
|
|
187
217
|
try {
|
|
188
|
-
const logDir = path.join(
|
|
218
|
+
const logDir = path.join(workspaceRoot, '.consult');
|
|
189
219
|
if (!fs.existsSync(logDir)) {
|
|
190
220
|
fs.mkdirSync(logDir, { recursive: true });
|
|
191
221
|
}
|
|
@@ -211,13 +241,109 @@ function commandExists(cmd) {
|
|
|
211
241
|
return false;
|
|
212
242
|
}
|
|
213
243
|
}
|
|
244
|
+
// Codex pricing for cost computation (matches values from old SUBPROCESS_MODEL_PRICING)
|
|
245
|
+
const CODEX_PRICING = { inputPer1M: 2.00, cachedInputPer1M: 1.00, outputPer1M: 8.00 };
|
|
246
|
+
/**
|
|
247
|
+
* Run Codex consultation via @openai/codex-sdk.
|
|
248
|
+
* Mirrors runClaudeConsultation() — streams events, captures usage, records metrics.
|
|
249
|
+
*/
|
|
250
|
+
export async function runCodexConsultation(queryText, role, workspaceRoot, outputPath, metricsCtx) {
|
|
251
|
+
const chunks = [];
|
|
252
|
+
const startTime = Date.now();
|
|
253
|
+
let usageData = null;
|
|
254
|
+
let errorMessage = null;
|
|
255
|
+
let exitCode = 0;
|
|
256
|
+
// Write role to temp file — SDK requires file path for instructions
|
|
257
|
+
const tempFile = path.join(tmpdir(), `codev-role-${Date.now()}.md`);
|
|
258
|
+
fs.writeFileSync(tempFile, role);
|
|
259
|
+
try {
|
|
260
|
+
const codex = new Codex({
|
|
261
|
+
config: {
|
|
262
|
+
experimental_instructions_file: tempFile,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
const thread = codex.startThread({
|
|
266
|
+
model: 'gpt-5.2-codex',
|
|
267
|
+
sandboxMode: 'read-only',
|
|
268
|
+
modelReasoningEffort: 'medium',
|
|
269
|
+
workingDirectory: workspaceRoot,
|
|
270
|
+
});
|
|
271
|
+
const { events } = await thread.runStreamed(queryText);
|
|
272
|
+
for await (const event of events) {
|
|
273
|
+
if (event.type === 'item.completed') {
|
|
274
|
+
const item = event.item;
|
|
275
|
+
if (item.type === 'agent_message') {
|
|
276
|
+
process.stdout.write(item.text);
|
|
277
|
+
chunks.push(item.text);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (event.type === 'turn.completed') {
|
|
281
|
+
const input = event.usage.input_tokens;
|
|
282
|
+
const cached = event.usage.cached_input_tokens;
|
|
283
|
+
const output = event.usage.output_tokens;
|
|
284
|
+
const uncached = input - cached;
|
|
285
|
+
const cost = (uncached / 1_000_000) * CODEX_PRICING.inputPer1M
|
|
286
|
+
+ (cached / 1_000_000) * CODEX_PRICING.cachedInputPer1M
|
|
287
|
+
+ (output / 1_000_000) * CODEX_PRICING.outputPer1M;
|
|
288
|
+
usageData = { inputTokens: input, cachedInputTokens: cached, outputTokens: output, costUsd: cost };
|
|
289
|
+
}
|
|
290
|
+
if (event.type === 'turn.failed') {
|
|
291
|
+
errorMessage = event.error.message ?? 'Codex turn failed';
|
|
292
|
+
exitCode = 1;
|
|
293
|
+
throw new Error(errorMessage);
|
|
294
|
+
}
|
|
295
|
+
if (event.type === 'error') {
|
|
296
|
+
errorMessage = event.message ?? 'Codex stream error';
|
|
297
|
+
exitCode = 1;
|
|
298
|
+
throw new Error(errorMessage);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Write output file
|
|
302
|
+
if (outputPath) {
|
|
303
|
+
const outputDir = path.dirname(outputPath);
|
|
304
|
+
if (!fs.existsSync(outputDir))
|
|
305
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
306
|
+
fs.writeFileSync(outputPath, chunks.join(''));
|
|
307
|
+
console.error(`\nOutput written to: ${outputPath}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
if (!errorMessage) {
|
|
312
|
+
errorMessage = (err instanceof Error ? err.message : String(err)).substring(0, 500);
|
|
313
|
+
exitCode = 1;
|
|
314
|
+
}
|
|
315
|
+
throw err;
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
// Clean up temp file
|
|
319
|
+
if (fs.existsSync(tempFile))
|
|
320
|
+
fs.unlinkSync(tempFile);
|
|
321
|
+
// Record metrics (always, even on error)
|
|
322
|
+
if (metricsCtx) {
|
|
323
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
324
|
+
recordMetrics(metricsCtx, {
|
|
325
|
+
durationSeconds: duration,
|
|
326
|
+
inputTokens: usageData?.inputTokens ?? null,
|
|
327
|
+
cachedInputTokens: usageData?.cachedInputTokens ?? null,
|
|
328
|
+
outputTokens: usageData?.outputTokens ?? null,
|
|
329
|
+
costUsd: usageData?.costUsd ?? null,
|
|
330
|
+
exitCode,
|
|
331
|
+
errorMessage,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
214
336
|
/**
|
|
215
337
|
* Run Claude consultation via Agent SDK.
|
|
216
338
|
* Uses the SDK's query() function instead of CLI subprocess.
|
|
217
339
|
* This avoids the CLAUDECODE nesting guard and enables tool use during reviews.
|
|
218
340
|
*/
|
|
219
|
-
async function runClaudeConsultation(queryText, role,
|
|
341
|
+
async function runClaudeConsultation(queryText, role, workspaceRoot, outputPath, metricsCtx) {
|
|
220
342
|
const chunks = [];
|
|
343
|
+
const startTime = Date.now();
|
|
344
|
+
let sdkResult;
|
|
345
|
+
let errorMessage = null;
|
|
346
|
+
let exitCode = 0;
|
|
221
347
|
// The SDK spawns a Claude Code subprocess that checks process.env.CLAUDECODE.
|
|
222
348
|
// We must remove it from process.env (not just the options env) to avoid
|
|
223
349
|
// the nesting guard. Restore it after the SDK call.
|
|
@@ -240,7 +366,7 @@ async function runClaudeConsultation(queryText, role, projectRoot, outputPath) {
|
|
|
240
366
|
model: 'claude-opus-4-6',
|
|
241
367
|
maxTurns: CLAUDE_MAX_TURNS,
|
|
242
368
|
maxBudgetUsd: 25,
|
|
243
|
-
cwd:
|
|
369
|
+
cwd: workspaceRoot,
|
|
244
370
|
env,
|
|
245
371
|
},
|
|
246
372
|
});
|
|
@@ -254,9 +380,14 @@ async function runClaudeConsultation(queryText, role, projectRoot, outputPath) {
|
|
|
254
380
|
}
|
|
255
381
|
}
|
|
256
382
|
if (message.type === 'result') {
|
|
257
|
-
if (message.subtype
|
|
383
|
+
if (message.subtype === 'success') {
|
|
384
|
+
sdkResult = message;
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
258
387
|
const errors = 'errors' in message ? message.errors : [];
|
|
259
|
-
|
|
388
|
+
errorMessage = `Claude SDK error (${message.subtype}): ${errors.join(', ')}`.substring(0, 500);
|
|
389
|
+
exitCode = 1;
|
|
390
|
+
throw new Error(errorMessage);
|
|
260
391
|
}
|
|
261
392
|
}
|
|
262
393
|
}
|
|
@@ -268,21 +399,42 @@ async function runClaudeConsultation(queryText, role, projectRoot, outputPath) {
|
|
|
268
399
|
console.error(`\nOutput written to: ${outputPath}`);
|
|
269
400
|
}
|
|
270
401
|
}
|
|
402
|
+
catch (err) {
|
|
403
|
+
if (!errorMessage) {
|
|
404
|
+
errorMessage = (err instanceof Error ? err.message : String(err)).substring(0, 500);
|
|
405
|
+
exitCode = 1;
|
|
406
|
+
}
|
|
407
|
+
throw err;
|
|
408
|
+
}
|
|
271
409
|
finally {
|
|
272
410
|
if (savedClaudeCode !== undefined) {
|
|
273
411
|
process.env.CLAUDECODE = savedClaudeCode;
|
|
274
412
|
}
|
|
413
|
+
// Record metrics (always, even on error)
|
|
414
|
+
if (metricsCtx) {
|
|
415
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
416
|
+
const usage = sdkResult ? extractUsage('claude', '', sdkResult) : null;
|
|
417
|
+
recordMetrics(metricsCtx, {
|
|
418
|
+
durationSeconds: duration,
|
|
419
|
+
inputTokens: usage?.inputTokens ?? null,
|
|
420
|
+
cachedInputTokens: usage?.cachedInputTokens ?? null,
|
|
421
|
+
outputTokens: usage?.outputTokens ?? null,
|
|
422
|
+
costUsd: usage?.costUsd ?? null,
|
|
423
|
+
exitCode,
|
|
424
|
+
errorMessage,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
275
427
|
}
|
|
276
428
|
}
|
|
277
429
|
/**
|
|
278
430
|
* Run the consultation
|
|
279
431
|
*/
|
|
280
|
-
async function runConsultation(model, query,
|
|
432
|
+
async function runConsultation(model, query, workspaceRoot, dryRun, reviewType, customRole, outputPath, metricsCtx) {
|
|
281
433
|
// Use custom role if specified, otherwise use default consultant role
|
|
282
|
-
let role = customRole ? loadCustomRole(
|
|
434
|
+
let role = customRole ? loadCustomRole(workspaceRoot, customRole) : loadRole(workspaceRoot);
|
|
283
435
|
// Append review type prompt if specified
|
|
284
436
|
if (reviewType) {
|
|
285
|
-
const typePrompt = loadReviewTypePrompt(
|
|
437
|
+
const typePrompt = loadReviewTypePrompt(workspaceRoot, reviewType);
|
|
286
438
|
if (typePrompt) {
|
|
287
439
|
role = role + '\n\n---\n\n' + typePrompt;
|
|
288
440
|
console.error(`Review type: ${reviewType}`);
|
|
@@ -291,7 +443,7 @@ async function runConsultation(model, query, projectRoot, dryRun, reviewType, cu
|
|
|
291
443
|
console.error(chalk.yellow(`Warning: Review type prompt not found: ${reviewType}`));
|
|
292
444
|
}
|
|
293
445
|
}
|
|
294
|
-
//
|
|
446
|
+
// SDK-based models — handle separately from CLI subprocess models
|
|
295
447
|
if (model === 'claude') {
|
|
296
448
|
if (dryRun) {
|
|
297
449
|
console.log(chalk.yellow(`[claude] Would invoke Agent SDK:`));
|
|
@@ -304,9 +456,26 @@ async function runConsultation(model, query, projectRoot, dryRun, reviewType, cu
|
|
|
304
456
|
return;
|
|
305
457
|
}
|
|
306
458
|
const startTime = Date.now();
|
|
307
|
-
await runClaudeConsultation(query, role,
|
|
459
|
+
await runClaudeConsultation(query, role, workspaceRoot, outputPath, metricsCtx);
|
|
308
460
|
const duration = (Date.now() - startTime) / 1000;
|
|
309
|
-
logQuery(
|
|
461
|
+
logQuery(workspaceRoot, model, query, duration);
|
|
462
|
+
console.error(`\n[${model} completed in ${duration.toFixed(1)}s]`);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
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
|
+
const startTime = Date.now();
|
|
476
|
+
await runCodexConsultation(query, role, workspaceRoot, outputPath, metricsCtx);
|
|
477
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
478
|
+
logQuery(workspaceRoot, model, query, duration);
|
|
310
479
|
console.error(`\n[${model} completed in ${duration.toFixed(1)}s]`);
|
|
311
480
|
return;
|
|
312
481
|
}
|
|
@@ -327,21 +496,7 @@ async function runConsultation(model, query, projectRoot, dryRun, reviewType, cu
|
|
|
327
496
|
tempFile = path.join(tmpdir(), `codev-role-${Date.now()}.md`);
|
|
328
497
|
fs.writeFileSync(tempFile, role);
|
|
329
498
|
env['GEMINI_SYSTEM_MD'] = tempFile;
|
|
330
|
-
cmd = [config.cli, ...config.args, query];
|
|
331
|
-
}
|
|
332
|
-
else if (model === 'codex') {
|
|
333
|
-
// Codex uses experimental_instructions_file config flag (not env var)
|
|
334
|
-
// This is the official approach per https://github.com/openai/codex/discussions/3896
|
|
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=low', // Faster responses (10-20% improvement)
|
|
342
|
-
'--full-auto',
|
|
343
|
-
query,
|
|
344
|
-
];
|
|
499
|
+
cmd = [config.cli, ...config.args, '--output-format', 'json', query];
|
|
345
500
|
}
|
|
346
501
|
else {
|
|
347
502
|
throw new Error(`Unknown model: ${model}`);
|
|
@@ -369,35 +524,56 @@ async function runConsultation(model, query, projectRoot, dryRun, reviewType, cu
|
|
|
369
524
|
// When outputPath is set, capture stdout to write to file (used by porch)
|
|
370
525
|
const fullEnv = { ...process.env, ...env };
|
|
371
526
|
const startTime = Date.now();
|
|
372
|
-
const stdoutMode =
|
|
527
|
+
const stdoutMode = 'pipe'; // Always pipe to capture structured output for metrics
|
|
373
528
|
return new Promise((resolve, reject) => {
|
|
374
529
|
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
530
|
+
cwd: workspaceRoot,
|
|
375
531
|
env: fullEnv,
|
|
376
532
|
stdio: ['ignore', stdoutMode, 'inherit'],
|
|
377
533
|
});
|
|
378
534
|
const chunks = [];
|
|
379
|
-
if (
|
|
535
|
+
if (proc.stdout) {
|
|
380
536
|
proc.stdout.on('data', (chunk) => {
|
|
381
537
|
chunks.push(chunk);
|
|
382
|
-
//
|
|
383
|
-
process.stdout.write(chunk);
|
|
538
|
+
// Gemini: buffer only (JSON is one blob, text emitted on close)
|
|
384
539
|
});
|
|
385
540
|
}
|
|
386
541
|
proc.on('close', (code) => {
|
|
387
542
|
const duration = (Date.now() - startTime) / 1000;
|
|
388
|
-
logQuery(
|
|
543
|
+
logQuery(workspaceRoot, model, query, duration);
|
|
389
544
|
if (tempFile && fs.existsSync(tempFile)) {
|
|
390
545
|
fs.unlinkSync(tempFile);
|
|
391
546
|
}
|
|
392
|
-
|
|
393
|
-
|
|
547
|
+
const rawOutput = Buffer.concat(chunks).toString('utf-8');
|
|
548
|
+
// Extract review text from structured output (JSON/JSONL → plain text)
|
|
549
|
+
const reviewText = extractReviewText(model, rawOutput);
|
|
550
|
+
const outputContent = reviewText ?? rawOutput; // Fallback to raw on parse failure
|
|
551
|
+
// Write extracted text to stdout for Gemini (was fully buffered)
|
|
552
|
+
if (model === 'gemini') {
|
|
553
|
+
process.stdout.write(outputContent);
|
|
554
|
+
}
|
|
555
|
+
// Write to output file
|
|
556
|
+
if (outputPath && outputContent.length > 0) {
|
|
394
557
|
const outputDir = path.dirname(outputPath);
|
|
395
558
|
if (!fs.existsSync(outputDir)) {
|
|
396
559
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
397
560
|
}
|
|
398
|
-
fs.writeFileSync(outputPath,
|
|
561
|
+
fs.writeFileSync(outputPath, outputContent);
|
|
399
562
|
console.error(`\nOutput written to: ${outputPath}`);
|
|
400
563
|
}
|
|
564
|
+
// Record metrics
|
|
565
|
+
if (metricsCtx) {
|
|
566
|
+
const usage = extractUsage(model, rawOutput);
|
|
567
|
+
recordMetrics(metricsCtx, {
|
|
568
|
+
durationSeconds: duration,
|
|
569
|
+
inputTokens: usage?.inputTokens ?? null,
|
|
570
|
+
cachedInputTokens: usage?.cachedInputTokens ?? null,
|
|
571
|
+
outputTokens: usage?.outputTokens ?? null,
|
|
572
|
+
costUsd: usage?.costUsd ?? null,
|
|
573
|
+
exitCode: code ?? 1,
|
|
574
|
+
errorMessage: code !== 0 ? `Process exited with code ${code}` : null,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
401
577
|
console.error(`\n[${model} completed in ${duration.toFixed(1)}s]`);
|
|
402
578
|
if (code !== 0) {
|
|
403
579
|
reject(new Error(`Process exited with code ${code}`));
|
|
@@ -410,6 +586,19 @@ async function runConsultation(model, query, projectRoot, dryRun, reviewType, cu
|
|
|
410
586
|
if (tempFile && fs.existsSync(tempFile)) {
|
|
411
587
|
fs.unlinkSync(tempFile);
|
|
412
588
|
}
|
|
589
|
+
// Record metrics for spawn failures
|
|
590
|
+
if (metricsCtx) {
|
|
591
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
592
|
+
recordMetrics(metricsCtx, {
|
|
593
|
+
durationSeconds: duration,
|
|
594
|
+
inputTokens: null,
|
|
595
|
+
cachedInputTokens: null,
|
|
596
|
+
outputTokens: null,
|
|
597
|
+
costUsd: null,
|
|
598
|
+
exitCode: 1,
|
|
599
|
+
errorMessage: (error.message || String(error)).substring(0, 500),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
413
602
|
reject(error);
|
|
414
603
|
});
|
|
415
604
|
});
|
|
@@ -418,9 +607,9 @@ async function runConsultation(model, query, projectRoot, dryRun, reviewType, cu
|
|
|
418
607
|
* Get a compact diff stat summary and list of changed files.
|
|
419
608
|
* Returns { stat, files } where stat is the `--stat` output and files is the list of paths.
|
|
420
609
|
*/
|
|
421
|
-
function getDiffStat(
|
|
422
|
-
const stat = execSync(`git diff --stat ${ref}`, { cwd:
|
|
423
|
-
const nameOnly = execSync(`git diff --name-only ${ref}`, { cwd:
|
|
610
|
+
function getDiffStat(workspaceRoot, ref) {
|
|
611
|
+
const stat = execSync(`git diff --stat ${ref}`, { cwd: workspaceRoot, encoding: 'utf-8' }).toString();
|
|
612
|
+
const nameOnly = execSync(`git diff --name-only ${ref}`, { cwd: workspaceRoot, encoding: 'utf-8' }).toString();
|
|
424
613
|
const files = nameOnly.trim().split('\n').filter(Boolean);
|
|
425
614
|
return { stat, files };
|
|
426
615
|
}
|
|
@@ -450,7 +639,7 @@ function fetchPRData(prNumber) {
|
|
|
450
639
|
* Build query for PR review.
|
|
451
640
|
* Provides file list and instructs reviewers to read files from disk.
|
|
452
641
|
*/
|
|
453
|
-
function buildPRQuery(prNumber,
|
|
642
|
+
function buildPRQuery(prNumber, _workspaceRoot) {
|
|
454
643
|
const prData = fetchPRData(prNumber);
|
|
455
644
|
const fileList = prData.changedFiles.map(f => `- ${f}`).join('\n');
|
|
456
645
|
return `Review Pull Request #${prNumber}
|
|
@@ -466,7 +655,7 @@ ${fileList}
|
|
|
466
655
|
## How to Review
|
|
467
656
|
**Read the changed files from disk** to review their current content. You have full filesystem access.
|
|
468
657
|
For each changed file listed above, read it and evaluate the code quality, correctness, and test coverage.
|
|
469
|
-
|
|
658
|
+
Do NOT rely on git diffs to determine the current state of code — diffs miss uncommitted changes in worktrees.
|
|
470
659
|
|
|
471
660
|
## Comments
|
|
472
661
|
${prData.comments}
|
|
@@ -504,6 +693,11 @@ Please read and review this specification:
|
|
|
504
693
|
query += `- Plan file: ${planPath}\n`;
|
|
505
694
|
}
|
|
506
695
|
query += `
|
|
696
|
+
## How to Review
|
|
697
|
+
**Read the files listed above directly from disk.** You have full filesystem access.
|
|
698
|
+
Do NOT rely on \`git diff\` or \`git log\` to review content — diffs may be truncated or miss uncommitted work.
|
|
699
|
+
Open the spec file, read it in full, and evaluate it directly.
|
|
700
|
+
|
|
507
701
|
Please review:
|
|
508
702
|
1. Clarity and completeness of requirements
|
|
509
703
|
2. Technical feasibility
|
|
@@ -527,15 +721,17 @@ KEY_ISSUES: [List of critical issues if any, or "None"]`;
|
|
|
527
721
|
* Build query for implementation review.
|
|
528
722
|
* Provides diff stat + file list and instructs reviewers to read files from disk.
|
|
529
723
|
*/
|
|
530
|
-
function buildImplQuery(projectNumber,
|
|
531
|
-
const specPath = findSpec(
|
|
532
|
-
const planPath = findPlan(
|
|
724
|
+
function buildImplQuery(projectNumber, workspaceRoot, planPhase) {
|
|
725
|
+
const specPath = findSpec(workspaceRoot, projectNumber);
|
|
726
|
+
const planPath = findPlan(workspaceRoot, projectNumber);
|
|
533
727
|
// Get compact diff summary against base branch
|
|
534
728
|
let diffStat = '';
|
|
535
729
|
let changedFiles = [];
|
|
536
730
|
try {
|
|
537
|
-
const mergeBase = execSync('git merge-base HEAD main', { cwd:
|
|
538
|
-
|
|
731
|
+
const mergeBase = execSync('git merge-base HEAD main', { cwd: workspaceRoot, encoding: 'utf-8' }).trim();
|
|
732
|
+
// Use mergeBase (not mergeBase..HEAD) to include uncommitted working tree changes.
|
|
733
|
+
// The ..HEAD syntax is commit-to-commit and misses uncommitted work in builder worktrees.
|
|
734
|
+
const result = getDiffStat(workspaceRoot, mergeBase);
|
|
539
735
|
diffStat = result.stat;
|
|
540
736
|
changedFiles = result.files;
|
|
541
737
|
}
|
|
@@ -569,7 +765,7 @@ function buildImplQuery(projectNumber, projectRoot, planPhase) {
|
|
|
569
765
|
query += `\n\n## How to Review\n`;
|
|
570
766
|
query += `**Read the changed files from disk** to review their actual content. You have full filesystem access.\n`;
|
|
571
767
|
query += `For each file listed above, read it and evaluate the implementation against the spec/plan.\n`;
|
|
572
|
-
query += `
|
|
768
|
+
query += `Do NOT rely on git diffs to determine the current state of code — diffs miss uncommitted changes in worktrees.\n`;
|
|
573
769
|
}
|
|
574
770
|
else {
|
|
575
771
|
query += `\n## Instructions\n\nRead the spec and plan files above, then explore the filesystem to find and review the implementation changes.\n`;
|
|
@@ -606,6 +802,11 @@ Please read and review this implementation plan:
|
|
|
606
802
|
query += `- Spec file: ${specPath} (for context)\n`;
|
|
607
803
|
}
|
|
608
804
|
query += `
|
|
805
|
+
## How to Review
|
|
806
|
+
**Read the files listed above directly from disk.** You have full filesystem access.
|
|
807
|
+
Do NOT rely on \`git diff\` or \`git log\` to review content — diffs may be truncated or miss uncommitted work.
|
|
808
|
+
Open the plan file (and spec if provided), read them in full, and evaluate the plan directly.
|
|
809
|
+
|
|
609
810
|
Please review:
|
|
610
811
|
1. Alignment with specification requirements
|
|
611
812
|
2. Implementation approach and architecture
|
|
@@ -641,8 +842,20 @@ export async function consult(options) {
|
|
|
641
842
|
if (reviewType && !VALID_REVIEW_TYPES.includes(reviewType)) {
|
|
642
843
|
throw new Error(`Invalid review type: ${reviewType}\nValid types: ${VALID_REVIEW_TYPES.join(', ')}`);
|
|
643
844
|
}
|
|
644
|
-
const
|
|
645
|
-
loadDotenv(
|
|
845
|
+
const workspaceRoot = findWorkspaceRoot();
|
|
846
|
+
loadDotenv(workspaceRoot);
|
|
847
|
+
// Capture timestamp at invocation start (before subprocess/SDK)
|
|
848
|
+
const timestamp = new Date().toISOString();
|
|
849
|
+
// Build metrics context with protocol/project defaults
|
|
850
|
+
const metricsCtx = {
|
|
851
|
+
timestamp,
|
|
852
|
+
model,
|
|
853
|
+
reviewType: reviewType ?? null,
|
|
854
|
+
subcommand,
|
|
855
|
+
protocol: options.protocol ?? 'manual',
|
|
856
|
+
projectId: options.projectId ?? null,
|
|
857
|
+
workspacePath: workspaceRoot,
|
|
858
|
+
};
|
|
646
859
|
console.error(`[${subcommand} review]`);
|
|
647
860
|
console.error(`Model: ${model}`);
|
|
648
861
|
// Log custom role if specified
|
|
@@ -659,7 +872,7 @@ export async function consult(options) {
|
|
|
659
872
|
if (isNaN(prNumber)) {
|
|
660
873
|
throw new Error(`Invalid PR number: ${args[0]}`);
|
|
661
874
|
}
|
|
662
|
-
query = buildPRQuery(prNumber,
|
|
875
|
+
query = buildPRQuery(prNumber, workspaceRoot);
|
|
663
876
|
break;
|
|
664
877
|
}
|
|
665
878
|
case 'spec': {
|
|
@@ -670,11 +883,11 @@ export async function consult(options) {
|
|
|
670
883
|
if (isNaN(specNumber)) {
|
|
671
884
|
throw new Error(`Invalid spec number: ${args[0]}`);
|
|
672
885
|
}
|
|
673
|
-
const specPath = findSpec(
|
|
886
|
+
const specPath = findSpec(workspaceRoot, specNumber);
|
|
674
887
|
if (!specPath) {
|
|
675
888
|
throw new Error(`Spec ${specNumber} not found`);
|
|
676
889
|
}
|
|
677
|
-
const planPath = findPlan(
|
|
890
|
+
const planPath = findPlan(workspaceRoot, specNumber);
|
|
678
891
|
query = buildSpecQuery(specPath, planPath);
|
|
679
892
|
console.error(`Spec: ${specPath}`);
|
|
680
893
|
if (planPath)
|
|
@@ -689,11 +902,11 @@ export async function consult(options) {
|
|
|
689
902
|
if (isNaN(planNumber)) {
|
|
690
903
|
throw new Error(`Invalid plan number: ${args[0]}`);
|
|
691
904
|
}
|
|
692
|
-
const planPath = findPlan(
|
|
905
|
+
const planPath = findPlan(workspaceRoot, planNumber);
|
|
693
906
|
if (!planPath) {
|
|
694
907
|
throw new Error(`Plan ${planNumber} not found`);
|
|
695
908
|
}
|
|
696
|
-
const specPath = findSpec(
|
|
909
|
+
const specPath = findSpec(workspaceRoot, planNumber);
|
|
697
910
|
query = buildPlanQuery(planPath, specPath);
|
|
698
911
|
console.error(`Plan: ${planPath}`);
|
|
699
912
|
if (specPath)
|
|
@@ -715,9 +928,9 @@ export async function consult(options) {
|
|
|
715
928
|
if (isNaN(implNumber)) {
|
|
716
929
|
throw new Error(`Invalid project number: ${args[0]}`);
|
|
717
930
|
}
|
|
718
|
-
const specPath = findSpec(
|
|
719
|
-
const planPath = findPlan(
|
|
720
|
-
query = buildImplQuery(implNumber,
|
|
931
|
+
const specPath = findSpec(workspaceRoot, implNumber);
|
|
932
|
+
const planPath = findPlan(workspaceRoot, implNumber);
|
|
933
|
+
query = buildImplQuery(implNumber, workspaceRoot, options.planPhase);
|
|
721
934
|
console.error(`Project: ${implNumber}`);
|
|
722
935
|
if (specPath)
|
|
723
936
|
console.error(`Spec: ${specPath}`);
|
|
@@ -752,8 +965,8 @@ export async function consult(options) {
|
|
|
752
965
|
console.error(`[${model.toUpperCase()}] Starting consultation...`);
|
|
753
966
|
console.error('='.repeat(60));
|
|
754
967
|
console.error('');
|
|
755
|
-
await runConsultation(model, query,
|
|
968
|
+
await runConsultation(model, query, workspaceRoot, dryRun, reviewType, customRole, outputPath, metricsCtx);
|
|
756
969
|
}
|
|
757
970
|
// Exported for testing
|
|
758
|
-
export { getDiffStat as _getDiffStat };
|
|
971
|
+
export { getDiffStat as _getDiffStat, buildSpecQuery as _buildSpecQuery, buildPlanQuery as _buildPlanQuery };
|
|
759
972
|
//# sourceMappingURL=index.js.map
|