sidecar-cli 0.1.4 → 0.1.5-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +257 -17
- package/dist/cli.js +673 -68
- 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 +102 -72
- package/dist/prompts/prompt-service.js +16 -4
- 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 +58 -0
- package/dist/runs/run-repository.js +2 -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, [
|
|
@@ -416,11 +481,164 @@ program
|
|
|
416
481
|
'Documentation: https://usesidecar.dev/',
|
|
417
482
|
...(resolvedInstructions ? ['', `Loaded instructions.md from ${resolvedInstructions.sourceLabel}`] : []),
|
|
418
483
|
]);
|
|
484
|
+
maybePrintUpdateNotice();
|
|
419
485
|
}
|
|
420
486
|
catch (err) {
|
|
421
487
|
handleCommandError(command, Boolean(opts.json), err);
|
|
422
488
|
}
|
|
423
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
|
+
});
|
|
424
642
|
program
|
|
425
643
|
.command('status')
|
|
426
644
|
.description('Show Sidecar status and recent project activity')
|
|
@@ -665,14 +883,24 @@ program
|
|
|
665
883
|
if (opts.json)
|
|
666
884
|
printJsonEnvelope(jsonSuccess(command, { events: rows }));
|
|
667
885
|
else {
|
|
668
|
-
|
|
886
|
+
const typed = rows;
|
|
887
|
+
if (typed.length === 0) {
|
|
669
888
|
console.log('No events found.');
|
|
670
889
|
return;
|
|
671
890
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
+
})));
|
|
676
904
|
}
|
|
677
905
|
}
|
|
678
906
|
catch (err) {
|
|
@@ -789,7 +1017,7 @@ task
|
|
|
789
1017
|
.option('--files-avoid <paths>', 'Comma-separated files to avoid')
|
|
790
1018
|
.option('--constraint-tech <items>', 'Comma-separated technical constraints')
|
|
791
1019
|
.option('--constraint-design <items>', 'Comma-separated design constraints')
|
|
792
|
-
.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".')
|
|
793
1021
|
.option('--dod <items>', 'Comma-separated definition-of-done checks')
|
|
794
1022
|
.option('--branch <name>', 'Branch name')
|
|
795
1023
|
.option('--worktree <path>', 'Worktree path')
|
|
@@ -887,13 +1115,18 @@ task
|
|
|
887
1115
|
console.log('No tasks found.');
|
|
888
1116
|
return;
|
|
889
1117
|
}
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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);
|
|
897
1130
|
}
|
|
898
1131
|
catch (err) {
|
|
899
1132
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -945,23 +1178,131 @@ task
|
|
|
945
1178
|
}
|
|
946
1179
|
});
|
|
947
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
|
+
}
|
|
948
1206
|
prompt
|
|
949
|
-
.command('compile <task-
|
|
950
|
-
.description('Compile a markdown execution brief from a task
|
|
951
|
-
.
|
|
952
|
-
.
|
|
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)')
|
|
953
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')
|
|
954
1218
|
.option('--json', 'Print machine-readable JSON output')
|
|
955
|
-
.addHelpText('after', '\nExamples:\n
|
|
956
|
-
|
|
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) => {
|
|
957
1238
|
const command = 'prompt compile';
|
|
958
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).
|
|
959
1298
|
const rootPath = resolveProjectRoot();
|
|
960
|
-
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');
|
|
961
1304
|
const runner = runnerTypeSchema.parse(opts.runner);
|
|
962
1305
|
const agentRole = String(opts.agentRole ?? '').trim();
|
|
963
|
-
if (!agentRole)
|
|
964
|
-
fail('Agent role is required');
|
|
965
1306
|
const compiled = compileTaskPrompt({
|
|
966
1307
|
rootPath,
|
|
967
1308
|
taskId,
|
|
@@ -974,11 +1315,16 @@ prompt
|
|
|
974
1315
|
runner_type: compiled.runner_type,
|
|
975
1316
|
agent_role: compiled.agent_role,
|
|
976
1317
|
prompt_path: compiled.prompt_path,
|
|
1318
|
+
prompt_optimization: compiled.prompt_optimization,
|
|
977
1319
|
preview: opts.preview ? compiled.prompt_markdown : null,
|
|
978
1320
|
}, [
|
|
979
1321
|
`Compiled prompt for ${compiled.task_id}.`,
|
|
980
1322
|
`Run: ${compiled.run_id}`,
|
|
981
1323
|
`Path: ${compiled.prompt_path}`,
|
|
1324
|
+
`Prompt estimate: ${compiled.prompt_optimization.estimated_tokens_before} -> ${compiled.prompt_optimization.estimated_tokens_after} tokens (target ${compiled.prompt_optimization.budget_target})`,
|
|
1325
|
+
...(compiled.prompt_optimization.trimmed_sections.length > 0
|
|
1326
|
+
? [`Trimmed: ${compiled.prompt_optimization.trimmed_sections.join(', ')}`]
|
|
1327
|
+
: []),
|
|
982
1328
|
...(opts.preview ? ['', compiled.prompt_markdown] : []),
|
|
983
1329
|
]);
|
|
984
1330
|
}
|
|
@@ -989,25 +1335,52 @@ prompt
|
|
|
989
1335
|
program
|
|
990
1336
|
.command('run-exec <task-id>')
|
|
991
1337
|
.description('Internal command backing `sidecar run <task-id>`')
|
|
992
|
-
.option('--runner <runner>', 'codex|claude')
|
|
1338
|
+
.option('--runner <runner>', 'codex|claude — comma-separated for a dual-runner pipeline (e.g. codex,claude)')
|
|
993
1339
|
.option('--agent-role <role>', 'planner|builder-ui|builder-app|reviewer|tester')
|
|
994
1340
|
.option('--dry-run', 'Prepare and compile only without executing external runner')
|
|
995
1341
|
.option('--json', 'Print machine-readable JSON output')
|
|
996
|
-
.action((taskIdText, opts) => {
|
|
1342
|
+
.action(async (taskIdText, opts) => {
|
|
997
1343
|
const command = 'run';
|
|
998
1344
|
try {
|
|
999
1345
|
const rootPath = resolveProjectRoot();
|
|
1000
1346
|
const defaults = loadRunnerPreferences(rootPath);
|
|
1001
|
-
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];
|
|
1002
1355
|
const selectedAgentRole = opts.agentRole
|
|
1003
1356
|
? agentRoleSchema.parse(opts.agentRole)
|
|
1004
1357
|
: defaults.default_agent_role;
|
|
1005
|
-
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({
|
|
1006
1378
|
rootPath,
|
|
1007
|
-
taskId
|
|
1008
|
-
runner:
|
|
1379
|
+
taskId,
|
|
1380
|
+
runner: runners[0],
|
|
1009
1381
|
agentRole: selectedAgentRole,
|
|
1010
1382
|
dryRun: Boolean(opts.dryRun),
|
|
1383
|
+
streamOutput: opts.json ? 'stderr' : 'stdout',
|
|
1011
1384
|
});
|
|
1012
1385
|
respondSuccess(command, Boolean(opts.json), result, [
|
|
1013
1386
|
`Prepared run ${result.run_id} for ${result.task_id}.`,
|
|
@@ -1016,6 +1389,9 @@ program
|
|
|
1016
1389
|
`Command: ${result.shell_command}`,
|
|
1017
1390
|
`Status: ${result.status}`,
|
|
1018
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'}`,
|
|
1019
1395
|
]);
|
|
1020
1396
|
}
|
|
1021
1397
|
catch (err) {
|
|
@@ -1025,7 +1401,7 @@ program
|
|
|
1025
1401
|
const run = program
|
|
1026
1402
|
.command('run')
|
|
1027
1403
|
.description('Run task execution or inspect run records')
|
|
1028
|
-
.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');
|
|
1029
1405
|
run
|
|
1030
1406
|
.command('queue')
|
|
1031
1407
|
.description('Queue all ready tasks with satisfied dependencies')
|
|
@@ -1051,13 +1427,22 @@ run
|
|
|
1051
1427
|
.option('--dry-run', 'Prepare and compile only without executing external runners')
|
|
1052
1428
|
.option('--json', 'Print machine-readable JSON output')
|
|
1053
1429
|
.addHelpText('after', '\nExamples:\n $ sidecar run start-ready\n $ sidecar run start-ready --dry-run --json')
|
|
1054
|
-
.action((opts) => {
|
|
1430
|
+
.action(async (opts) => {
|
|
1055
1431
|
const command = 'run start-ready';
|
|
1056
1432
|
try {
|
|
1057
1433
|
const rootPath = resolveProjectRoot();
|
|
1058
1434
|
const queueDecisions = queueReadyTasks(rootPath);
|
|
1059
1435
|
const queuedTasks = listTaskPackets(rootPath).filter((task) => task.status === 'queued');
|
|
1060
|
-
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
|
+
}
|
|
1061
1446
|
respondSuccess(command, Boolean(opts.json), { queued: queueDecisions, results }, [
|
|
1062
1447
|
`Queued in this pass: ${queueDecisions.filter((d) => d.queued).length}`,
|
|
1063
1448
|
`Started: ${results.length}`,
|
|
@@ -1068,6 +1453,45 @@ run
|
|
|
1068
1453
|
handleCommandError(command, Boolean(opts.json), err);
|
|
1069
1454
|
}
|
|
1070
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
|
+
});
|
|
1071
1495
|
run
|
|
1072
1496
|
.command('approve <run-id>')
|
|
1073
1497
|
.description('Review a completed run as approved, needs changes, or merged')
|
|
@@ -1141,13 +1565,18 @@ run
|
|
|
1141
1565
|
console.log('No run records found.');
|
|
1142
1566
|
return;
|
|
1143
1567
|
}
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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);
|
|
1151
1580
|
}
|
|
1152
1581
|
catch (err) {
|
|
1153
1582
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -1189,12 +1618,78 @@ run
|
|
|
1189
1618
|
respondSuccess(command, true, { run: runRecord }, []);
|
|
1190
1619
|
return;
|
|
1191
1620
|
}
|
|
1192
|
-
|
|
1621
|
+
printRunHuman(runRecord);
|
|
1193
1622
|
}
|
|
1194
1623
|
catch (err) {
|
|
1195
1624
|
handleCommandError(command, Boolean(opts.json), err);
|
|
1196
1625
|
}
|
|
1197
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
|
+
}
|
|
1198
1693
|
const session = program.command('session').description('Session commands');
|
|
1199
1694
|
session
|
|
1200
1695
|
.command('start')
|
|
@@ -1334,13 +1829,22 @@ artifact
|
|
|
1334
1829
|
if (opts.json)
|
|
1335
1830
|
printJsonEnvelope(jsonSuccess(command, { artifacts: rows }));
|
|
1336
1831
|
else {
|
|
1337
|
-
|
|
1832
|
+
const typed = rows;
|
|
1833
|
+
if (typed.length === 0) {
|
|
1338
1834
|
console.log('No artifacts found.');
|
|
1339
1835
|
return;
|
|
1340
1836
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
+
})));
|
|
1344
1848
|
}
|
|
1345
1849
|
}
|
|
1346
1850
|
catch (err) {
|
|
@@ -1353,9 +1857,110 @@ if (process.argv.length === 2) {
|
|
|
1353
1857
|
console.log('');
|
|
1354
1858
|
}
|
|
1355
1859
|
program.outputHelp();
|
|
1356
|
-
maybePrintUpdateNotice();
|
|
1357
1860
|
process.exit(0);
|
|
1358
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
|
+
}
|
|
1359
1964
|
if (process.argv[2] === 'run' &&
|
|
1360
1965
|
process.argv[3] &&
|
|
1361
1966
|
!process.argv[3].startsWith('-') &&
|
|
@@ -1365,8 +1970,8 @@ if (process.argv[2] === 'run' &&
|
|
|
1365
1970
|
process.argv[3] !== 'start-ready' &&
|
|
1366
1971
|
process.argv[3] !== 'approve' &&
|
|
1367
1972
|
process.argv[3] !== 'block' &&
|
|
1368
|
-
process.argv[3] !== 'summary'
|
|
1973
|
+
process.argv[3] !== 'summary' &&
|
|
1974
|
+
process.argv[3] !== 'replay') {
|
|
1369
1975
|
process.argv.splice(2, 1, 'run-exec');
|
|
1370
1976
|
}
|
|
1371
1977
|
program.parse(process.argv);
|
|
1372
|
-
maybePrintUpdateNotice();
|