sidecar-cli 0.1.5-beta.1 → 0.1.5-rc.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 +246 -17
- package/dist/cli.js +667 -66
- package/dist/lib/banner.js +17 -1
- package/dist/lib/color.js +30 -0
- package/dist/lib/format.js +7 -1
- package/dist/lib/table.js +97 -0
- package/dist/prompts/packet-sections.js +203 -0
- package/dist/prompts/prompt-compiler.js +90 -163
- package/dist/prompts/prompt-service.js +7 -0
- package/dist/prompts/prompt-spec.js +128 -0
- package/dist/prompts/sections.js +194 -0
- package/dist/runners/claude-runner.js +7 -28
- package/dist/runners/codex-runner.js +7 -28
- package/dist/runners/config.js +75 -0
- package/dist/runners/runner-exec.js +152 -0
- package/dist/runs/capture.js +429 -0
- package/dist/runs/run-record.js +42 -0
- package/dist/runs/run-repository.js +1 -0
- package/dist/services/hook-service.js +130 -0
- package/dist/services/run-orchestrator-service.js +210 -11
- package/dist/services/run-review-service.js +1 -1
- package/dist/tasks/task-packet.js +18 -1
- package/dist/tasks/task-service.js +4 -1
- package/dist/templates/hooks.js +34 -0
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,8 @@ import { SidecarError } from './lib/errors.js';
|
|
|
12
12
|
import { GLOBAL_INSTRUCTIONS_DIR, resolveInstructionsSource } from './lib/instructions.js';
|
|
13
13
|
import { jsonFailure, jsonSuccess, printJsonEnvelope } from './lib/output.js';
|
|
14
14
|
import { bannerDisabled, renderBanner } from './lib/banner.js';
|
|
15
|
+
import { c } from './lib/color.js';
|
|
16
|
+
import { renderTable } from './lib/table.js';
|
|
15
17
|
import { getUpdateNotice } from './lib/update-check.js';
|
|
16
18
|
import { ensureUiInstalled, launchUiServer } from './lib/ui.js';
|
|
17
19
|
import { requireInitialized } from './db/client.js';
|
|
@@ -22,6 +24,11 @@ import { getCapabilitiesManifest } from './services/capabilities-service.js';
|
|
|
22
24
|
import { addArtifact, listArtifacts } from './services/artifact-service.js';
|
|
23
25
|
import { addDecision, addNote, addWorklog, getActiveSessionId, listRecentEvents } from './services/event-service.js';
|
|
24
26
|
import { currentSession, endSession, startSession, verifySessionHygiene } from './services/session-service.js';
|
|
27
|
+
import { HOOK_EVENTS, handleHookEvent, hookEventSchema, hookPayloadSchema } from './services/hook-service.js';
|
|
28
|
+
import { loadPromptSpec } from './prompts/prompt-spec.js';
|
|
29
|
+
import { compileSections } from './prompts/sections.js';
|
|
30
|
+
import { loadPromptPreferences } from './runners/config.js';
|
|
31
|
+
import { renderClaudeCodeHooksJson } from './templates/hooks.js';
|
|
25
32
|
import { eventIngestSchema, ingestEvent } from './services/event-ingest-service.js';
|
|
26
33
|
import { buildExportJson, buildExportJsonlEvents, writeOutputFile } from './services/export-service.js';
|
|
27
34
|
import { createTaskPacketRecord, getTaskPacket, listTaskPackets } from './tasks/task-service.js';
|
|
@@ -29,7 +36,7 @@ import { taskPacketPrioritySchema, taskPacketStatusSchema, taskPacketTypeSchema
|
|
|
29
36
|
import { getRunRecord, listRunRecords, listRunRecordsForTask } from './runs/run-service.js';
|
|
30
37
|
import { runStatusSchema, runnerTypeSchema } from './runs/run-record.js';
|
|
31
38
|
import { compileTaskPrompt } from './prompts/prompt-service.js';
|
|
32
|
-
import { runTaskExecution } from './services/run-orchestrator-service.js';
|
|
39
|
+
import { runPipelineExecution, runTaskExecution } from './services/run-orchestrator-service.js';
|
|
33
40
|
import { loadRunnerPreferences } from './runners/config.js';
|
|
34
41
|
import { assignTask, queueReadyTasks } from './services/task-orchestration-service.js';
|
|
35
42
|
import { buildReviewSummary, createFollowupTaskFromRun, reviewRun } from './services/run-review-service.js';
|
|
@@ -41,6 +48,20 @@ const runListStatusSchema = runStatusSchema.or(z.literal('all'));
|
|
|
41
48
|
const agentRoleSchema = z.enum(['planner', 'builder-ui', 'builder-app', 'reviewer', 'tester']);
|
|
42
49
|
const exportFormatSchema = z.enum(['json', 'jsonl']);
|
|
43
50
|
const NOT_INITIALIZED_MSG = 'Sidecar is not initialized in this directory or any parent directory';
|
|
51
|
+
function formatStatus(value) {
|
|
52
|
+
const v = value.toLowerCase();
|
|
53
|
+
if (v === 'ready' || v === 'draft')
|
|
54
|
+
return c.cyan(value);
|
|
55
|
+
if (v === 'running' || v === 'queued')
|
|
56
|
+
return c.yellow(value);
|
|
57
|
+
if (v === 'review')
|
|
58
|
+
return c.magenta(value);
|
|
59
|
+
if (v === 'blocked')
|
|
60
|
+
return c.red(value);
|
|
61
|
+
if (v === 'done' || v === 'merged' || v === 'approved')
|
|
62
|
+
return c.green(value);
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
44
65
|
function fail(message) {
|
|
45
66
|
throw new SidecarError(message);
|
|
46
67
|
}
|
|
@@ -60,7 +81,7 @@ function handleCommandError(command, asJson, err) {
|
|
|
60
81
|
printJsonEnvelope(jsonFailure(command, message));
|
|
61
82
|
}
|
|
62
83
|
else {
|
|
63
|
-
console.error(message);
|
|
84
|
+
console.error(`${c.red(c.bold('Error:'))} ${message}`);
|
|
64
85
|
}
|
|
65
86
|
process.exit(err instanceof SidecarError ? err.exitCode : 1);
|
|
66
87
|
}
|
|
@@ -73,17 +94,6 @@ function respondSuccess(command, asJson, data, lines = []) {
|
|
|
73
94
|
console.log(line);
|
|
74
95
|
}
|
|
75
96
|
}
|
|
76
|
-
function renderInitBanner() {
|
|
77
|
-
return [
|
|
78
|
-
' [■]─[▪]',
|
|
79
|
-
' ███████╗██╗██████╗ ███████╗ ██████╗ █████╗ ██████╗',
|
|
80
|
-
' ██╔════╝██║██╔══██╗██╔════╝██╔════╝██╔══██╗██╔══██╗',
|
|
81
|
-
' ███████╗██║██║ ██║█████╗ ██║ ███████║██████╔╝',
|
|
82
|
-
' ╚════██║██║██║ ██║██╔══╝ ██║ ██╔══██║██╔══██╗',
|
|
83
|
-
' ███████║██║██████╔╝███████╗╚██████╗██║ ██║██║ ██║',
|
|
84
|
-
' ╚══════╝╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝',
|
|
85
|
-
].join('\n');
|
|
86
|
-
}
|
|
87
97
|
function summaryWasRefreshedRecently(db, projectId) {
|
|
88
98
|
return Boolean(db
|
|
89
99
|
.prepare(`SELECT id FROM events WHERE project_id = ? AND type = 'summary_generated' AND created_at >= datetime('now', '-3 day') LIMIT 1`)
|
|
@@ -223,8 +233,58 @@ async function askWithDefault(rl, question, fallback) {
|
|
|
223
233
|
return fallback ?? '';
|
|
224
234
|
}
|
|
225
235
|
const program = new Command();
|
|
226
|
-
program
|
|
236
|
+
program
|
|
237
|
+
.name('sidecar')
|
|
238
|
+
.description('Local-first project memory and agent runner. Two namespaces:\n' +
|
|
239
|
+
' sidecar log <memory-cmd> (worklog, decision, note, recent, context, summary, session, event, artifact)\n' +
|
|
240
|
+
' sidecar work <runner-cmd> (task, run, prompt, hooks)\n' +
|
|
241
|
+
'The underlying verbs also work directly (e.g. `sidecar worklog record`), so existing scripts keep working.')
|
|
242
|
+
.version(pkg.version);
|
|
227
243
|
program.option('--no-banner', 'Disable Sidecar banner output');
|
|
244
|
+
const LOG_NAMESPACE_MEMBERS = [
|
|
245
|
+
'worklog',
|
|
246
|
+
'decision',
|
|
247
|
+
'note',
|
|
248
|
+
'recent',
|
|
249
|
+
'context',
|
|
250
|
+
'summary',
|
|
251
|
+
'session',
|
|
252
|
+
'event',
|
|
253
|
+
'artifact',
|
|
254
|
+
];
|
|
255
|
+
const WORK_NAMESPACE_MEMBERS = ['task', 'run', 'prompt', 'hooks'];
|
|
256
|
+
// Approach C for the namespace split (memory vs runner): NEW top-level groups
|
|
257
|
+
// `log` and `work` that proxy to the existing verbs by rewriting argv before
|
|
258
|
+
// commander parses it. Existing verbs remain registered as-is, so scripts,
|
|
259
|
+
// agents, and CLAUDE.md files in the wild keep working — the new namespaces
|
|
260
|
+
// just add a clean positioning surface on top.
|
|
261
|
+
function rewriteNamespaceArgv(argv) {
|
|
262
|
+
const out = [...argv];
|
|
263
|
+
if (out.length < 4)
|
|
264
|
+
return out;
|
|
265
|
+
const ns = out[2];
|
|
266
|
+
const next = out[3];
|
|
267
|
+
if (ns === 'log' && LOG_NAMESPACE_MEMBERS.includes(next)) {
|
|
268
|
+
out.splice(2, 1);
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
if (ns === 'work' && WORK_NAMESPACE_MEMBERS.includes(next)) {
|
|
272
|
+
out.splice(2, 1);
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
function printNamespaceHelp(kind) {
|
|
278
|
+
const members = kind === 'log' ? LOG_NAMESPACE_MEMBERS : WORK_NAMESPACE_MEMBERS;
|
|
279
|
+
const heading = kind === 'log' ? 'Memory commands' : 'Runner commands';
|
|
280
|
+
console.log(`${heading} — use ${c.cyan(`sidecar ${kind} <command> …`)} or the verb directly.`);
|
|
281
|
+
console.log('');
|
|
282
|
+
for (const m of members) {
|
|
283
|
+
console.log(` ${c.cyan(`sidecar ${kind} ${m}`)} → ${c.dim(`sidecar ${m}`)}`);
|
|
284
|
+
}
|
|
285
|
+
console.log('');
|
|
286
|
+
console.log(`Run ${c.cyan(`sidecar <command> --help`)} for details on any verb.`);
|
|
287
|
+
}
|
|
228
288
|
function maybePrintUpdateNotice() {
|
|
229
289
|
const jsonRequested = process.argv.includes('--json');
|
|
230
290
|
const notice = getUpdateNotice({
|
|
@@ -236,8 +296,8 @@ function maybePrintUpdateNotice() {
|
|
|
236
296
|
return;
|
|
237
297
|
const installTag = notice.channel === 'latest' ? 'latest' : notice.channel;
|
|
238
298
|
console.log('');
|
|
239
|
-
console.log(`Update available: ${pkg.version} -> ${notice.latestVersion}`);
|
|
240
|
-
console.log(`Run: npm install -g sidecar-cli@${installTag}`);
|
|
299
|
+
console.log(c.yellow(`Update available: ${pkg.version} -> ${notice.latestVersion}`));
|
|
300
|
+
console.log(`Run: ${c.cyan(`npm install -g sidecar-cli@${installTag}`)}`);
|
|
241
301
|
}
|
|
242
302
|
program
|
|
243
303
|
.command('ui')
|
|
@@ -261,13 +321,16 @@ program
|
|
|
261
321
|
console.log('');
|
|
262
322
|
}
|
|
263
323
|
console.log('Launching Sidecar UI');
|
|
264
|
-
|
|
324
|
+
// Format info with aligned labels
|
|
325
|
+
const projectLabel = 'Project:'.padEnd(15);
|
|
326
|
+
const versionLabel = 'UI version:'.padEnd(15);
|
|
327
|
+
console.log(` ${projectLabel}${projectRoot}`);
|
|
265
328
|
const { installedVersion } = ensureUiInstalled({
|
|
266
329
|
cliVersion: pkg.version,
|
|
267
330
|
reinstall: Boolean(opts.reinstall),
|
|
268
|
-
onStatus: (line) => console.log(line),
|
|
331
|
+
onStatus: (line) => console.log(` … ${line}`),
|
|
269
332
|
});
|
|
270
|
-
console.log(`
|
|
333
|
+
console.log(` ${versionLabel}${installedVersion}`);
|
|
271
334
|
if (opts.installOnly) {
|
|
272
335
|
console.log('Install-only mode complete.');
|
|
273
336
|
return;
|
|
@@ -277,9 +340,11 @@ program
|
|
|
277
340
|
port,
|
|
278
341
|
openBrowser: opts.open !== false,
|
|
279
342
|
});
|
|
280
|
-
console.log(
|
|
343
|
+
console.log('');
|
|
344
|
+
const openLabel = 'Open:'.padEnd(15);
|
|
345
|
+
console.log(` ${openLabel}${c.cyan(url)}`);
|
|
281
346
|
if (opts.open === false) {
|
|
282
|
-
console.log('Browser auto-open disabled.');
|
|
347
|
+
console.log(' Browser auto-open disabled.');
|
|
283
348
|
}
|
|
284
349
|
}
|
|
285
350
|
catch (err) {
|
|
@@ -408,7 +473,7 @@ program
|
|
|
408
473
|
};
|
|
409
474
|
const shouldShowBanner = !opts.json && !bannerDisabled();
|
|
410
475
|
if (shouldShowBanner) {
|
|
411
|
-
console.log(
|
|
476
|
+
console.log(renderBanner('block'));
|
|
412
477
|
console.log('');
|
|
413
478
|
}
|
|
414
479
|
respondSuccess(command, Boolean(opts.json), data, [
|
|
@@ -422,6 +487,158 @@ program
|
|
|
422
487
|
handleCommandError(command, Boolean(opts.json), err);
|
|
423
488
|
}
|
|
424
489
|
});
|
|
490
|
+
program
|
|
491
|
+
.command('demo')
|
|
492
|
+
.description('One-shot walkthrough in a temp dir: init → task → prompt → worklog → decision → summary')
|
|
493
|
+
.option('--cleanup', 'Delete the demo directory when done (default: keep so you can inspect the files)')
|
|
494
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
495
|
+
.addHelpText('after', '\nExamples:\n $ sidecar demo\n $ sidecar demo --cleanup\n $ sidecar demo --json')
|
|
496
|
+
.action(async (opts) => {
|
|
497
|
+
const command = 'demo';
|
|
498
|
+
const os = await import('node:os');
|
|
499
|
+
const asJson = Boolean(opts.json);
|
|
500
|
+
const previousCwd = process.cwd();
|
|
501
|
+
const demoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sidecar-demo-'));
|
|
502
|
+
const log = asJson ? () => { } : (line = '') => console.log(line);
|
|
503
|
+
const step = (n, title) => log(`\n${c.bold(c.cyan(`[${n}] ${title}`))}`);
|
|
504
|
+
try {
|
|
505
|
+
process.chdir(demoRoot);
|
|
506
|
+
log(c.bold('Sidecar demo'));
|
|
507
|
+
log(c.dim(`Sandbox: ${demoRoot}`));
|
|
508
|
+
log(c.dim('Nothing below modifies your current project.'));
|
|
509
|
+
step(1, 'Initialize (.sidecar/, config, DB)');
|
|
510
|
+
const sidecar = getSidecarPaths(demoRoot);
|
|
511
|
+
fs.mkdirSync(sidecar.sidecarPath, { recursive: true });
|
|
512
|
+
fs.mkdirSync(sidecar.tasksPath, { recursive: true });
|
|
513
|
+
fs.mkdirSync(sidecar.runsPath, { recursive: true });
|
|
514
|
+
fs.mkdirSync(sidecar.promptsPath, { recursive: true });
|
|
515
|
+
const ts = nowIso();
|
|
516
|
+
const projectName = 'demo-project';
|
|
517
|
+
{
|
|
518
|
+
const db = new DatabaseSync(sidecar.dbPath);
|
|
519
|
+
initializeSchema(db);
|
|
520
|
+
db.prepare(`DELETE FROM projects`).run();
|
|
521
|
+
db.prepare(`INSERT INTO projects (name, root_path, created_at, updated_at) VALUES (?, ?, ?, ?)`).run(projectName, demoRoot, ts, ts);
|
|
522
|
+
db.close();
|
|
523
|
+
}
|
|
524
|
+
const config = {
|
|
525
|
+
schemaVersion: 1,
|
|
526
|
+
project: { name: projectName, rootPath: demoRoot, createdAt: ts },
|
|
527
|
+
defaults: { summary: { recentLimit: 10 } },
|
|
528
|
+
settings: {},
|
|
529
|
+
};
|
|
530
|
+
fs.writeFileSync(sidecar.configPath, stringifyJson(config));
|
|
531
|
+
fs.writeFileSync(sidecar.preferencesPath, stringifyJson({
|
|
532
|
+
summary: { format: 'markdown', recentLimit: 8 },
|
|
533
|
+
output: { humanTime: true },
|
|
534
|
+
runner: {
|
|
535
|
+
defaultRunner: 'codex',
|
|
536
|
+
preferredRunners: ['codex', 'claude'],
|
|
537
|
+
defaultAgentRole: 'builder-app',
|
|
538
|
+
},
|
|
539
|
+
}));
|
|
540
|
+
fs.writeFileSync(sidecar.agentsPath, renderAgentsMarkdown(projectName));
|
|
541
|
+
log(c.green(' ✓ ') + 'Sidecar initialized');
|
|
542
|
+
log(c.dim(` try: sidecar status`));
|
|
543
|
+
step(2, 'Create a sample task packet');
|
|
544
|
+
const created = createTaskPacketRecord(demoRoot, {
|
|
545
|
+
title: 'Add welcome banner',
|
|
546
|
+
summary: 'Show a friendly greeting on first launch so new users know the tool is working.',
|
|
547
|
+
goal: 'Render a configurable banner at the top of the home page.',
|
|
548
|
+
type: 'feature',
|
|
549
|
+
status: 'ready',
|
|
550
|
+
priority: 'medium',
|
|
551
|
+
scope_in_scope: ['Render banner component', 'Wire to /home route'],
|
|
552
|
+
scope_out_of_scope: ['Dismissal persistence'],
|
|
553
|
+
files_to_read: ['src/pages/home.tsx'],
|
|
554
|
+
definition_of_done: ['Banner visible on load', 'No layout shift'],
|
|
555
|
+
validation_commands: ['typecheck@30s:tsc --noEmit', 'test@2m:npm test'],
|
|
556
|
+
});
|
|
557
|
+
log(c.green(' ✓ ') + `${created.task.task_id} — ${created.task.title}`);
|
|
558
|
+
log(c.dim(` packet: ${path.relative(demoRoot, created.path)}`));
|
|
559
|
+
log(c.dim(` try: sidecar task show ${created.task.task_id}`));
|
|
560
|
+
step(3, 'Compile a prompt (no runner spawn)');
|
|
561
|
+
const compiled = compileTaskPrompt({
|
|
562
|
+
rootPath: demoRoot,
|
|
563
|
+
taskId: created.task.task_id,
|
|
564
|
+
runner: 'codex',
|
|
565
|
+
agentRole: 'builder-app',
|
|
566
|
+
});
|
|
567
|
+
const promptPreview = compiled.prompt_markdown
|
|
568
|
+
.split('\n')
|
|
569
|
+
.slice(0, 10)
|
|
570
|
+
.map((l) => ' ' + l)
|
|
571
|
+
.join('\n');
|
|
572
|
+
log(c.green(' ✓ ') + `${compiled.run_id} compiled to ${path.relative(demoRoot, compiled.prompt_path)}`);
|
|
573
|
+
log(c.dim(` tokens: ${compiled.prompt_optimization.estimated_tokens_before} → ${compiled.prompt_optimization.estimated_tokens_after} (budget ${compiled.prompt_optimization.budget_target})`));
|
|
574
|
+
log(c.dim(' preview:'));
|
|
575
|
+
log(c.dim(promptPreview));
|
|
576
|
+
log(c.dim(` try: sidecar run-exec ${created.task.task_id} --dry-run`));
|
|
577
|
+
step(4, 'Record a worklog + decision');
|
|
578
|
+
const db = new DatabaseSync(sidecar.dbPath);
|
|
579
|
+
const row = db.prepare(`SELECT id FROM projects LIMIT 1`).get();
|
|
580
|
+
const projectId = row.id;
|
|
581
|
+
addWorklog(db, {
|
|
582
|
+
projectId,
|
|
583
|
+
goal: 'welcome banner',
|
|
584
|
+
done: 'Scaffolded banner component and wired the home route.',
|
|
585
|
+
files: 'src/pages/home.tsx,src/components/Banner.tsx',
|
|
586
|
+
by: 'agent',
|
|
587
|
+
});
|
|
588
|
+
addDecision(db, {
|
|
589
|
+
projectId,
|
|
590
|
+
title: 'Use inline SVG for the banner icon',
|
|
591
|
+
summary: 'Avoids an extra network request on first load; matches existing icon patterns.',
|
|
592
|
+
by: 'agent',
|
|
593
|
+
});
|
|
594
|
+
log(c.green(' ✓ ') + 'worklog + decision recorded');
|
|
595
|
+
log(c.dim(' try: sidecar worklog list | sidecar decision list'));
|
|
596
|
+
step(5, 'Refresh summary + show context');
|
|
597
|
+
const refreshed = refreshSummaryFile(db, demoRoot, projectId, 10);
|
|
598
|
+
const ctx = buildContext(db, { projectId, limit: 10 });
|
|
599
|
+
db.close();
|
|
600
|
+
log(c.green(' ✓ ') + `summary.md refreshed (${refreshed.generatedAt})`);
|
|
601
|
+
log(c.dim(` path: ${path.relative(demoRoot, sidecar.summaryPath)}`));
|
|
602
|
+
log(c.dim(` worklogs: ${ctx.recentWorklogs.length}, decisions: ${ctx.recentDecisions.length}, open tasks: ${ctx.openTasks.length}`));
|
|
603
|
+
log(c.dim(' try: sidecar context --format markdown'));
|
|
604
|
+
log('');
|
|
605
|
+
log(c.bold('Done.'));
|
|
606
|
+
if (opts.cleanup) {
|
|
607
|
+
process.chdir(previousCwd);
|
|
608
|
+
fs.rmSync(demoRoot, { recursive: true, force: true });
|
|
609
|
+
log(c.dim('Sandbox cleaned up.'));
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
log(`Sandbox kept at ${c.cyan(demoRoot)} — poke around, then ${c.dim('rm -rf')} when done.`);
|
|
613
|
+
}
|
|
614
|
+
const data = {
|
|
615
|
+
demo_root: demoRoot,
|
|
616
|
+
task_id: created.task.task_id,
|
|
617
|
+
run_id: compiled.run_id,
|
|
618
|
+
prompt_path: compiled.prompt_path,
|
|
619
|
+
cleaned_up: Boolean(opts.cleanup),
|
|
620
|
+
};
|
|
621
|
+
if (asJson) {
|
|
622
|
+
process.chdir(previousCwd);
|
|
623
|
+
printJsonEnvelope(jsonSuccess(command, data));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
try {
|
|
628
|
+
process.chdir(previousCwd);
|
|
629
|
+
}
|
|
630
|
+
catch { /* ignore */ }
|
|
631
|
+
handleCommandError(command, asJson, err);
|
|
632
|
+
}
|
|
633
|
+
finally {
|
|
634
|
+
if (process.cwd() !== previousCwd) {
|
|
635
|
+
try {
|
|
636
|
+
process.chdir(previousCwd);
|
|
637
|
+
}
|
|
638
|
+
catch { /* ignore */ }
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
});
|
|
425
642
|
program
|
|
426
643
|
.command('status')
|
|
427
644
|
.description('Show Sidecar status and recent project activity')
|
|
@@ -666,14 +883,24 @@ program
|
|
|
666
883
|
if (opts.json)
|
|
667
884
|
printJsonEnvelope(jsonSuccess(command, { events: rows }));
|
|
668
885
|
else {
|
|
669
|
-
|
|
886
|
+
const typed = rows;
|
|
887
|
+
if (typed.length === 0) {
|
|
670
888
|
console.log('No events found.');
|
|
671
889
|
return;
|
|
672
890
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
891
|
+
renderTable([
|
|
892
|
+
{ key: 'id', label: 'ID', align: 'right' },
|
|
893
|
+
{ key: 'when', label: 'When' },
|
|
894
|
+
{ key: 'type', label: 'Type' },
|
|
895
|
+
{ key: 'title', label: 'Title', maxWidth: 40 },
|
|
896
|
+
{ key: 'summary', label: 'Summary', maxWidth: 60 },
|
|
897
|
+
], typed.map((row) => ({
|
|
898
|
+
id: `#${row.id}`,
|
|
899
|
+
when: humanTime(row.created_at),
|
|
900
|
+
type: row.type,
|
|
901
|
+
title: row.title ?? '',
|
|
902
|
+
summary: row.summary ?? '',
|
|
903
|
+
})));
|
|
677
904
|
}
|
|
678
905
|
}
|
|
679
906
|
catch (err) {
|
|
@@ -790,7 +1017,7 @@ task
|
|
|
790
1017
|
.option('--files-avoid <paths>', 'Comma-separated files to avoid')
|
|
791
1018
|
.option('--constraint-tech <items>', 'Comma-separated technical constraints')
|
|
792
1019
|
.option('--constraint-design <items>', 'Comma-separated design constraints')
|
|
793
|
-
.option('--validate-cmds <commands>', 'Comma-separated validation commands')
|
|
1020
|
+
.option('--validate-cmds <commands>', 'Comma-separated validation commands. Use "kind:command" to tag (typecheck|lint|test|build|custom), e.g. "typecheck:tsc --noEmit,test:npm test". Append "@30s" / "@2m" / "@1500ms" to the kind to override the timeout, e.g. "test@2m:npm test".')
|
|
794
1021
|
.option('--dod <items>', 'Comma-separated definition-of-done checks')
|
|
795
1022
|
.option('--branch <name>', 'Branch name')
|
|
796
1023
|
.option('--worktree <path>', 'Worktree path')
|
|
@@ -888,13 +1115,18 @@ task
|
|
|
888
1115
|
console.log('No tasks found.');
|
|
889
1116
|
return;
|
|
890
1117
|
}
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1118
|
+
const tableRows = rows.map((r) => ({
|
|
1119
|
+
task_id: r.task_id,
|
|
1120
|
+
status: r.status,
|
|
1121
|
+
priority: r.priority,
|
|
1122
|
+
title: r.title,
|
|
1123
|
+
}));
|
|
1124
|
+
renderTable([
|
|
1125
|
+
{ key: 'task_id', label: 'TASK ID', minWidth: 6 },
|
|
1126
|
+
{ key: 'status', label: 'STATUS', minWidth: 8, format: formatStatus },
|
|
1127
|
+
{ key: 'priority', label: 'PRIORITY', minWidth: 8 },
|
|
1128
|
+
{ key: 'title', label: 'TITLE', maxWidth: 60 },
|
|
1129
|
+
], tableRows);
|
|
898
1130
|
}
|
|
899
1131
|
catch (err) {
|
|
900
1132
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -946,23 +1178,131 @@ task
|
|
|
946
1178
|
}
|
|
947
1179
|
});
|
|
948
1180
|
const prompt = program.command('prompt').description('Prompt compilation commands');
|
|
1181
|
+
function parseSectionPolicy(raw) {
|
|
1182
|
+
if (!raw)
|
|
1183
|
+
return undefined;
|
|
1184
|
+
const out = {};
|
|
1185
|
+
for (const pair of raw.split(',')) {
|
|
1186
|
+
const [id, policy] = pair.split('=').map((s) => s.trim());
|
|
1187
|
+
if (!id || !policy)
|
|
1188
|
+
continue;
|
|
1189
|
+
if (policy !== 'keep' && policy !== 'trim-last' && policy !== 'drop') {
|
|
1190
|
+
fail(`Invalid policy for ${id}: ${policy} (expected keep|trim-last|drop)`);
|
|
1191
|
+
}
|
|
1192
|
+
out[id] = policy;
|
|
1193
|
+
}
|
|
1194
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
1195
|
+
}
|
|
1196
|
+
function isSpecFileTarget(target) {
|
|
1197
|
+
const lower = target.toLowerCase();
|
|
1198
|
+
if (lower.endsWith('.yaml') || lower.endsWith('.yml') || lower.endsWith('.json'))
|
|
1199
|
+
return true;
|
|
1200
|
+
if (target.includes('/') || target.startsWith('.'))
|
|
1201
|
+
return true;
|
|
1202
|
+
if (fs.existsSync(target))
|
|
1203
|
+
return true;
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
949
1206
|
prompt
|
|
950
|
-
.command('compile <task-
|
|
951
|
-
.description('Compile a markdown execution brief from a task
|
|
952
|
-
.
|
|
953
|
-
.
|
|
1207
|
+
.command('compile <task-or-file>')
|
|
1208
|
+
.description('Compile a markdown execution brief from a task id OR a freestanding prompt spec file (.yaml|.yml|.json)')
|
|
1209
|
+
.option('--runner <runner>', 'codex|claude (task-id mode only)')
|
|
1210
|
+
.option('--agent-role <role>', 'Agent role, for example builder (task-id mode only)')
|
|
954
1211
|
.option('--preview', 'Print compiled prompt content after writing file')
|
|
1212
|
+
.option('--budget <tokens>', 'Override target budget (spec-file mode)', (v) => Number.parseInt(v, 10))
|
|
1213
|
+
.option('--budget-max <tokens>', 'Override ceiling budget (spec-file mode)', (v) => Number.parseInt(v, 10))
|
|
1214
|
+
.option('--section-policy <id=policy,...>', 'Per-section policy overrides (keep|trim-last|drop), spec-file mode')
|
|
1215
|
+
.option('--explain', 'Print a per-section trace of what got kept, trimmed, or dropped')
|
|
1216
|
+
.option('-o, --out <path>', 'Write compiled markdown to this path (spec-file mode; default prints to stdout)')
|
|
1217
|
+
.option('--format <format>', 'markdown|json (spec-file mode; default markdown)', 'markdown')
|
|
955
1218
|
.option('--json', 'Print machine-readable JSON output')
|
|
956
|
-
.addHelpText('after', '\nExamples:\n
|
|
957
|
-
|
|
1219
|
+
.addHelpText('after', '\nExamples:\n' +
|
|
1220
|
+
' $ sidecar prompt compile T-001 --runner codex --agent-role builder\n' +
|
|
1221
|
+
' $ sidecar prompt compile T-001 --runner claude --agent-role builder --preview\n' +
|
|
1222
|
+
' $ sidecar prompt compile ./prompt.yaml\n' +
|
|
1223
|
+
' $ sidecar prompt compile prompt.yaml --budget 2000 --explain\n' +
|
|
1224
|
+
' $ sidecar prompt compile prompt.yaml --section-policy notes=drop,decisions=keep -o out.md\n' +
|
|
1225
|
+
'\nSpec schema (YAML or JSON):\n' +
|
|
1226
|
+
' header: ["# Title", "..."]\n' +
|
|
1227
|
+
' sections:\n' +
|
|
1228
|
+
' - id: objective\n' +
|
|
1229
|
+
' title: Objective\n' +
|
|
1230
|
+
' content: "What to build"\n' +
|
|
1231
|
+
' required: true\n' +
|
|
1232
|
+
' - id: scope\n' +
|
|
1233
|
+
' title: In scope\n' +
|
|
1234
|
+
' list: ["...", "..."]\n' +
|
|
1235
|
+
' trim: { policy: trim-last, limit: 8, limit_strict: 3, overflow_label: "in-scope items" }\n' +
|
|
1236
|
+
' budget: { target: 1200, max: 1500 }\n')
|
|
1237
|
+
.action((target, opts) => {
|
|
958
1238
|
const command = 'prompt compile';
|
|
959
1239
|
try {
|
|
1240
|
+
const targetText = String(target).trim();
|
|
1241
|
+
const usingSpec = isSpecFileTarget(targetText);
|
|
1242
|
+
if (usingSpec) {
|
|
1243
|
+
const rootPath = resolveProjectRoot();
|
|
1244
|
+
const pref = loadPromptPreferences(rootPath);
|
|
1245
|
+
const { spec, input } = loadPromptSpec(targetText);
|
|
1246
|
+
const target_budget = Number.isFinite(opts.budget) ? Number(opts.budget) : spec.budget?.target ?? pref.budget_target;
|
|
1247
|
+
const max_budget = Number.isFinite(opts.budgetMax)
|
|
1248
|
+
? Number(opts.budgetMax)
|
|
1249
|
+
: spec.budget?.max ?? Math.max(target_budget, pref.budget_max);
|
|
1250
|
+
const policyOverrides = {
|
|
1251
|
+
...(input.policy_overrides ?? {}),
|
|
1252
|
+
...(parseSectionPolicy(opts.sectionPolicy) ?? {}),
|
|
1253
|
+
};
|
|
1254
|
+
const result = compileSections({
|
|
1255
|
+
...input,
|
|
1256
|
+
budget: { target: target_budget, max: max_budget },
|
|
1257
|
+
...(Object.keys(policyOverrides).length > 0 ? { policy_overrides: policyOverrides } : {}),
|
|
1258
|
+
});
|
|
1259
|
+
const format = String(opts.format ?? 'markdown').toLowerCase();
|
|
1260
|
+
if (opts.out) {
|
|
1261
|
+
fs.mkdirSync(path.dirname(path.resolve(String(opts.out))), { recursive: true });
|
|
1262
|
+
fs.writeFileSync(path.resolve(String(opts.out)), result.markdown, 'utf8');
|
|
1263
|
+
}
|
|
1264
|
+
if (opts.json || format === 'json') {
|
|
1265
|
+
printJsonEnvelope(jsonSuccess(command, {
|
|
1266
|
+
source: targetText,
|
|
1267
|
+
out_path: opts.out ? path.resolve(String(opts.out)) : null,
|
|
1268
|
+
markdown: result.markdown,
|
|
1269
|
+
metadata: result.metadata,
|
|
1270
|
+
}));
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (!opts.out) {
|
|
1274
|
+
process.stdout.write(result.markdown);
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
console.log(`Compiled ${targetText} -> ${path.resolve(String(opts.out))}`);
|
|
1278
|
+
console.log(`Estimate: ${result.metadata.estimated_tokens_before} -> ${result.metadata.estimated_tokens_after} tokens (target ${target_budget}, max ${max_budget})`);
|
|
1279
|
+
}
|
|
1280
|
+
if (opts.explain) {
|
|
1281
|
+
console.error(''); // blank line before explain block when stdout carried markdown
|
|
1282
|
+
console.error('--- explain ---');
|
|
1283
|
+
for (const s of result.metadata.sections) {
|
|
1284
|
+
const status = s.was_dropped ? 'dropped' : s.was_trimmed ? 'trimmed' : 'kept';
|
|
1285
|
+
const counts = s.kind === 'list' ? ` ${s.kept_items ?? 0}/${s.total_items ?? 0}` : '';
|
|
1286
|
+
console.error(` [${status}] ${s.id} (${s.kind}${counts}) — ~${s.estimated_tokens} tokens — policy=${s.policy_applied}`);
|
|
1287
|
+
}
|
|
1288
|
+
if (result.metadata.trimmed_sections.length > 0) {
|
|
1289
|
+
console.error(`trimmed: ${result.metadata.trimmed_sections.join(', ')}`);
|
|
1290
|
+
}
|
|
1291
|
+
if (result.metadata.dropped_sections.length > 0) {
|
|
1292
|
+
console.error(`dropped: ${result.metadata.dropped_sections.join(', ')}`);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
// Task-id mode (legacy path).
|
|
960
1298
|
const rootPath = resolveProjectRoot();
|
|
961
|
-
const taskId =
|
|
1299
|
+
const taskId = targetText.toUpperCase();
|
|
1300
|
+
if (!opts.runner)
|
|
1301
|
+
fail('--runner is required when compiling a task packet');
|
|
1302
|
+
if (!opts.agentRole)
|
|
1303
|
+
fail('--agent-role is required when compiling a task packet');
|
|
962
1304
|
const runner = runnerTypeSchema.parse(opts.runner);
|
|
963
1305
|
const agentRole = String(opts.agentRole ?? '').trim();
|
|
964
|
-
if (!agentRole)
|
|
965
|
-
fail('Agent role is required');
|
|
966
1306
|
const compiled = compileTaskPrompt({
|
|
967
1307
|
rootPath,
|
|
968
1308
|
taskId,
|
|
@@ -995,25 +1335,52 @@ prompt
|
|
|
995
1335
|
program
|
|
996
1336
|
.command('run-exec <task-id>')
|
|
997
1337
|
.description('Internal command backing `sidecar run <task-id>`')
|
|
998
|
-
.option('--runner <runner>', 'codex|claude')
|
|
1338
|
+
.option('--runner <runner>', 'codex|claude — comma-separated for a dual-runner pipeline (e.g. codex,claude)')
|
|
999
1339
|
.option('--agent-role <role>', 'planner|builder-ui|builder-app|reviewer|tester')
|
|
1000
1340
|
.option('--dry-run', 'Prepare and compile only without executing external runner')
|
|
1001
1341
|
.option('--json', 'Print machine-readable JSON output')
|
|
1002
|
-
.action((taskIdText, opts) => {
|
|
1342
|
+
.action(async (taskIdText, opts) => {
|
|
1003
1343
|
const command = 'run';
|
|
1004
1344
|
try {
|
|
1005
1345
|
const rootPath = resolveProjectRoot();
|
|
1006
1346
|
const defaults = loadRunnerPreferences(rootPath);
|
|
1007
|
-
const
|
|
1347
|
+
const runnersRaw = typeof opts.runner === 'string' ? opts.runner : '';
|
|
1348
|
+
const runners = runnersRaw
|
|
1349
|
+
? runnersRaw
|
|
1350
|
+
.split(',')
|
|
1351
|
+
.map((s) => s.trim())
|
|
1352
|
+
.filter(Boolean)
|
|
1353
|
+
.map((s) => runnerTypeSchema.parse(s))
|
|
1354
|
+
: [defaults.default_runner];
|
|
1008
1355
|
const selectedAgentRole = opts.agentRole
|
|
1009
1356
|
? agentRoleSchema.parse(opts.agentRole)
|
|
1010
1357
|
: defaults.default_agent_role;
|
|
1011
|
-
const
|
|
1358
|
+
const taskId = String(taskIdText).trim().toUpperCase();
|
|
1359
|
+
if (runners.length > 1) {
|
|
1360
|
+
const pipeline = await runPipelineExecution({
|
|
1361
|
+
rootPath,
|
|
1362
|
+
taskId,
|
|
1363
|
+
runners,
|
|
1364
|
+
agentRole: selectedAgentRole,
|
|
1365
|
+
dryRun: Boolean(opts.dryRun),
|
|
1366
|
+
streamOutput: opts.json ? 'stderr' : 'stdout',
|
|
1367
|
+
});
|
|
1368
|
+
const lines = [
|
|
1369
|
+
`Pipeline ${pipeline.pipeline_id} — ${pipeline.steps.length} runners for ${taskId}.`,
|
|
1370
|
+
];
|
|
1371
|
+
pipeline.steps.forEach((r, i) => {
|
|
1372
|
+
lines.push(` [${i + 1}/${pipeline.steps.length}] ${r.runner_type} (${r.agent_role}) → ${r.run_id} · ${r.status} · ${(r.duration_ms / 1000).toFixed(1)}s · changed ${r.changed_files.length}`);
|
|
1373
|
+
});
|
|
1374
|
+
respondSuccess(command, Boolean(opts.json), pipeline, lines);
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
const result = await runTaskExecution({
|
|
1012
1378
|
rootPath,
|
|
1013
|
-
taskId
|
|
1014
|
-
runner:
|
|
1379
|
+
taskId,
|
|
1380
|
+
runner: runners[0],
|
|
1015
1381
|
agentRole: selectedAgentRole,
|
|
1016
1382
|
dryRun: Boolean(opts.dryRun),
|
|
1383
|
+
streamOutput: opts.json ? 'stderr' : 'stdout',
|
|
1017
1384
|
});
|
|
1018
1385
|
respondSuccess(command, Boolean(opts.json), result, [
|
|
1019
1386
|
`Prepared run ${result.run_id} for ${result.task_id}.`,
|
|
@@ -1022,6 +1389,9 @@ program
|
|
|
1022
1389
|
`Command: ${result.shell_command}`,
|
|
1023
1390
|
`Status: ${result.status}`,
|
|
1024
1391
|
`Summary: ${result.summary}`,
|
|
1392
|
+
`Changed files: ${result.changed_files.length}`,
|
|
1393
|
+
`Duration: ${(result.duration_ms / 1000).toFixed(1)}s`,
|
|
1394
|
+
`Log: ${result.log_path ?? 'n/a'}`,
|
|
1025
1395
|
]);
|
|
1026
1396
|
}
|
|
1027
1397
|
catch (err) {
|
|
@@ -1031,7 +1401,7 @@ program
|
|
|
1031
1401
|
const run = program
|
|
1032
1402
|
.command('run')
|
|
1033
1403
|
.description('Run task execution or inspect run records')
|
|
1034
|
-
.addHelpText('after', '\nExamples:\n $ sidecar run T-001 --dry-run\n $ sidecar run T-001 --runner claude --agent-role reviewer\n $ sidecar run queue\n $ sidecar run start-ready --dry-run\n $ sidecar run list --task T-001\n $ sidecar run show R-001');
|
|
1404
|
+
.addHelpText('after', '\nExamples:\n $ sidecar run T-001 --dry-run\n $ sidecar run T-001 --runner claude --agent-role reviewer\n $ sidecar run replay R-010 --edit-prompt\n $ sidecar run replay R-010 --runner claude --reason "second opinion"\n $ sidecar run queue\n $ sidecar run start-ready --dry-run\n $ sidecar run list --task T-001\n $ sidecar run show R-001');
|
|
1035
1405
|
run
|
|
1036
1406
|
.command('queue')
|
|
1037
1407
|
.description('Queue all ready tasks with satisfied dependencies')
|
|
@@ -1057,13 +1427,22 @@ run
|
|
|
1057
1427
|
.option('--dry-run', 'Prepare and compile only without executing external runners')
|
|
1058
1428
|
.option('--json', 'Print machine-readable JSON output')
|
|
1059
1429
|
.addHelpText('after', '\nExamples:\n $ sidecar run start-ready\n $ sidecar run start-ready --dry-run --json')
|
|
1060
|
-
.action((opts) => {
|
|
1430
|
+
.action(async (opts) => {
|
|
1061
1431
|
const command = 'run start-ready';
|
|
1062
1432
|
try {
|
|
1063
1433
|
const rootPath = resolveProjectRoot();
|
|
1064
1434
|
const queueDecisions = queueReadyTasks(rootPath);
|
|
1065
1435
|
const queuedTasks = listTaskPackets(rootPath).filter((task) => task.status === 'queued');
|
|
1066
|
-
const results =
|
|
1436
|
+
const results = [];
|
|
1437
|
+
for (const task of queuedTasks) {
|
|
1438
|
+
const result = await runTaskExecution({
|
|
1439
|
+
rootPath,
|
|
1440
|
+
taskId: task.task_id,
|
|
1441
|
+
dryRun: Boolean(opts.dryRun),
|
|
1442
|
+
streamOutput: opts.json ? 'stderr' : 'stdout',
|
|
1443
|
+
});
|
|
1444
|
+
results.push(result);
|
|
1445
|
+
}
|
|
1067
1446
|
respondSuccess(command, Boolean(opts.json), { queued: queueDecisions, results }, [
|
|
1068
1447
|
`Queued in this pass: ${queueDecisions.filter((d) => d.queued).length}`,
|
|
1069
1448
|
`Started: ${results.length}`,
|
|
@@ -1074,6 +1453,45 @@ run
|
|
|
1074
1453
|
handleCommandError(command, Boolean(opts.json), err);
|
|
1075
1454
|
}
|
|
1076
1455
|
});
|
|
1456
|
+
run
|
|
1457
|
+
.command('replay <run-id>')
|
|
1458
|
+
.description('Replay an existing run as a new run on the same task')
|
|
1459
|
+
.option('--runner <runner>', 'Override the runner for the replay (codex|claude)')
|
|
1460
|
+
.option('--agent-role <role>', 'Override the agent role (planner|builder-ui|builder-app|reviewer|tester)')
|
|
1461
|
+
.option('--reason <text>', 'Why you are replaying (stored on the new run)')
|
|
1462
|
+
.option('--edit-prompt', 'Open the compiled prompt in $EDITOR before executing')
|
|
1463
|
+
.option('--dry-run', 'Prepare and compile only without executing external runners')
|
|
1464
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
1465
|
+
.addHelpText('after', '\nExamples:\n $ sidecar run replay R-010\n $ sidecar run replay R-010 --runner claude --reason "second opinion"\n $ sidecar run replay R-010 --edit-prompt\n $ sidecar run replay R-010 --dry-run --json')
|
|
1466
|
+
.action(async (runIdText, opts) => {
|
|
1467
|
+
const command = 'run replay';
|
|
1468
|
+
try {
|
|
1469
|
+
const rootPath = resolveProjectRoot();
|
|
1470
|
+
const parentRunId = String(runIdText).trim().toUpperCase();
|
|
1471
|
+
const parent = getRunRecord(rootPath, parentRunId);
|
|
1472
|
+
const runner = opts.runner ? runnerTypeSchema.parse(opts.runner) : parent.runner_type;
|
|
1473
|
+
const agentRole = opts.agentRole ? agentRoleSchema.parse(opts.agentRole) : parent.agent_role;
|
|
1474
|
+
const result = await runTaskExecution({
|
|
1475
|
+
rootPath,
|
|
1476
|
+
taskId: parent.task_id,
|
|
1477
|
+
runner,
|
|
1478
|
+
agentRole: agentRole,
|
|
1479
|
+
dryRun: Boolean(opts.dryRun),
|
|
1480
|
+
streamOutput: opts.json ? 'stderr' : 'stdout',
|
|
1481
|
+
parentRunId,
|
|
1482
|
+
replayReason: opts.reason ? String(opts.reason) : undefined,
|
|
1483
|
+
editPrompt: Boolean(opts.editPrompt),
|
|
1484
|
+
});
|
|
1485
|
+
respondSuccess(command, Boolean(opts.json), result, [
|
|
1486
|
+
`Replayed ${parentRunId} as ${result.run_id} (${result.status}).`,
|
|
1487
|
+
`Runner: ${result.runner_type} · Role: ${result.agent_role}`,
|
|
1488
|
+
...(result.summary ? [result.summary] : []),
|
|
1489
|
+
]);
|
|
1490
|
+
}
|
|
1491
|
+
catch (err) {
|
|
1492
|
+
handleCommandError(command, Boolean(opts.json), err);
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1077
1495
|
run
|
|
1078
1496
|
.command('approve <run-id>')
|
|
1079
1497
|
.description('Review a completed run as approved, needs changes, or merged')
|
|
@@ -1147,13 +1565,18 @@ run
|
|
|
1147
1565
|
console.log('No run records found.');
|
|
1148
1566
|
return;
|
|
1149
1567
|
}
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1568
|
+
const tableRows = rows.map((r) => ({
|
|
1569
|
+
run_id: r.run_id,
|
|
1570
|
+
task_id: r.task_id,
|
|
1571
|
+
status: r.status,
|
|
1572
|
+
started: humanTime(r.started_at),
|
|
1573
|
+
}));
|
|
1574
|
+
renderTable([
|
|
1575
|
+
{ key: 'run_id', label: 'RUN ID', minWidth: 6 },
|
|
1576
|
+
{ key: 'task_id', label: 'TASK ID', minWidth: 7 },
|
|
1577
|
+
{ key: 'status', label: 'STATUS', minWidth: 8, format: formatStatus },
|
|
1578
|
+
{ key: 'started', label: 'STARTED', minWidth: 16 },
|
|
1579
|
+
], tableRows);
|
|
1157
1580
|
}
|
|
1158
1581
|
catch (err) {
|
|
1159
1582
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -1195,12 +1618,78 @@ run
|
|
|
1195
1618
|
respondSuccess(command, true, { run: runRecord }, []);
|
|
1196
1619
|
return;
|
|
1197
1620
|
}
|
|
1198
|
-
|
|
1621
|
+
printRunHuman(runRecord);
|
|
1199
1622
|
}
|
|
1200
1623
|
catch (err) {
|
|
1201
1624
|
handleCommandError(command, Boolean(opts.json), err);
|
|
1202
1625
|
}
|
|
1203
1626
|
});
|
|
1627
|
+
function printRunHuman(run) {
|
|
1628
|
+
console.log(`${c.bold(run.run_id)} — ${run.task_id} [${formatStatus(run.status)}]`);
|
|
1629
|
+
console.log(`Runner: ${run.runner_type} · Role: ${run.agent_role}`);
|
|
1630
|
+
console.log(`Started: ${humanTime(run.started_at)}${run.completed_at ? ` · Completed: ${humanTime(run.completed_at)}` : ''}`);
|
|
1631
|
+
if (run.parent_run_id) {
|
|
1632
|
+
const reason = run.replay_reason ? ` — ${run.replay_reason}` : '';
|
|
1633
|
+
console.log(`Replay of: ${c.cyan(run.parent_run_id)}${c.dim(reason)}`);
|
|
1634
|
+
}
|
|
1635
|
+
if (run.summary)
|
|
1636
|
+
console.log(`Summary: ${run.summary}`);
|
|
1637
|
+
// Show child replays if any (rooted lineage is rendered at the start of the tree).
|
|
1638
|
+
try {
|
|
1639
|
+
const rootPath = resolveProjectRoot();
|
|
1640
|
+
const children = listRunRecordsForTask(rootPath, run.task_id).filter((r) => r.parent_run_id === run.run_id);
|
|
1641
|
+
if (children.length > 0) {
|
|
1642
|
+
console.log(`Replayed as: ${children.map((r) => c.cyan(r.run_id)).join(', ')}`);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
catch {
|
|
1646
|
+
// ignore — lineage is a nice-to-have, not critical path
|
|
1647
|
+
}
|
|
1648
|
+
const isAuto = run.reviewed_by === 'sidecar:auto';
|
|
1649
|
+
const reviewLine = isAuto
|
|
1650
|
+
? `Review: ${formatStatus(run.review_state)} ${c.dim('(auto-approved)')}`
|
|
1651
|
+
: `Review: ${formatStatus(run.review_state)}${run.reviewed_by ? ` by ${run.reviewed_by}` : ''}`;
|
|
1652
|
+
console.log(reviewLine);
|
|
1653
|
+
if (run.review_note)
|
|
1654
|
+
console.log(` Note: ${run.review_note}`);
|
|
1655
|
+
if (run.validation.length > 0) {
|
|
1656
|
+
console.log('');
|
|
1657
|
+
console.log(c.bold('Validation:'));
|
|
1658
|
+
for (const v of run.validation) {
|
|
1659
|
+
const kindLabel = v.name ? `${v.kind}:${v.name}` : v.kind;
|
|
1660
|
+
const badge = v.ok ? c.green('✓ ok') : v.timed_out ? c.red('⏱ timed out') : c.red(`✗ failed (exit ${v.exit_code})`);
|
|
1661
|
+
const duration = `${(v.duration_ms / 1000).toFixed(1)}s`;
|
|
1662
|
+
console.log(` ${c.cyan(`[${kindLabel}]`)} ${badge} ${c.dim(duration)} ${v.command}`);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
else if (run.validation_results.length > 0) {
|
|
1666
|
+
// Legacy pre-kind records — still show them.
|
|
1667
|
+
console.log('');
|
|
1668
|
+
console.log(c.bold('Validation (legacy):'));
|
|
1669
|
+
for (const line of run.validation_results)
|
|
1670
|
+
console.log(` ${line}`);
|
|
1671
|
+
}
|
|
1672
|
+
if (run.changed_files.length > 0) {
|
|
1673
|
+
console.log('');
|
|
1674
|
+
console.log(c.bold(`Changed files (${run.changed_files.length}):`));
|
|
1675
|
+
for (const f of run.changed_files.slice(0, 20))
|
|
1676
|
+
console.log(` ${f}`);
|
|
1677
|
+
if (run.changed_files.length > 20)
|
|
1678
|
+
console.log(c.dim(` … ${run.changed_files.length - 20} more`));
|
|
1679
|
+
}
|
|
1680
|
+
if (run.blockers.length > 0) {
|
|
1681
|
+
console.log('');
|
|
1682
|
+
console.log(c.bold('Blockers:'));
|
|
1683
|
+
for (const b of run.blockers)
|
|
1684
|
+
console.log(` - ${b}`);
|
|
1685
|
+
}
|
|
1686
|
+
if (run.follow_ups.length > 0) {
|
|
1687
|
+
console.log('');
|
|
1688
|
+
console.log(c.bold('Follow-ups:'));
|
|
1689
|
+
for (const f of run.follow_ups)
|
|
1690
|
+
console.log(` - ${f}`);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1204
1693
|
const session = program.command('session').description('Session commands');
|
|
1205
1694
|
session
|
|
1206
1695
|
.command('start')
|
|
@@ -1340,13 +1829,22 @@ artifact
|
|
|
1340
1829
|
if (opts.json)
|
|
1341
1830
|
printJsonEnvelope(jsonSuccess(command, { artifacts: rows }));
|
|
1342
1831
|
else {
|
|
1343
|
-
|
|
1832
|
+
const typed = rows;
|
|
1833
|
+
if (typed.length === 0) {
|
|
1344
1834
|
console.log('No artifacts found.');
|
|
1345
1835
|
return;
|
|
1346
1836
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1837
|
+
renderTable([
|
|
1838
|
+
{ key: 'id', label: 'ID', align: 'right' },
|
|
1839
|
+
{ key: 'kind', label: 'Kind' },
|
|
1840
|
+
{ key: 'path', label: 'Path', maxWidth: 60 },
|
|
1841
|
+
{ key: 'note', label: 'Note', maxWidth: 40 },
|
|
1842
|
+
], typed.map((row) => ({
|
|
1843
|
+
id: `#${row.id}`,
|
|
1844
|
+
kind: row.kind,
|
|
1845
|
+
path: row.path,
|
|
1846
|
+
note: row.note ?? '',
|
|
1847
|
+
})));
|
|
1350
1848
|
}
|
|
1351
1849
|
}
|
|
1352
1850
|
catch (err) {
|
|
@@ -1361,6 +1859,108 @@ if (process.argv.length === 2) {
|
|
|
1361
1859
|
program.outputHelp();
|
|
1362
1860
|
process.exit(0);
|
|
1363
1861
|
}
|
|
1862
|
+
const hooks = program.command('hooks').description('Hook integration helpers');
|
|
1863
|
+
hooks
|
|
1864
|
+
.command('print')
|
|
1865
|
+
.description('Print a Claude Code settings.json hooks block wiring ambient capture')
|
|
1866
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
1867
|
+
.addHelpText('after', '\nCopy the output into .claude/settings.json (project) or ~/.claude/settings.json (user). Claude Code merges hook arrays across scopes.')
|
|
1868
|
+
.action((opts) => {
|
|
1869
|
+
const command = 'hooks print';
|
|
1870
|
+
try {
|
|
1871
|
+
const json = renderClaudeCodeHooksJson();
|
|
1872
|
+
if (opts.json) {
|
|
1873
|
+
printJsonEnvelope(jsonSuccess(command, { settings_json: json }));
|
|
1874
|
+
}
|
|
1875
|
+
else {
|
|
1876
|
+
console.log(json);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
catch (err) {
|
|
1880
|
+
handleCommandError(command, Boolean(opts.json), err);
|
|
1881
|
+
}
|
|
1882
|
+
});
|
|
1883
|
+
program
|
|
1884
|
+
.command('hook <event>')
|
|
1885
|
+
.description(`Ambient capture entry point for Claude Code / Codex hooks (event: ${HOOK_EVENTS.join('|')})`)
|
|
1886
|
+
.option('--actor-name <name>', 'Override the session actor_name (default: claude-code[:session])')
|
|
1887
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
1888
|
+
.addHelpText('after', '\nReads an optional JSON payload from stdin. Exit code is always 0 so hooks never block the caller — internal errors go to stderr.\n' +
|
|
1889
|
+
'\nExamples:\n' +
|
|
1890
|
+
' $ echo \'{"session_id":"abc"}\' | sidecar hook session-start\n' +
|
|
1891
|
+
' $ echo \'{"tool_name":"Edit","tool_input":{"file_path":"/abs/src/foo.ts"}}\' | sidecar hook file-edit\n' +
|
|
1892
|
+
' $ sidecar hook session-end')
|
|
1893
|
+
.action(async (eventArg, opts) => {
|
|
1894
|
+
const command = `hook ${eventArg}`;
|
|
1895
|
+
const asJson = Boolean(opts.json);
|
|
1896
|
+
try {
|
|
1897
|
+
const event = hookEventSchema.parse(eventArg);
|
|
1898
|
+
let payload = {};
|
|
1899
|
+
if (!process.stdin.isTTY) {
|
|
1900
|
+
const raw = (await readStdinText()).trim();
|
|
1901
|
+
if (raw.length > 0) {
|
|
1902
|
+
try {
|
|
1903
|
+
payload = hookPayloadSchema.parse(JSON.parse(raw));
|
|
1904
|
+
}
|
|
1905
|
+
catch (parseErr) {
|
|
1906
|
+
const msg = parseErr instanceof Error ? parseErr.message : String(parseErr);
|
|
1907
|
+
console.error(`sidecar hook: ignoring malformed payload (${msg})`);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
const { db, projectId, rootPath } = requireInitialized();
|
|
1912
|
+
const result = handleHookEvent({
|
|
1913
|
+
db,
|
|
1914
|
+
projectId,
|
|
1915
|
+
projectRoot: rootPath,
|
|
1916
|
+
event,
|
|
1917
|
+
payload,
|
|
1918
|
+
...(opts.actorName ? { actorName: String(opts.actorName) } : {}),
|
|
1919
|
+
});
|
|
1920
|
+
db.close();
|
|
1921
|
+
if (asJson) {
|
|
1922
|
+
printJsonEnvelope(jsonSuccess(command, result));
|
|
1923
|
+
}
|
|
1924
|
+
process.exit(0);
|
|
1925
|
+
}
|
|
1926
|
+
catch (err) {
|
|
1927
|
+
// Hooks must never block the caller — log to stderr and exit 0.
|
|
1928
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1929
|
+
if (asJson) {
|
|
1930
|
+
printJsonEnvelope(jsonSuccess(command, { ok: true, event: eventArg, action: 'skipped', detail: message }));
|
|
1931
|
+
}
|
|
1932
|
+
else {
|
|
1933
|
+
console.error(`sidecar hook: ${message}`);
|
|
1934
|
+
}
|
|
1935
|
+
process.exit(0);
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
program
|
|
1939
|
+
.command('log')
|
|
1940
|
+
.description('Memory namespace (alias group) — see `sidecar log --help`')
|
|
1941
|
+
.allowUnknownOption(true)
|
|
1942
|
+
.allowExcessArguments(true)
|
|
1943
|
+
.action(() => {
|
|
1944
|
+
printNamespaceHelp('log');
|
|
1945
|
+
})
|
|
1946
|
+
.addHelpText('after', `\nMembers:\n${LOG_NAMESPACE_MEMBERS.map((m) => ` sidecar log ${m} → sidecar ${m}`).join('\n')}\n`);
|
|
1947
|
+
program
|
|
1948
|
+
.command('work')
|
|
1949
|
+
.description('Runner namespace (alias group) — see `sidecar work --help`')
|
|
1950
|
+
.allowUnknownOption(true)
|
|
1951
|
+
.allowExcessArguments(true)
|
|
1952
|
+
.action(() => {
|
|
1953
|
+
printNamespaceHelp('work');
|
|
1954
|
+
})
|
|
1955
|
+
.addHelpText('after', `\nMembers:\n${WORK_NAMESPACE_MEMBERS.map((m) => ` sidecar work ${m} → sidecar ${m}`).join('\n')}\n`);
|
|
1956
|
+
// Rewrite `sidecar log <member> …` and `sidecar work <member> …` before
|
|
1957
|
+
// commander sees argv, so the verb's existing subcommand tree handles the call
|
|
1958
|
+
// verbatim (options, help, JSON envelopes, everything).
|
|
1959
|
+
const rewrittenArgv = rewriteNamespaceArgv(process.argv);
|
|
1960
|
+
if (rewrittenArgv !== process.argv) {
|
|
1961
|
+
process.argv.length = 0;
|
|
1962
|
+
process.argv.push(...rewrittenArgv);
|
|
1963
|
+
}
|
|
1364
1964
|
if (process.argv[2] === 'run' &&
|
|
1365
1965
|
process.argv[3] &&
|
|
1366
1966
|
!process.argv[3].startsWith('-') &&
|
|
@@ -1370,7 +1970,8 @@ if (process.argv[2] === 'run' &&
|
|
|
1370
1970
|
process.argv[3] !== 'start-ready' &&
|
|
1371
1971
|
process.argv[3] !== 'approve' &&
|
|
1372
1972
|
process.argv[3] !== 'block' &&
|
|
1373
|
-
process.argv[3] !== 'summary'
|
|
1973
|
+
process.argv[3] !== 'summary' &&
|
|
1974
|
+
process.argv[3] !== 'replay') {
|
|
1374
1975
|
process.argv.splice(2, 1, 'run-exec');
|
|
1375
1976
|
}
|
|
1376
1977
|
program.parse(process.argv);
|