sidecar-cli 0.1.2 → 0.1.3-beta.1
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/dist/cli.js +446 -56
- package/dist/lib/paths.js +3 -0
- package/dist/lib/ui.js +4 -1
- package/dist/prompts/prompt-compiler.js +88 -0
- package/dist/prompts/prompt-service.js +35 -0
- package/dist/runners/claude-runner.js +38 -0
- package/dist/runners/codex-runner.js +38 -0
- package/dist/runners/config.js +39 -0
- package/dist/runners/factory.js +10 -0
- package/dist/runners/runner-adapter.js +1 -0
- package/dist/runs/run-record.js +97 -0
- package/dist/runs/run-repository.js +99 -0
- package/dist/runs/run-service.js +27 -0
- package/dist/services/capabilities-service.js +124 -9
- package/dist/services/run-orchestrator-service.js +59 -0
- package/dist/services/run-review-service.js +76 -0
- package/dist/services/task-orchestration-service.js +94 -0
- package/dist/tasks/task-packet.js +132 -0
- package/dist/tasks/task-repository.js +78 -0
- package/dist/tasks/task-service.js +79 -0
- package/package.json +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { nowIso } from '../lib/format.js';
|
|
4
|
+
import { getSidecarPaths } from '../lib/paths.js';
|
|
5
|
+
function section(title, lines) {
|
|
6
|
+
return [`## ${title}`, ...lines, ''].join('\n');
|
|
7
|
+
}
|
|
8
|
+
function bullets(items, empty = '- none') {
|
|
9
|
+
if (items.length === 0)
|
|
10
|
+
return [empty];
|
|
11
|
+
return items.map((item) => `- ${item}`);
|
|
12
|
+
}
|
|
13
|
+
function finalResponseFormat(runner) {
|
|
14
|
+
if (runner === 'codex') {
|
|
15
|
+
return [
|
|
16
|
+
'- Start with a one-line outcome summary.',
|
|
17
|
+
'- List files changed with concise reasons.',
|
|
18
|
+
'- Include validation commands run and their results.',
|
|
19
|
+
'- Note risks, blockers, or follow-up tasks.',
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
return [
|
|
23
|
+
'- Use a brief plan -> implementation -> summary structure.',
|
|
24
|
+
'- Call out assumptions and tradeoffs explicitly.',
|
|
25
|
+
'- List changed files and validation results.',
|
|
26
|
+
'- End with remaining risks and next steps if any.',
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
function runnerGuidance(runner) {
|
|
30
|
+
if (runner === 'codex') {
|
|
31
|
+
return [
|
|
32
|
+
'Work directly in this repository and keep changes tightly scoped to the task.',
|
|
33
|
+
'Prefer existing project helpers and patterns over introducing new abstractions.',
|
|
34
|
+
'Keep final reporting concise and implementation-focused.',
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
return [
|
|
38
|
+
'Begin with a short plan, then execute changes in small coherent steps.',
|
|
39
|
+
'Explain implementation choices and tradeoffs briefly as you go.',
|
|
40
|
+
'Provide a clear summary with validation and follow-up notes at the end.',
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
export function compilePromptMarkdown(input) {
|
|
44
|
+
const { task, run, runner, agentRole, linkedContext } = input;
|
|
45
|
+
const lines = [];
|
|
46
|
+
lines.push('# Sidecar Execution Brief');
|
|
47
|
+
lines.push('');
|
|
48
|
+
lines.push(`Runner: ${runner}`);
|
|
49
|
+
lines.push(`Agent role: ${agentRole}`);
|
|
50
|
+
lines.push(`Run id: ${run.run_id}`);
|
|
51
|
+
lines.push(`Task id: ${task.task_id}`);
|
|
52
|
+
lines.push(`Compiled at: ${nowIso()}`);
|
|
53
|
+
lines.push('');
|
|
54
|
+
lines.push(section('Task', [
|
|
55
|
+
`- ${task.title}`,
|
|
56
|
+
`- Type: ${task.type}`,
|
|
57
|
+
`- Priority: ${task.priority}`,
|
|
58
|
+
`- Status: ${task.status}`,
|
|
59
|
+
]));
|
|
60
|
+
lines.push(section('Objective', [task.goal]));
|
|
61
|
+
lines.push(section('Why this matters', [task.summary]));
|
|
62
|
+
lines.push(section('In scope', bullets(task.scope.in_scope)));
|
|
63
|
+
lines.push(section('Out of scope', bullets(task.scope.out_of_scope)));
|
|
64
|
+
lines.push(section('Read these first', bullets(task.implementation.files_to_read)));
|
|
65
|
+
lines.push(section('Avoid changing', bullets(task.implementation.files_to_avoid)));
|
|
66
|
+
const relatedDecisions = linkedContext?.related_decisions ?? task.context.related_decisions;
|
|
67
|
+
const relatedNotes = linkedContext?.related_notes ?? task.context.related_notes;
|
|
68
|
+
lines.push(section('Linked context', [
|
|
69
|
+
...bullets(relatedDecisions, '- no related decisions'),
|
|
70
|
+
...bullets(relatedNotes, '- no related notes'),
|
|
71
|
+
]));
|
|
72
|
+
lines.push(section('Constraints', [
|
|
73
|
+
...bullets(task.constraints.technical, '- no technical constraints'),
|
|
74
|
+
...bullets(task.constraints.design, '- no design constraints'),
|
|
75
|
+
]));
|
|
76
|
+
lines.push(section('Validation', bullets(task.execution.commands.validation)));
|
|
77
|
+
lines.push(section('Definition of done', bullets(task.definition_of_done)));
|
|
78
|
+
lines.push(section('Runner guidance', runnerGuidance(runner)));
|
|
79
|
+
lines.push(section('Final response format', finalResponseFormat(runner)));
|
|
80
|
+
return `${lines.join('\n').trim()}\n`;
|
|
81
|
+
}
|
|
82
|
+
export function saveCompiledPrompt(rootPath, runId, markdown) {
|
|
83
|
+
const promptsPath = getSidecarPaths(rootPath).promptsPath;
|
|
84
|
+
fs.mkdirSync(promptsPath, { recursive: true });
|
|
85
|
+
const promptPath = path.join(promptsPath, `${runId}.md`);
|
|
86
|
+
fs.writeFileSync(promptPath, markdown, 'utf8');
|
|
87
|
+
return promptPath;
|
|
88
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createRunRecordEntry, updateRunRecordEntry } from '../runs/run-service.js';
|
|
2
|
+
import { getTaskPacket } from '../tasks/task-service.js';
|
|
3
|
+
import { compilePromptMarkdown, saveCompiledPrompt } from './prompt-compiler.js';
|
|
4
|
+
export function compileTaskPrompt(input) {
|
|
5
|
+
const task = getTaskPacket(input.rootPath, input.taskId);
|
|
6
|
+
const created = createRunRecordEntry(input.rootPath, {
|
|
7
|
+
task_id: task.task_id,
|
|
8
|
+
runner_type: input.runner,
|
|
9
|
+
agent_role: input.agentRole,
|
|
10
|
+
status: 'preparing',
|
|
11
|
+
branch: task.tracking.branch,
|
|
12
|
+
worktree: task.tracking.worktree,
|
|
13
|
+
});
|
|
14
|
+
const promptMarkdown = compilePromptMarkdown({
|
|
15
|
+
task,
|
|
16
|
+
run: created.run,
|
|
17
|
+
runner: input.runner,
|
|
18
|
+
agentRole: input.agentRole,
|
|
19
|
+
linkedContext: input.linkedContext,
|
|
20
|
+
});
|
|
21
|
+
const promptPath = saveCompiledPrompt(input.rootPath, created.run.run_id, promptMarkdown);
|
|
22
|
+
updateRunRecordEntry(input.rootPath, created.run.run_id, {
|
|
23
|
+
status: 'queued',
|
|
24
|
+
prompt_path: promptPath,
|
|
25
|
+
summary: `Compiled prompt for task ${task.task_id}`,
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
run_id: created.run.run_id,
|
|
29
|
+
task_id: task.task_id,
|
|
30
|
+
runner_type: input.runner,
|
|
31
|
+
agent_role: input.agentRole,
|
|
32
|
+
prompt_path: promptPath,
|
|
33
|
+
prompt_markdown: promptMarkdown,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class ClaudeRunnerAdapter {
|
|
2
|
+
runner = 'claude';
|
|
3
|
+
prepare(input) {
|
|
4
|
+
const args = ['run', '--prompt-file', input.promptPath, '--role', input.agentRole];
|
|
5
|
+
return {
|
|
6
|
+
command: 'claude',
|
|
7
|
+
args,
|
|
8
|
+
shellLine: `claude ${args.join(' ')}`,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
execute(input) {
|
|
12
|
+
if (input.dryRun) {
|
|
13
|
+
return {
|
|
14
|
+
ok: true,
|
|
15
|
+
executed: false,
|
|
16
|
+
exitCode: 0,
|
|
17
|
+
summary: 'Dry run: prepared Claude command only.',
|
|
18
|
+
commandsRun: [input.prepared.shellLine],
|
|
19
|
+
validationResults: ['dry-run'],
|
|
20
|
+
blockers: [],
|
|
21
|
+
followUps: [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
ok: true,
|
|
26
|
+
executed: false,
|
|
27
|
+
exitCode: 0,
|
|
28
|
+
summary: 'Prepared Claude command. Live execution is placeholder behavior in v1.',
|
|
29
|
+
commandsRun: [input.prepared.shellLine],
|
|
30
|
+
validationResults: ['runner execute placeholder'],
|
|
31
|
+
blockers: [],
|
|
32
|
+
followUps: ['Integrate real Claude command execution in runner adapter.'],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
collectResult(result) {
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class CodexRunnerAdapter {
|
|
2
|
+
runner = 'codex';
|
|
3
|
+
prepare(input) {
|
|
4
|
+
const args = ['run', '--prompt-file', input.promptPath, '--role', input.agentRole];
|
|
5
|
+
return {
|
|
6
|
+
command: 'codex',
|
|
7
|
+
args,
|
|
8
|
+
shellLine: `codex ${args.join(' ')}`,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
execute(input) {
|
|
12
|
+
if (input.dryRun) {
|
|
13
|
+
return {
|
|
14
|
+
ok: true,
|
|
15
|
+
executed: false,
|
|
16
|
+
exitCode: 0,
|
|
17
|
+
summary: 'Dry run: prepared Codex command only.',
|
|
18
|
+
commandsRun: [input.prepared.shellLine],
|
|
19
|
+
validationResults: ['dry-run'],
|
|
20
|
+
blockers: [],
|
|
21
|
+
followUps: [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
ok: true,
|
|
26
|
+
executed: false,
|
|
27
|
+
exitCode: 0,
|
|
28
|
+
summary: 'Prepared Codex command. Live execution is placeholder behavior in v1.',
|
|
29
|
+
commandsRun: [input.prepared.shellLine],
|
|
30
|
+
validationResults: ['runner execute placeholder'],
|
|
31
|
+
blockers: [],
|
|
32
|
+
followUps: ['Integrate real Codex command execution in runner adapter.'],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
collectResult(result) {
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { getSidecarPaths } from '../lib/paths.js';
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
default_runner: 'codex',
|
|
5
|
+
preferred_runners: ['codex', 'claude'],
|
|
6
|
+
default_agent_role: 'builder-app',
|
|
7
|
+
};
|
|
8
|
+
export function loadRunnerPreferences(rootPath) {
|
|
9
|
+
const prefsPath = getSidecarPaths(rootPath).preferencesPath;
|
|
10
|
+
if (!fs.existsSync(prefsPath))
|
|
11
|
+
return DEFAULTS;
|
|
12
|
+
try {
|
|
13
|
+
const raw = JSON.parse(fs.readFileSync(prefsPath, 'utf8'));
|
|
14
|
+
const defaultRunner = raw.runner?.defaultRunner;
|
|
15
|
+
const preferredRunners = raw.runner?.preferredRunners;
|
|
16
|
+
const defaultAgentRole = raw.runner?.defaultAgentRole;
|
|
17
|
+
return {
|
|
18
|
+
default_runner: defaultRunner === 'codex' || defaultRunner === 'claude' ? defaultRunner : DEFAULTS.default_runner,
|
|
19
|
+
preferred_runners: Array.isArray(preferredRunners) && preferredRunners.every((r) => r === 'codex' || r === 'claude')
|
|
20
|
+
? preferredRunners
|
|
21
|
+
: DEFAULTS.preferred_runners,
|
|
22
|
+
default_agent_role: (() => {
|
|
23
|
+
if (defaultAgentRole === 'builder')
|
|
24
|
+
return 'builder-app';
|
|
25
|
+
if (defaultAgentRole === 'planner' ||
|
|
26
|
+
defaultAgentRole === 'builder-ui' ||
|
|
27
|
+
defaultAgentRole === 'builder-app' ||
|
|
28
|
+
defaultAgentRole === 'reviewer' ||
|
|
29
|
+
defaultAgentRole === 'tester') {
|
|
30
|
+
return defaultAgentRole;
|
|
31
|
+
}
|
|
32
|
+
return DEFAULTS.default_agent_role;
|
|
33
|
+
})(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return DEFAULTS;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SidecarError } from '../lib/errors.js';
|
|
2
|
+
import { CodexRunnerAdapter } from './codex-runner.js';
|
|
3
|
+
import { ClaudeRunnerAdapter } from './claude-runner.js';
|
|
4
|
+
export function getRunnerAdapter(runner) {
|
|
5
|
+
if (runner === 'codex')
|
|
6
|
+
return new CodexRunnerAdapter();
|
|
7
|
+
if (runner === 'claude')
|
|
8
|
+
return new ClaudeRunnerAdapter();
|
|
9
|
+
throw new SidecarError(`Unsupported runner: ${runner}`);
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { nowIso } from '../lib/format.js';
|
|
3
|
+
export const RUN_RECORD_VERSION = '1.0';
|
|
4
|
+
export const runIdSchema = z.string().regex(/^R-\d{3,}$/, 'Run id must look like R-001');
|
|
5
|
+
export const runStatusSchema = z.enum(['queued', 'preparing', 'running', 'review', 'blocked', 'completed', 'failed']);
|
|
6
|
+
export const runnerTypeSchema = z.enum(['codex', 'claude']);
|
|
7
|
+
export const runReviewStateSchema = z.enum(['pending', 'approved', 'needs_changes', 'blocked', 'merged']);
|
|
8
|
+
export const runRecordSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
version: z.string().default(RUN_RECORD_VERSION),
|
|
11
|
+
run_id: runIdSchema,
|
|
12
|
+
task_id: z.string().regex(/^T-\d{3,}$/, 'task_id must look like T-001'),
|
|
13
|
+
runner_type: runnerTypeSchema,
|
|
14
|
+
agent_role: z.string().min(1, 'agent_role is required'),
|
|
15
|
+
status: runStatusSchema,
|
|
16
|
+
branch: z.string().default(''),
|
|
17
|
+
worktree: z.string().default(''),
|
|
18
|
+
prompt_path: z.string().default(''),
|
|
19
|
+
started_at: z.string().datetime({ offset: true }),
|
|
20
|
+
completed_at: z.string().datetime({ offset: true }).nullable().default(null),
|
|
21
|
+
summary: z.string().default(''),
|
|
22
|
+
changed_files: z.array(z.string()).default([]),
|
|
23
|
+
commands_run: z.array(z.string()).default([]),
|
|
24
|
+
validation_results: z.array(z.string()).default([]),
|
|
25
|
+
blockers: z.array(z.string()).default([]),
|
|
26
|
+
follow_ups: z.array(z.string()).default([]),
|
|
27
|
+
review_state: runReviewStateSchema.default('pending'),
|
|
28
|
+
reviewed_at: z.string().datetime({ offset: true }).nullable().default(null),
|
|
29
|
+
reviewed_by: z.string().default(''),
|
|
30
|
+
review_note: z.string().default(''),
|
|
31
|
+
})
|
|
32
|
+
.strict();
|
|
33
|
+
export const runRecordCreateInputSchema = runRecordSchema
|
|
34
|
+
.omit({ run_id: true, version: true })
|
|
35
|
+
.partial({
|
|
36
|
+
status: true,
|
|
37
|
+
branch: true,
|
|
38
|
+
worktree: true,
|
|
39
|
+
prompt_path: true,
|
|
40
|
+
started_at: true,
|
|
41
|
+
completed_at: true,
|
|
42
|
+
summary: true,
|
|
43
|
+
changed_files: true,
|
|
44
|
+
commands_run: true,
|
|
45
|
+
validation_results: true,
|
|
46
|
+
blockers: true,
|
|
47
|
+
follow_ups: true,
|
|
48
|
+
review_state: true,
|
|
49
|
+
reviewed_at: true,
|
|
50
|
+
reviewed_by: true,
|
|
51
|
+
review_note: true,
|
|
52
|
+
});
|
|
53
|
+
export const runRecordUpdateInputSchema = z
|
|
54
|
+
.object({
|
|
55
|
+
status: runStatusSchema.optional(),
|
|
56
|
+
branch: z.string().optional(),
|
|
57
|
+
worktree: z.string().optional(),
|
|
58
|
+
prompt_path: z.string().optional(),
|
|
59
|
+
completed_at: z.string().datetime({ offset: true }).nullable().optional(),
|
|
60
|
+
summary: z.string().optional(),
|
|
61
|
+
changed_files: z.array(z.string()).optional(),
|
|
62
|
+
commands_run: z.array(z.string()).optional(),
|
|
63
|
+
validation_results: z.array(z.string()).optional(),
|
|
64
|
+
blockers: z.array(z.string()).optional(),
|
|
65
|
+
follow_ups: z.array(z.string()).optional(),
|
|
66
|
+
review_state: runReviewStateSchema.optional(),
|
|
67
|
+
reviewed_at: z.string().datetime({ offset: true }).nullable().optional(),
|
|
68
|
+
reviewed_by: z.string().optional(),
|
|
69
|
+
review_note: z.string().optional(),
|
|
70
|
+
})
|
|
71
|
+
.strict();
|
|
72
|
+
export function createRunRecord(runId, input) {
|
|
73
|
+
const normalized = {
|
|
74
|
+
version: RUN_RECORD_VERSION,
|
|
75
|
+
run_id: runId,
|
|
76
|
+
task_id: input.task_id,
|
|
77
|
+
runner_type: input.runner_type,
|
|
78
|
+
agent_role: input.agent_role,
|
|
79
|
+
status: input.status ?? 'queued',
|
|
80
|
+
branch: input.branch ?? '',
|
|
81
|
+
worktree: input.worktree ?? '',
|
|
82
|
+
prompt_path: input.prompt_path ?? '',
|
|
83
|
+
started_at: input.started_at ?? nowIso(),
|
|
84
|
+
completed_at: input.completed_at ?? null,
|
|
85
|
+
summary: input.summary ?? '',
|
|
86
|
+
changed_files: input.changed_files ?? [],
|
|
87
|
+
commands_run: input.commands_run ?? [],
|
|
88
|
+
validation_results: input.validation_results ?? [],
|
|
89
|
+
blockers: input.blockers ?? [],
|
|
90
|
+
follow_ups: input.follow_ups ?? [],
|
|
91
|
+
review_state: input.review_state ?? 'pending',
|
|
92
|
+
reviewed_at: input.reviewed_at ?? null,
|
|
93
|
+
reviewed_by: input.reviewed_by ?? '',
|
|
94
|
+
review_note: input.review_note ?? '',
|
|
95
|
+
};
|
|
96
|
+
return runRecordSchema.parse(normalized);
|
|
97
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getSidecarPaths } from '../lib/paths.js';
|
|
4
|
+
import { SidecarError } from '../lib/errors.js';
|
|
5
|
+
import { stringifyJson } from '../lib/format.js';
|
|
6
|
+
import { createRunRecord, runRecordCreateInputSchema, runRecordSchema, runRecordUpdateInputSchema, } from './run-record.js';
|
|
7
|
+
function runFilePath(runsPath, runId) {
|
|
8
|
+
return path.join(runsPath, `${runId}.json`);
|
|
9
|
+
}
|
|
10
|
+
function parseRunOrdinal(runId) {
|
|
11
|
+
const match = /^R-(\d+)$/.exec(runId);
|
|
12
|
+
return match ? Number.parseInt(match[1], 10) : 0;
|
|
13
|
+
}
|
|
14
|
+
export class RunRecordRepository {
|
|
15
|
+
rootPath;
|
|
16
|
+
constructor(rootPath) {
|
|
17
|
+
this.rootPath = rootPath;
|
|
18
|
+
}
|
|
19
|
+
get runsPath() {
|
|
20
|
+
return getSidecarPaths(this.rootPath).runsPath;
|
|
21
|
+
}
|
|
22
|
+
ensureStorage() {
|
|
23
|
+
fs.mkdirSync(this.runsPath, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
generateNextRunId() {
|
|
26
|
+
this.ensureStorage();
|
|
27
|
+
const files = fs.readdirSync(this.runsPath, { withFileTypes: true });
|
|
28
|
+
let max = 0;
|
|
29
|
+
for (const file of files) {
|
|
30
|
+
if (!file.isFile() || !file.name.endsWith('.json'))
|
|
31
|
+
continue;
|
|
32
|
+
const id = file.name.slice(0, -'.json'.length);
|
|
33
|
+
max = Math.max(max, parseRunOrdinal(id));
|
|
34
|
+
}
|
|
35
|
+
return `R-${String(max + 1).padStart(3, '0')}`;
|
|
36
|
+
}
|
|
37
|
+
create(input) {
|
|
38
|
+
const parsed = runRecordCreateInputSchema.parse(input);
|
|
39
|
+
const runId = this.generateNextRunId();
|
|
40
|
+
const run = createRunRecord(runId, parsed);
|
|
41
|
+
const filePath = runFilePath(this.runsPath, runId);
|
|
42
|
+
fs.writeFileSync(filePath, `${stringifyJson(run)}\n`, 'utf8');
|
|
43
|
+
return { run, path: filePath };
|
|
44
|
+
}
|
|
45
|
+
update(runId, patch) {
|
|
46
|
+
const existing = this.get(runId);
|
|
47
|
+
const parsedPatch = runRecordUpdateInputSchema.parse(patch);
|
|
48
|
+
const merged = {
|
|
49
|
+
...existing,
|
|
50
|
+
...parsedPatch,
|
|
51
|
+
changed_files: parsedPatch.changed_files ?? existing.changed_files,
|
|
52
|
+
commands_run: parsedPatch.commands_run ?? existing.commands_run,
|
|
53
|
+
validation_results: parsedPatch.validation_results ?? existing.validation_results,
|
|
54
|
+
blockers: parsedPatch.blockers ?? existing.blockers,
|
|
55
|
+
follow_ups: parsedPatch.follow_ups ?? existing.follow_ups,
|
|
56
|
+
};
|
|
57
|
+
const validated = runRecordSchema.parse(merged);
|
|
58
|
+
const filePath = runFilePath(this.runsPath, runId);
|
|
59
|
+
fs.writeFileSync(filePath, `${stringifyJson(validated)}\n`, 'utf8');
|
|
60
|
+
return validated;
|
|
61
|
+
}
|
|
62
|
+
get(runId) {
|
|
63
|
+
const filePath = runFilePath(this.runsPath, runId);
|
|
64
|
+
if (!fs.existsSync(filePath)) {
|
|
65
|
+
throw new SidecarError(`Run not found: ${runId}`);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
69
|
+
return runRecordSchema.parse(raw);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
73
|
+
throw new SidecarError(`Invalid run record at ${filePath}: ${message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
list() {
|
|
77
|
+
this.ensureStorage();
|
|
78
|
+
const files = fs
|
|
79
|
+
.readdirSync(this.runsPath, { withFileTypes: true })
|
|
80
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
81
|
+
.map((entry) => path.join(this.runsPath, entry.name))
|
|
82
|
+
.sort();
|
|
83
|
+
const runs = [];
|
|
84
|
+
for (const filePath of files) {
|
|
85
|
+
try {
|
|
86
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
87
|
+
runs.push(runRecordSchema.parse(raw));
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
91
|
+
throw new SidecarError(`Invalid run record at ${filePath}: ${message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return runs;
|
|
95
|
+
}
|
|
96
|
+
listForTask(taskId) {
|
|
97
|
+
return this.list().filter((run) => run.task_id === taskId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { RunRecordRepository } from './run-repository.js';
|
|
2
|
+
export function createRunRecordEntry(rootPath, input) {
|
|
3
|
+
const repo = new RunRecordRepository(rootPath);
|
|
4
|
+
return repo.create(input);
|
|
5
|
+
}
|
|
6
|
+
export function updateRunRecordEntry(rootPath, runId, patch) {
|
|
7
|
+
const repo = new RunRecordRepository(rootPath);
|
|
8
|
+
return repo.update(runId, patch);
|
|
9
|
+
}
|
|
10
|
+
export function getRunRecord(rootPath, runId) {
|
|
11
|
+
const repo = new RunRecordRepository(rootPath);
|
|
12
|
+
return repo.get(runId);
|
|
13
|
+
}
|
|
14
|
+
export function listRunRecords(rootPath) {
|
|
15
|
+
const repo = new RunRecordRepository(rootPath);
|
|
16
|
+
return repo
|
|
17
|
+
.list()
|
|
18
|
+
.slice()
|
|
19
|
+
.sort((a, b) => b.started_at.localeCompare(a.started_at));
|
|
20
|
+
}
|
|
21
|
+
export function listRunRecordsForTask(rootPath, taskId) {
|
|
22
|
+
const repo = new RunRecordRepository(rootPath);
|
|
23
|
+
return repo
|
|
24
|
+
.listForTask(taskId)
|
|
25
|
+
.slice()
|
|
26
|
+
.sort((a, b) => b.started_at.localeCompare(a.started_at));
|
|
27
|
+
}
|
|
@@ -10,6 +10,10 @@ export function getCapabilitiesManifest(version) {
|
|
|
10
10
|
'event_ingest',
|
|
11
11
|
'export_json',
|
|
12
12
|
'export_jsonl_events',
|
|
13
|
+
'run_records_v1',
|
|
14
|
+
'prompt_compiler_v1',
|
|
15
|
+
'task_orchestration_v1',
|
|
16
|
+
'run_review_workflow_v1',
|
|
13
17
|
'optional_local_ui',
|
|
14
18
|
],
|
|
15
19
|
commands: [
|
|
@@ -168,25 +172,136 @@ export function getCapabilitiesManifest(version) {
|
|
|
168
172
|
options: [],
|
|
169
173
|
subcommands: [
|
|
170
174
|
{
|
|
171
|
-
name: '
|
|
172
|
-
description: 'Create
|
|
175
|
+
name: 'create',
|
|
176
|
+
description: 'Create a structured task packet',
|
|
173
177
|
json_output: true,
|
|
174
|
-
arguments: [
|
|
175
|
-
options: [
|
|
178
|
+
arguments: [],
|
|
179
|
+
options: [
|
|
180
|
+
'--title <title>',
|
|
181
|
+
'--summary <summary>',
|
|
182
|
+
'--goal <goal>',
|
|
183
|
+
'--type feature|bug|chore|research',
|
|
184
|
+
'--status draft|ready|queued|running|review|blocked|done',
|
|
185
|
+
'--priority low|medium|high',
|
|
186
|
+
'--dependencies <task-ids>',
|
|
187
|
+
'--tags <tags>',
|
|
188
|
+
'--target-areas <areas>',
|
|
189
|
+
'--scope-in <items>',
|
|
190
|
+
'--scope-out <items>',
|
|
191
|
+
'--related-decisions <items>',
|
|
192
|
+
'--related-notes <items>',
|
|
193
|
+
'--files-read <paths>',
|
|
194
|
+
'--files-avoid <paths>',
|
|
195
|
+
'--constraint-tech <items>',
|
|
196
|
+
'--constraint-design <items>',
|
|
197
|
+
'--validate-cmds <commands>',
|
|
198
|
+
'--dod <items>',
|
|
199
|
+
'--branch <name>',
|
|
200
|
+
'--worktree <path>',
|
|
201
|
+
'--json',
|
|
202
|
+
],
|
|
176
203
|
},
|
|
177
204
|
{
|
|
178
|
-
name: '
|
|
179
|
-
description: '
|
|
205
|
+
name: 'show',
|
|
206
|
+
description: 'Show a task packet',
|
|
180
207
|
json_output: true,
|
|
181
208
|
arguments: ['<task-id>'],
|
|
182
|
-
options: ['--
|
|
209
|
+
options: ['--json'],
|
|
183
210
|
},
|
|
184
211
|
{
|
|
185
212
|
name: 'list',
|
|
186
|
-
description: 'List
|
|
213
|
+
description: 'List task packets',
|
|
187
214
|
json_output: true,
|
|
188
215
|
arguments: [],
|
|
189
|
-
options: ['--status
|
|
216
|
+
options: ['--status draft|ready|queued|running|review|blocked|done|all', '--json'],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'assign',
|
|
220
|
+
description: 'Assign task to an agent role and runner',
|
|
221
|
+
json_output: true,
|
|
222
|
+
arguments: ['<task-id>'],
|
|
223
|
+
options: ['--agent-role planner|builder-ui|builder-app|reviewer|tester', '--runner codex|claude', '--json'],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: 'create-followup',
|
|
227
|
+
description: 'Create follow-up task from run report',
|
|
228
|
+
json_output: true,
|
|
229
|
+
arguments: ['<run-id>'],
|
|
230
|
+
options: ['--json'],
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'prompt',
|
|
236
|
+
description: 'Prompt compilation operations',
|
|
237
|
+
json_output: true,
|
|
238
|
+
arguments: [],
|
|
239
|
+
options: [],
|
|
240
|
+
subcommands: [
|
|
241
|
+
{
|
|
242
|
+
name: 'compile',
|
|
243
|
+
description: 'Compile an execution brief prompt from a task packet',
|
|
244
|
+
json_output: true,
|
|
245
|
+
arguments: ['<task-id>'],
|
|
246
|
+
options: ['--runner codex|claude', '--agent-role <role>', '--preview', '--json'],
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: 'run',
|
|
252
|
+
description: 'Execute a task run or inspect run records',
|
|
253
|
+
json_output: true,
|
|
254
|
+
arguments: ['[task-id]'],
|
|
255
|
+
options: ['--runner codex|claude', '--agent-role planner|builder-ui|builder-app|reviewer|tester', '--dry-run', '--json'],
|
|
256
|
+
subcommands: [
|
|
257
|
+
{
|
|
258
|
+
name: 'queue',
|
|
259
|
+
description: 'Queue ready tasks with satisfied dependencies',
|
|
260
|
+
json_output: true,
|
|
261
|
+
arguments: [],
|
|
262
|
+
options: ['--json'],
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: 'start-ready',
|
|
266
|
+
description: 'Queue and start all runnable ready tasks',
|
|
267
|
+
json_output: true,
|
|
268
|
+
arguments: [],
|
|
269
|
+
options: ['--dry-run', '--json'],
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'approve',
|
|
273
|
+
description: 'Review run as approved, needs changes, or merged',
|
|
274
|
+
json_output: true,
|
|
275
|
+
arguments: ['<run-id>'],
|
|
276
|
+
options: ['--state approved|needs_changes|merged', '--note <text>', '--by <name>', '--json'],
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: 'block',
|
|
280
|
+
description: 'Mark run blocked',
|
|
281
|
+
json_output: true,
|
|
282
|
+
arguments: ['<run-id>'],
|
|
283
|
+
options: ['--note <text>', '--by <name>', '--json'],
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: 'list',
|
|
287
|
+
description: 'List run records',
|
|
288
|
+
json_output: true,
|
|
289
|
+
arguments: [],
|
|
290
|
+
options: ['--task <task-id>', '--status queued|preparing|running|review|blocked|completed|failed|all', '--json'],
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: 'show',
|
|
294
|
+
description: 'Show a run record',
|
|
295
|
+
json_output: true,
|
|
296
|
+
arguments: ['<run-id>'],
|
|
297
|
+
options: ['--json'],
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'summary',
|
|
301
|
+
description: 'Show project-level run review summary',
|
|
302
|
+
json_output: true,
|
|
303
|
+
arguments: [],
|
|
304
|
+
options: ['--json'],
|
|
190
305
|
},
|
|
191
306
|
],
|
|
192
307
|
},
|