@vexdo/cli 0.1.0 → 0.1.2
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/README.md +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1597 -0
- package/package.json +9 -1
- package/.eslintrc.json +0 -23
- package/.github/workflows/ci.yml +0 -84
- package/.idea/copilot.data.migration.ask2agent.xml +0 -6
- package/.idea/go.imports.xml +0 -11
- package/.idea/misc.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -7
- package/.idea/vexdo-cli.iml +0 -9
- package/.prettierrc +0 -5
- package/CLAUDE.md +0 -93
- package/CONTRIBUTING.md +0 -62
- package/src/commands/abort.ts +0 -66
- package/src/commands/fix.ts +0 -106
- package/src/commands/init.ts +0 -142
- package/src/commands/logs.ts +0 -74
- package/src/commands/review.ts +0 -107
- package/src/commands/start.ts +0 -197
- package/src/commands/status.ts +0 -52
- package/src/commands/submit.ts +0 -38
- package/src/index.ts +0 -42
- package/src/lib/claude.ts +0 -259
- package/src/lib/codex.ts +0 -96
- package/src/lib/config.ts +0 -157
- package/src/lib/gh.ts +0 -78
- package/src/lib/git.ts +0 -119
- package/src/lib/logger.ts +0 -147
- package/src/lib/requirements.ts +0 -18
- package/src/lib/review-loop.ts +0 -154
- package/src/lib/state.ts +0 -121
- package/src/lib/submit-task.ts +0 -43
- package/src/lib/tasks.ts +0 -94
- package/src/prompts/arbiter.ts +0 -21
- package/src/prompts/reviewer.ts +0 -20
- package/src/types/index.ts +0 -96
- package/test/config.test.ts +0 -124
- package/test/state.test.ts +0 -147
- package/test/unit/claude.test.ts +0 -117
- package/test/unit/codex.test.ts +0 -67
- package/test/unit/gh.test.ts +0 -49
- package/test/unit/git.test.ts +0 -120
- package/test/unit/review-loop.test.ts +0 -198
- package/tests/integration/review.test.ts +0 -137
- package/tests/integration/start.test.ts +0 -220
- package/tests/unit/init.test.ts +0 -91
- package/tsconfig.json +0 -15
- package/tsup.config.ts +0 -8
- package/vitest.config.ts +0 -7
package/src/lib/logger.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import ora from 'ora';
|
|
2
|
-
import type { Ora } from 'ora';
|
|
3
|
-
import pc from 'picocolors';
|
|
4
|
-
|
|
5
|
-
import type { ReviewComment } from '../types/index.js';
|
|
6
|
-
|
|
7
|
-
let verboseEnabled = false;
|
|
8
|
-
|
|
9
|
-
function safeLog(method: 'log' | 'error', message: string): void {
|
|
10
|
-
try {
|
|
11
|
-
if (method === 'error') {
|
|
12
|
-
console.error(message);
|
|
13
|
-
} else {
|
|
14
|
-
console.log(message);
|
|
15
|
-
}
|
|
16
|
-
} catch {
|
|
17
|
-
// never throw
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function setVerbose(enabled: boolean): void {
|
|
22
|
-
verboseEnabled = enabled;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function info(message: string): void {
|
|
26
|
-
safeLog('log', `${pc.cyan('→')} ${message}`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function success(message: string): void {
|
|
30
|
-
safeLog('log', `${pc.green('✓')} ${message}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function warn(message: string): void {
|
|
34
|
-
safeLog('log', `${pc.yellow('⚠')} ${message}`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function error(message: string): void {
|
|
38
|
-
safeLog('error', `${pc.red('✖')} ${message}`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function debug(message: string): void {
|
|
42
|
-
if (!verboseEnabled) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
safeLog('log', `${pc.gray('•')} ${message}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function header(title: string): void {
|
|
49
|
-
safeLog('log', `${pc.bold(pc.white(title))}\n${pc.gray('─'.repeat(title.length))}`);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function step(n: number, total: number, title: string): void {
|
|
53
|
-
safeLog('log', `${pc.bold(`Step ${String(n)}/${String(total)}:`)} ${title}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function iteration(n: number, max: number): void {
|
|
57
|
-
safeLog('log', pc.gray(`Iteration ${String(n)}/${String(max)}`));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function fatal(message: string, hint?: string): void {
|
|
61
|
-
safeLog('error', `${pc.bold(pc.red('Error:'))} ${message}`);
|
|
62
|
-
if (hint) {
|
|
63
|
-
safeLog('error', `${pc.gray('Hint:')} ${hint}`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function spinner(text: string): Ora {
|
|
68
|
-
try {
|
|
69
|
-
return ora({ text });
|
|
70
|
-
} catch {
|
|
71
|
-
return ora({ text: '' });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function escalation(context: {
|
|
76
|
-
taskId: string;
|
|
77
|
-
service: string;
|
|
78
|
-
iteration: number;
|
|
79
|
-
spec: string;
|
|
80
|
-
diff: string;
|
|
81
|
-
reviewComments: ReviewComment[];
|
|
82
|
-
arbiterReasoning: string;
|
|
83
|
-
summary: string;
|
|
84
|
-
}): void {
|
|
85
|
-
const lines = [
|
|
86
|
-
pc.bold(pc.red('Escalation triggered')),
|
|
87
|
-
`${pc.gray('Task:')} ${context.taskId}`,
|
|
88
|
-
`${pc.gray('Service:')} ${context.service}`,
|
|
89
|
-
`${pc.gray('Iteration:')} ${String(context.iteration)}`,
|
|
90
|
-
`${pc.gray('Summary:')} ${context.summary}`,
|
|
91
|
-
'',
|
|
92
|
-
pc.bold('Spec:'),
|
|
93
|
-
context.spec,
|
|
94
|
-
'',
|
|
95
|
-
pc.bold('Arbiter reasoning:'),
|
|
96
|
-
context.arbiterReasoning,
|
|
97
|
-
'',
|
|
98
|
-
pc.bold('Review comments:'),
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
for (const comment of context.reviewComments) {
|
|
102
|
-
const sevColor =
|
|
103
|
-
comment.severity === 'critical'
|
|
104
|
-
? pc.red
|
|
105
|
-
: comment.severity === 'important'
|
|
106
|
-
? pc.yellow
|
|
107
|
-
: comment.severity === 'minor'
|
|
108
|
-
? pc.cyan
|
|
109
|
-
: pc.gray;
|
|
110
|
-
const location = comment.file ? ` (${comment.file}${comment.line ? `:${String(comment.line)}` : ''})` : '';
|
|
111
|
-
lines.push(`- ${sevColor(comment.severity.toUpperCase())}${location}: ${comment.comment}`);
|
|
112
|
-
if (comment.suggestion) {
|
|
113
|
-
lines.push(` ${pc.gray(`Suggestion: ${comment.suggestion}`)}`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
lines.push('', pc.bold('Diff:'), context.diff, '', pc.gray('Hint: run `vexdo abort` to clear state.'));
|
|
118
|
-
|
|
119
|
-
safeLog('error', lines.join('\n'));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function reviewSummary(comments: ReviewComment[]): void {
|
|
123
|
-
const counts = {
|
|
124
|
-
critical: 0,
|
|
125
|
-
important: 0,
|
|
126
|
-
minor: 0,
|
|
127
|
-
noise: 0,
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
for (const comment of comments) {
|
|
131
|
-
counts[comment.severity] += 1;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
safeLog(
|
|
135
|
-
'log',
|
|
136
|
-
`${pc.bold('Review:')} ${String(counts.critical)} critical ${String(counts.important)} important ${String(counts.minor)} minor`,
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
for (const comment of comments) {
|
|
140
|
-
if (comment.severity === 'noise') {
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const location = comment.file ? ` (${comment.file}${comment.line ? `:${String(comment.line)}` : ''})` : '';
|
|
145
|
-
safeLog('log', `- ${comment.severity}${location}: ${comment.comment}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
package/src/lib/requirements.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import * as codex from './codex.js';
|
|
2
|
-
import * as gh from './gh.js';
|
|
3
|
-
|
|
4
|
-
export function requireAnthropicApiKey(): string {
|
|
5
|
-
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
6
|
-
if (!apiKey) {
|
|
7
|
-
throw new Error('ANTHROPIC_API_KEY is required');
|
|
8
|
-
}
|
|
9
|
-
return apiKey;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function requireCodexAvailable(): Promise<void> {
|
|
13
|
-
await codex.checkCodexAvailable();
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function requireGhAvailable(): Promise<void> {
|
|
17
|
-
await gh.checkGhAvailable();
|
|
18
|
-
}
|
package/src/lib/review-loop.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
|
|
3
|
-
import * as codex from './codex.js';
|
|
4
|
-
import type { ClaudeClient } from './claude.js';
|
|
5
|
-
import * as git from './git.js';
|
|
6
|
-
import * as logger from './logger.js';
|
|
7
|
-
import * as state from './state.js';
|
|
8
|
-
import type {
|
|
9
|
-
ArbiterResult,
|
|
10
|
-
ReviewComment,
|
|
11
|
-
StepState,
|
|
12
|
-
Task,
|
|
13
|
-
TaskStep,
|
|
14
|
-
VexdoConfig,
|
|
15
|
-
} from '../types/index.js';
|
|
16
|
-
|
|
17
|
-
export interface ReviewLoopOptions {
|
|
18
|
-
taskId: string;
|
|
19
|
-
task: Task;
|
|
20
|
-
step: TaskStep;
|
|
21
|
-
stepState: StepState;
|
|
22
|
-
projectRoot: string;
|
|
23
|
-
config: VexdoConfig;
|
|
24
|
-
claude: ClaudeClient;
|
|
25
|
-
dryRun?: boolean;
|
|
26
|
-
verbose?: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface ReviewLoopResult {
|
|
30
|
-
decision: 'submit' | 'escalate';
|
|
31
|
-
finalIteration: number;
|
|
32
|
-
lastReviewComments: ReviewComment[];
|
|
33
|
-
lastArbiterResult: ArbiterResult;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function runReviewLoop(opts: ReviewLoopOptions): Promise<ReviewLoopResult> {
|
|
37
|
-
if (opts.dryRun) {
|
|
38
|
-
logger.info(`[dry-run] Would run review loop for service ${opts.step.service}`);
|
|
39
|
-
return {
|
|
40
|
-
decision: 'submit',
|
|
41
|
-
finalIteration: opts.stepState.iteration,
|
|
42
|
-
lastReviewComments: [],
|
|
43
|
-
lastArbiterResult: {
|
|
44
|
-
decision: 'submit',
|
|
45
|
-
reasoning: 'Dry run: skipped reviewer and arbiter calls.',
|
|
46
|
-
summary: 'Dry run mode; submitting without external calls.',
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const serviceConfig = opts.config.services.find((service) => service.name === opts.step.service);
|
|
52
|
-
if (!serviceConfig) {
|
|
53
|
-
throw new Error(`Unknown service in step: ${opts.step.service}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const serviceRoot = path.resolve(opts.projectRoot, serviceConfig.path);
|
|
57
|
-
let iteration = opts.stepState.iteration;
|
|
58
|
-
|
|
59
|
-
for (;;) {
|
|
60
|
-
logger.iteration(iteration + 1, opts.config.review.max_iterations);
|
|
61
|
-
|
|
62
|
-
const diff = await git.getDiff(serviceRoot);
|
|
63
|
-
if (!diff.trim()) {
|
|
64
|
-
return {
|
|
65
|
-
decision: 'submit',
|
|
66
|
-
finalIteration: iteration,
|
|
67
|
-
lastReviewComments: [],
|
|
68
|
-
lastArbiterResult: {
|
|
69
|
-
decision: 'submit',
|
|
70
|
-
reasoning: 'No changes in git diff for service directory.',
|
|
71
|
-
summary: 'No diff detected, nothing to review.',
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const review = await opts.claude.runReviewer({
|
|
77
|
-
spec: opts.step.spec,
|
|
78
|
-
diff,
|
|
79
|
-
model: opts.config.review.model,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
logger.reviewSummary(review.comments);
|
|
83
|
-
|
|
84
|
-
const arbiter = await opts.claude.runArbiter({
|
|
85
|
-
spec: opts.step.spec,
|
|
86
|
-
diff,
|
|
87
|
-
reviewComments: review.comments,
|
|
88
|
-
model: opts.config.review.model,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
state.saveIterationLog(opts.projectRoot, opts.taskId, opts.step.service, iteration, {
|
|
92
|
-
diff,
|
|
93
|
-
review,
|
|
94
|
-
arbiter,
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
opts.stepState.lastReviewComments = review.comments;
|
|
98
|
-
opts.stepState.lastArbiterResult = arbiter;
|
|
99
|
-
|
|
100
|
-
if (arbiter.decision === 'submit') {
|
|
101
|
-
return {
|
|
102
|
-
decision: 'submit',
|
|
103
|
-
finalIteration: iteration,
|
|
104
|
-
lastReviewComments: review.comments,
|
|
105
|
-
lastArbiterResult: arbiter,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (arbiter.decision === 'escalate') {
|
|
110
|
-
return {
|
|
111
|
-
decision: 'escalate',
|
|
112
|
-
finalIteration: iteration,
|
|
113
|
-
lastReviewComments: review.comments,
|
|
114
|
-
lastArbiterResult: arbiter,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (iteration >= opts.config.review.max_iterations) {
|
|
119
|
-
return {
|
|
120
|
-
decision: 'escalate',
|
|
121
|
-
finalIteration: iteration,
|
|
122
|
-
lastReviewComments: review.comments,
|
|
123
|
-
lastArbiterResult: {
|
|
124
|
-
decision: 'escalate',
|
|
125
|
-
reasoning: 'Max review iterations reached while arbiter still requested fixes.',
|
|
126
|
-
summary: 'Escalated because maximum iterations were exhausted.',
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (!arbiter.feedback_for_codex) {
|
|
132
|
-
return {
|
|
133
|
-
decision: 'escalate',
|
|
134
|
-
finalIteration: iteration,
|
|
135
|
-
lastReviewComments: review.comments,
|
|
136
|
-
lastArbiterResult: {
|
|
137
|
-
decision: 'escalate',
|
|
138
|
-
reasoning: 'Arbiter returned fix decision without feedback_for_codex.',
|
|
139
|
-
summary: 'Escalated because fix instructions were missing.',
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
await codex.exec({
|
|
145
|
-
spec: arbiter.feedback_for_codex,
|
|
146
|
-
model: opts.config.codex.model,
|
|
147
|
-
cwd: serviceRoot,
|
|
148
|
-
verbose: opts.verbose,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
iteration += 1;
|
|
152
|
-
opts.stepState.iteration = iteration;
|
|
153
|
-
}
|
|
154
|
-
}
|
package/src/lib/state.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import type { ArbiterResult, ReviewResult, StepState, VexdoState } from '../types/index.js';
|
|
5
|
-
|
|
6
|
-
function nowIso(): string {
|
|
7
|
-
return new Date().toISOString();
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function getStateDir(projectRoot: string): string {
|
|
11
|
-
return path.join(projectRoot, '.vexdo');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function getStatePath(projectRoot: string): string {
|
|
15
|
-
return path.join(getStateDir(projectRoot), 'state.json');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function getLogsDir(projectRoot: string, taskId: string): string {
|
|
19
|
-
return path.join(getStateDir(projectRoot), 'logs', taskId);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function ensureLogsDir(projectRoot: string, taskId: string): string {
|
|
23
|
-
const logsDir = getLogsDir(projectRoot, taskId);
|
|
24
|
-
fs.mkdirSync(logsDir, { recursive: true });
|
|
25
|
-
return logsDir;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function loadState(projectRoot: string): VexdoState | null {
|
|
29
|
-
const statePath = getStatePath(projectRoot);
|
|
30
|
-
if (!fs.existsSync(statePath)) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const raw = fs.readFileSync(statePath, 'utf8');
|
|
35
|
-
try {
|
|
36
|
-
return JSON.parse(raw) as VexdoState;
|
|
37
|
-
} catch (error: unknown) {
|
|
38
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
-
throw new Error(`Corrupt state file at ${statePath}: ${message}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function saveState(projectRoot: string, state: VexdoState): void {
|
|
44
|
-
const stateDir = getStateDir(projectRoot);
|
|
45
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
46
|
-
|
|
47
|
-
const nextState: VexdoState = {
|
|
48
|
-
...state,
|
|
49
|
-
updatedAt: nowIso(),
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
fs.writeFileSync(getStatePath(projectRoot), JSON.stringify(nextState, null, 2) + '\n', 'utf8');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function clearState(projectRoot: string): void {
|
|
56
|
-
const statePath = getStatePath(projectRoot);
|
|
57
|
-
if (fs.existsSync(statePath)) {
|
|
58
|
-
fs.rmSync(statePath);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function hasActiveTask(projectRoot: string): boolean {
|
|
63
|
-
const state = loadState(projectRoot);
|
|
64
|
-
return state?.status === 'in_progress' || state?.status === 'review';
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function createState(
|
|
68
|
-
taskId: string,
|
|
69
|
-
taskTitle: string,
|
|
70
|
-
taskPath: string,
|
|
71
|
-
steps: StepState[],
|
|
72
|
-
): VexdoState {
|
|
73
|
-
const timestamp = nowIso();
|
|
74
|
-
return {
|
|
75
|
-
taskId,
|
|
76
|
-
taskTitle,
|
|
77
|
-
taskPath,
|
|
78
|
-
status: 'in_progress',
|
|
79
|
-
steps: [...steps],
|
|
80
|
-
startedAt: timestamp,
|
|
81
|
-
updatedAt: timestamp,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function updateStep(
|
|
86
|
-
state: VexdoState,
|
|
87
|
-
service: string,
|
|
88
|
-
updates: Partial<Omit<StepState, 'service'>>,
|
|
89
|
-
): VexdoState {
|
|
90
|
-
return {
|
|
91
|
-
...state,
|
|
92
|
-
steps: state.steps.map((step) => {
|
|
93
|
-
if (step.service !== service) {
|
|
94
|
-
return step;
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
...step,
|
|
98
|
-
...updates,
|
|
99
|
-
};
|
|
100
|
-
}),
|
|
101
|
-
updatedAt: nowIso(),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function saveIterationLog(
|
|
106
|
-
projectRoot: string,
|
|
107
|
-
taskId: string,
|
|
108
|
-
service: string,
|
|
109
|
-
iteration: number,
|
|
110
|
-
payload: { diff: string; review: ReviewResult; arbiter: ArbiterResult },
|
|
111
|
-
): void {
|
|
112
|
-
const logsDir = ensureLogsDir(projectRoot, taskId);
|
|
113
|
-
const base = `${service}-iteration-${String(iteration)}`;
|
|
114
|
-
fs.writeFileSync(path.join(logsDir, `${base}-diff.txt`), payload.diff, 'utf8');
|
|
115
|
-
fs.writeFileSync(path.join(logsDir, `${base}-review.json`), JSON.stringify(payload.review, null, 2) + '\n', 'utf8');
|
|
116
|
-
fs.writeFileSync(
|
|
117
|
-
path.join(logsDir, `${base}-arbiter.json`),
|
|
118
|
-
JSON.stringify(payload.arbiter, null, 2) + '\n',
|
|
119
|
-
'utf8',
|
|
120
|
-
);
|
|
121
|
-
}
|
package/src/lib/submit-task.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import * as gh from './gh.js';
|
|
5
|
-
import * as logger from './logger.js';
|
|
6
|
-
import { clearState, saveState } from './state.js';
|
|
7
|
-
import { ensureTaskDirectory, moveTaskFileAtomically } from './tasks.js';
|
|
8
|
-
import type { VexdoConfig, VexdoState } from '../types/index.js';
|
|
9
|
-
|
|
10
|
-
export async function submitActiveTask(projectRoot: string, config: VexdoConfig, state: VexdoState): Promise<void> {
|
|
11
|
-
for (const step of state.steps) {
|
|
12
|
-
if (step.status !== 'done' && step.status !== 'in_progress') {
|
|
13
|
-
continue;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const service = config.services.find((item) => item.name === step.service);
|
|
17
|
-
if (!service) {
|
|
18
|
-
throw new Error(`Unknown service in state: ${step.service}`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const servicePath = path.resolve(projectRoot, service.path);
|
|
22
|
-
const body = `Task: ${state.taskId}\nService: ${step.service}`;
|
|
23
|
-
const url = await gh.createPr({
|
|
24
|
-
title: `${state.taskTitle} [${step.service}]`,
|
|
25
|
-
body,
|
|
26
|
-
base: 'main',
|
|
27
|
-
cwd: servicePath,
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
logger.success(`PR created: ${url}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
state.status = 'done';
|
|
34
|
-
saveState(projectRoot, state);
|
|
35
|
-
|
|
36
|
-
const doneDir = ensureTaskDirectory(projectRoot, 'done');
|
|
37
|
-
if (fs.existsSync(state.taskPath)) {
|
|
38
|
-
state.taskPath = moveTaskFileAtomically(state.taskPath, doneDir);
|
|
39
|
-
saveState(projectRoot, state);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
clearState(projectRoot);
|
|
43
|
-
}
|
package/src/lib/tasks.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import { load as parseYaml } from 'js-yaml';
|
|
5
|
-
|
|
6
|
-
import type { StepState, Task, TaskStep, VexdoConfig } from '../types/index.js';
|
|
7
|
-
|
|
8
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
9
|
-
return typeof value === 'object' && value !== null;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function requireString(value: unknown, field: string): string {
|
|
13
|
-
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
14
|
-
throw new Error(`${field} must be a non-empty string`);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function parseTaskStep(value: unknown, index: number, config: VexdoConfig): TaskStep {
|
|
21
|
-
if (!isRecord(value)) {
|
|
22
|
-
throw new Error(`steps[${String(index)}] must be an object`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const service = requireString(value.service, `steps[${String(index)}].service`);
|
|
26
|
-
const spec = requireString(value.spec, `steps[${String(index)}].spec`);
|
|
27
|
-
|
|
28
|
-
if (!config.services.some((item) => item.name === service)) {
|
|
29
|
-
throw new Error(`steps[${String(index)}].service references unknown service '${service}'`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const dependsOnRaw = value.depends_on;
|
|
33
|
-
let depends_on: string[] | undefined;
|
|
34
|
-
|
|
35
|
-
if (dependsOnRaw !== undefined) {
|
|
36
|
-
if (!Array.isArray(dependsOnRaw) || !dependsOnRaw.every((dep) => typeof dep === 'string' && dep.trim().length > 0)) {
|
|
37
|
-
throw new Error(`steps[${String(index)}].depends_on must be an array of non-empty strings`);
|
|
38
|
-
}
|
|
39
|
-
depends_on = dependsOnRaw;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
service,
|
|
44
|
-
spec,
|
|
45
|
-
depends_on,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function loadAndValidateTask(taskPath: string, config: VexdoConfig): Task {
|
|
50
|
-
const raw = fs.readFileSync(taskPath, 'utf8');
|
|
51
|
-
|
|
52
|
-
let parsed: unknown;
|
|
53
|
-
try {
|
|
54
|
-
parsed = parseYaml(raw);
|
|
55
|
-
} catch (error: unknown) {
|
|
56
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
57
|
-
throw new Error(`Invalid task YAML: ${message}`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (!isRecord(parsed)) {
|
|
61
|
-
throw new Error('task must be a YAML object');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const id = requireString(parsed.id, 'id');
|
|
65
|
-
const title = requireString(parsed.title, 'title');
|
|
66
|
-
|
|
67
|
-
if (!Array.isArray(parsed.steps) || parsed.steps.length === 0) {
|
|
68
|
-
throw new Error('steps must be a non-empty array');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const steps = parsed.steps.map((step, index) => parseTaskStep(step, index, config));
|
|
72
|
-
|
|
73
|
-
return { id, title, steps };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function buildInitialStepState(task: Task): StepState[] {
|
|
77
|
-
return task.steps.map((step) => ({
|
|
78
|
-
service: step.service,
|
|
79
|
-
status: 'pending',
|
|
80
|
-
iteration: 0,
|
|
81
|
-
}));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function ensureTaskDirectory(projectRoot: string, taskState: 'backlog' | 'in_progress' | 'review' | 'done' | 'blocked'): string {
|
|
85
|
-
const dir = path.join(projectRoot, 'tasks', taskState);
|
|
86
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
87
|
-
return dir;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function moveTaskFileAtomically(taskPath: string, destinationDir: string): string {
|
|
91
|
-
const destinationPath = path.join(destinationDir, path.basename(taskPath));
|
|
92
|
-
fs.renameSync(taskPath, destinationPath);
|
|
93
|
-
return destinationPath;
|
|
94
|
-
}
|
package/src/prompts/arbiter.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export const ARBITER_SYSTEM_PROMPT = `You are a technical arbiter receiving: spec, diff, and reviewer comments.
|
|
2
|
-
|
|
3
|
-
Rules:
|
|
4
|
-
- Treat the spec as the single source of truth, not reviewer opinion.
|
|
5
|
-
- Explicitly check each reviewer comment against the spec:
|
|
6
|
-
- If a comment correctly identifies a spec violation, include it in the decision.
|
|
7
|
-
- If a comment conflicts with the spec or invents requirements, flag it and escalate.
|
|
8
|
-
- If comments are ambiguous, escalate.
|
|
9
|
-
- Never resolve architectural decisions autonomously; escalate instead.
|
|
10
|
-
- When decision is fix, feedback_for_codex must be concrete and actionable: what to change, where, and how.
|
|
11
|
-
|
|
12
|
-
Decision rules:
|
|
13
|
-
- submit: no critical/important comments that reflect real spec violations.
|
|
14
|
-
- fix: clear spec violations with actionable fixes; include feedback_for_codex.
|
|
15
|
-
- escalate: any reviewer/spec conflict, architectural ambiguity, or if max-iteration-like uncertainty would require escalation.
|
|
16
|
-
|
|
17
|
-
Output requirements:
|
|
18
|
-
- Output ONLY valid JSON with schema:
|
|
19
|
-
{ "decision", "reasoning", "feedback_for_codex", "summary" }
|
|
20
|
-
- feedback_for_codex is required when decision="fix" and must be omitted otherwise.
|
|
21
|
-
- Never output prose outside the JSON.`;
|
package/src/prompts/reviewer.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export const REVIEWER_SYSTEM_PROMPT = `You are a strict code reviewer evaluating a git diff against a provided spec.
|
|
2
|
-
|
|
3
|
-
Rules:
|
|
4
|
-
- Treat the spec (acceptance criteria + architectural constraints) as the ONLY source of truth.
|
|
5
|
-
- Never invent requirements that are not explicitly present in the spec.
|
|
6
|
-
- Evaluate whether the diff violates the spec; distinguish real violations from stylistic preferences.
|
|
7
|
-
- For each issue, provide exact file and line number when visible in the diff.
|
|
8
|
-
- Severity definitions (strict):
|
|
9
|
-
- critical: breaks an acceptance criterion or architectural constraint.
|
|
10
|
-
- important: likely to cause bugs or maintenance issues directly related to the spec.
|
|
11
|
-
- minor: code quality issue not blocking the spec.
|
|
12
|
-
- noise: style/preference, spec-neutral.
|
|
13
|
-
- If the diff fully satisfies the spec, return no comments.
|
|
14
|
-
|
|
15
|
-
Output requirements:
|
|
16
|
-
- Output ONLY valid JSON.
|
|
17
|
-
- JSON schema:
|
|
18
|
-
{ "comments": [ { "severity", "file", "line", "comment", "suggestion" } ] }
|
|
19
|
-
- If no issues: { "comments": [] }
|
|
20
|
-
- Never output prose outside the JSON.`;
|