sidecar-cli 0.1.1 → 0.1.2-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/README.md +132 -1
- package/dist/cli.js +656 -68
- package/dist/lib/output.js +3 -0
- package/dist/lib/paths.js +3 -0
- package/dist/lib/ui.js +109 -0
- 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 +193 -14
- package/dist/services/event-ingest-service.js +72 -0
- package/dist/services/export-service.js +79 -0
- 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/dist/types/api.js +1 -0
- package/package.json +1 -1
package/dist/lib/output.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { stringifyJson } from './format.js';
|
|
2
|
+
export const JSON_CONTRACT_VERSION = '1.0';
|
|
2
3
|
export function jsonSuccess(command, data) {
|
|
3
4
|
return {
|
|
4
5
|
ok: true,
|
|
6
|
+
version: JSON_CONTRACT_VERSION,
|
|
5
7
|
command,
|
|
6
8
|
data,
|
|
7
9
|
errors: [],
|
|
@@ -10,6 +12,7 @@ export function jsonSuccess(command, data) {
|
|
|
10
12
|
export function jsonFailure(command, message) {
|
|
11
13
|
return {
|
|
12
14
|
ok: false,
|
|
15
|
+
version: JSON_CONTRACT_VERSION,
|
|
13
16
|
command,
|
|
14
17
|
data: null,
|
|
15
18
|
errors: [message],
|
package/dist/lib/paths.js
CHANGED
|
@@ -6,6 +6,9 @@ export function getSidecarPaths(rootPath) {
|
|
|
6
6
|
return {
|
|
7
7
|
rootPath,
|
|
8
8
|
sidecarPath,
|
|
9
|
+
tasksPath: path.join(sidecarPath, 'tasks'),
|
|
10
|
+
runsPath: path.join(sidecarPath, 'runs'),
|
|
11
|
+
promptsPath: path.join(sidecarPath, 'prompts'),
|
|
9
12
|
rootAgentsPath: path.join(rootPath, 'AGENTS.md'),
|
|
10
13
|
rootClaudePath: path.join(rootPath, 'CLAUDE.md'),
|
|
11
14
|
dbPath: path.join(sidecarPath, 'sidecar.db'),
|
package/dist/lib/ui.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
5
|
+
import { detectReleaseChannel } from './update-check.js';
|
|
6
|
+
import { SidecarError } from './errors.js';
|
|
7
|
+
const UI_PACKAGE = '@sidecar/ui';
|
|
8
|
+
const UI_RUNTIME_DIR = path.join(os.homedir(), '.sidecar', 'ui');
|
|
9
|
+
function ensureRuntimeDir() {
|
|
10
|
+
fs.mkdirSync(UI_RUNTIME_DIR, { recursive: true });
|
|
11
|
+
const runtimePkgPath = path.join(UI_RUNTIME_DIR, 'package.json');
|
|
12
|
+
if (!fs.existsSync(runtimePkgPath)) {
|
|
13
|
+
fs.writeFileSync(runtimePkgPath, JSON.stringify({ name: 'sidecar-ui-runtime', private: true, version: '0.0.0' }, null, 2));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function readInstalledUiVersion() {
|
|
17
|
+
const p = path.join(UI_RUNTIME_DIR, 'node_modules', '@sidecar', 'ui', 'package.json');
|
|
18
|
+
if (!fs.existsSync(p))
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
22
|
+
return pkg.version ?? null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function major(version) {
|
|
29
|
+
const m = version.match(/^(\d+)\./);
|
|
30
|
+
if (!m)
|
|
31
|
+
return null;
|
|
32
|
+
return Number.parseInt(m[1], 10);
|
|
33
|
+
}
|
|
34
|
+
function isCompatible(cliVersion, uiVersion) {
|
|
35
|
+
if (!uiVersion)
|
|
36
|
+
return false;
|
|
37
|
+
const cliMajor = major(cliVersion);
|
|
38
|
+
const uiMajor = major(uiVersion);
|
|
39
|
+
return cliMajor !== null && uiMajor !== null && cliMajor === uiMajor;
|
|
40
|
+
}
|
|
41
|
+
function npmInstall(spec) {
|
|
42
|
+
execFileSync('npm', ['install', '--no-audit', '--no-fund', spec], {
|
|
43
|
+
cwd: UI_RUNTIME_DIR,
|
|
44
|
+
stdio: 'inherit',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function getDesiredTag(cliVersion) {
|
|
48
|
+
return detectReleaseChannel(cliVersion);
|
|
49
|
+
}
|
|
50
|
+
export function ensureUiInstalled(options) {
|
|
51
|
+
ensureRuntimeDir();
|
|
52
|
+
const tag = getDesiredTag(options.cliVersion);
|
|
53
|
+
const installed = readInstalledUiVersion();
|
|
54
|
+
const shouldInstall = Boolean(options.reinstall) || !isCompatible(options.cliVersion, installed);
|
|
55
|
+
if (shouldInstall) {
|
|
56
|
+
options.onStatus?.(installed
|
|
57
|
+
? `Updating Sidecar UI (${installed}) for CLI compatibility...`
|
|
58
|
+
: 'Installing Sidecar UI for first use...');
|
|
59
|
+
const spec = `${UI_PACKAGE}@${tag}`;
|
|
60
|
+
try {
|
|
61
|
+
npmInstall(spec);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
const localUiPkg = path.resolve(process.cwd(), 'packages', 'ui');
|
|
65
|
+
if (fs.existsSync(path.join(localUiPkg, 'package.json'))) {
|
|
66
|
+
options.onStatus?.('Falling back to local workspace UI package...');
|
|
67
|
+
npmInstall(localUiPkg);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
throw new SidecarError('Failed to install Sidecar UI package. Check npm access/network and retry `sidecar ui --reinstall`.');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const finalVersion = readInstalledUiVersion();
|
|
75
|
+
if (!finalVersion) {
|
|
76
|
+
throw new SidecarError('Sidecar UI appears missing after install. Retry with `sidecar ui --reinstall`.');
|
|
77
|
+
}
|
|
78
|
+
return { installedVersion: finalVersion };
|
|
79
|
+
}
|
|
80
|
+
export function launchUiServer(options) {
|
|
81
|
+
const serverPath = path.join(UI_RUNTIME_DIR, 'node_modules', '@sidecar', 'ui', 'server.js');
|
|
82
|
+
if (!fs.existsSync(serverPath)) {
|
|
83
|
+
throw new SidecarError('Sidecar UI server entry was not found after install.');
|
|
84
|
+
}
|
|
85
|
+
const child = spawn(process.execPath, [serverPath, '--project', options.projectPath, '--port', String(options.port)], {
|
|
86
|
+
stdio: 'inherit',
|
|
87
|
+
env: {
|
|
88
|
+
...process.env,
|
|
89
|
+
SIDECAR_CLI_JS: process.argv[1] || '',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
const url = `http://localhost:${options.port}`;
|
|
93
|
+
if (options.openBrowser) {
|
|
94
|
+
openBrowser(url);
|
|
95
|
+
}
|
|
96
|
+
return { url, child };
|
|
97
|
+
}
|
|
98
|
+
export function openBrowser(url) {
|
|
99
|
+
const platform = process.platform;
|
|
100
|
+
if (platform === 'darwin') {
|
|
101
|
+
spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (platform === 'win32') {
|
|
105
|
+
spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
109
|
+
}
|
|
@@ -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
|
+
}
|