sidecar-cli 0.1.1-rc.1 → 0.1.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import readline from 'node:readline/promises';
4
5
  import { Command } from 'commander';
5
6
  import Database from 'better-sqlite3';
6
7
  import { z } from 'zod';
@@ -19,15 +20,24 @@ import { buildContext } from './services/context-service.js';
19
20
  import { getCapabilitiesManifest } from './services/capabilities-service.js';
20
21
  import { addArtifact, listArtifacts } from './services/artifact-service.js';
21
22
  import { addDecision, addNote, addWorklog, getActiveSessionId, listRecentEvents } from './services/event-service.js';
22
- import { addTask, listTasks, markTaskDone } from './services/task-service.js';
23
23
  import { currentSession, endSession, startSession, verifySessionHygiene } from './services/session-service.js';
24
24
  import { eventIngestSchema, ingestEvent } from './services/event-ingest-service.js';
25
25
  import { buildExportJson, buildExportJsonlEvents, writeOutputFile } from './services/export-service.js';
26
+ import { createTaskPacketRecord, getTaskPacket, listTaskPackets } from './tasks/task-service.js';
27
+ import { taskPacketPrioritySchema, taskPacketStatusSchema, taskPacketTypeSchema } from './tasks/task-packet.js';
28
+ import { getRunRecord, listRunRecords, listRunRecordsForTask } from './runs/run-service.js';
29
+ import { runStatusSchema, runnerTypeSchema } from './runs/run-record.js';
30
+ import { compileTaskPrompt } from './prompts/prompt-service.js';
31
+ import { runTaskExecution } from './services/run-orchestrator-service.js';
32
+ import { loadRunnerPreferences } from './runners/config.js';
33
+ import { assignTask, queueReadyTasks } from './services/task-orchestration-service.js';
34
+ import { buildReviewSummary, createFollowupTaskFromRun, reviewRun } from './services/run-review-service.js';
26
35
  const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
27
36
  const actorSchema = z.enum(['human', 'agent']);
28
- const taskPrioritySchema = z.enum(['low', 'medium', 'high']);
29
37
  const artifactKindSchema = z.enum(['file', 'doc', 'screenshot', 'other']);
30
- const taskStatusSchema = z.enum(['open', 'done', 'all']);
38
+ const taskListStatusSchema = z.enum(['draft', 'ready', 'queued', 'running', 'review', 'blocked', 'done', 'all']);
39
+ const runListStatusSchema = runStatusSchema.or(z.literal('all'));
40
+ const agentRoleSchema = z.enum(['planner', 'builder-ui', 'builder-app', 'reviewer', 'tester']);
31
41
  const exportFormatSchema = z.enum(['json', 'jsonl']);
32
42
  const NOT_INITIALIZED_MSG = 'Sidecar is not initialized in this directory or any parent directory';
33
43
  function fail(message) {
@@ -185,6 +195,21 @@ function resolveProjectRoot(projectPath) {
185
195
  }
186
196
  return root;
187
197
  }
198
+ function parseCsvOption(input) {
199
+ if (!input)
200
+ return [];
201
+ return input
202
+ .split(',')
203
+ .map((item) => item.trim())
204
+ .filter(Boolean);
205
+ }
206
+ async function askWithDefault(rl, question, fallback) {
207
+ const suffix = fallback ? ` [${fallback}]` : '';
208
+ const answer = (await rl.question(`${question}${suffix}: `)).trim();
209
+ if (answer.length > 0)
210
+ return answer;
211
+ return fallback ?? '';
212
+ }
188
213
  const program = new Command();
189
214
  program.name('sidecar').description('Local-first project memory and recording CLI').version(pkg.version);
190
215
  program.option('--no-banner', 'Disable Sidecar banner output');
@@ -275,6 +300,9 @@ program
275
300
  fail('Sidecar is already initialized in this project. Re-run with --force to recreate .sidecar files.');
276
301
  }
277
302
  const files = [
303
+ sidecar.tasksPath,
304
+ sidecar.runsPath,
305
+ sidecar.promptsPath,
278
306
  sidecar.dbPath,
279
307
  sidecar.configPath,
280
308
  sidecar.preferencesPath,
@@ -297,9 +325,12 @@ program
297
325
  sidecar.preferencesPath,
298
326
  sidecar.agentsPath,
299
327
  sidecar.summaryPath,
328
+ sidecar.tasksPath,
329
+ sidecar.runsPath,
330
+ sidecar.promptsPath,
300
331
  ]) {
301
332
  if (fs.existsSync(file))
302
- fs.rmSync(file);
333
+ fs.rmSync(file, { recursive: true, force: true });
303
334
  }
304
335
  }
305
336
  const db = new Database(sidecar.dbPath);
@@ -315,9 +346,17 @@ program
315
346
  settings: {},
316
347
  };
317
348
  fs.writeFileSync(sidecar.configPath, stringifyJson(config));
349
+ fs.mkdirSync(sidecar.tasksPath, { recursive: true });
350
+ fs.mkdirSync(sidecar.runsPath, { recursive: true });
351
+ fs.mkdirSync(sidecar.promptsPath, { recursive: true });
318
352
  fs.writeFileSync(sidecar.preferencesPath, stringifyJson({
319
353
  summary: { format: 'markdown', recentLimit: 8 },
320
354
  output: { humanTime: true },
355
+ runner: {
356
+ defaultRunner: 'codex',
357
+ preferredRunners: ['codex', 'claude'],
358
+ defaultAgentRole: 'builder-app',
359
+ },
321
360
  }));
322
361
  fs.writeFileSync(sidecar.agentsPath, renderAgentsMarkdown(projectName));
323
362
  if (shouldWriteRootAgents) {
@@ -705,46 +744,98 @@ worklog
705
744
  });
706
745
  const task = program.command('task').description('Task commands');
707
746
  task
708
- .command('add <title>')
709
- .description('Create an open task')
710
- .option('--description <text>', 'Description')
747
+ .command('create')
748
+ .description('Create a structured task packet')
749
+ .option('--title <title>', 'Task title')
750
+ .option('--type <type>', 'feature|bug|chore|research', 'chore')
751
+ .option('--status <status>', 'draft|ready|queued|running|review|blocked|done', 'draft')
711
752
  .option('--priority <priority>', 'low|medium|high', 'medium')
712
- .option('--by <actor>', 'human|agent', 'human')
753
+ .option('--summary <summary>', 'Task summary')
754
+ .option('--goal <goal>', 'Task goal')
755
+ .option('--dependencies <task-ids>', 'Comma-separated dependency task IDs')
756
+ .option('--tags <tags>', 'Comma-separated tags')
757
+ .option('--target-areas <areas>', 'Comma-separated target areas')
758
+ .option('--scope-in <items>', 'Comma-separated in-scope items')
759
+ .option('--scope-out <items>', 'Comma-separated out-of-scope items')
760
+ .option('--related-decisions <items>', 'Comma-separated related decision IDs/titles')
761
+ .option('--related-notes <items>', 'Comma-separated related notes')
762
+ .option('--files-read <paths>', 'Comma-separated files to read')
763
+ .option('--files-avoid <paths>', 'Comma-separated files to avoid')
764
+ .option('--constraint-tech <items>', 'Comma-separated technical constraints')
765
+ .option('--constraint-design <items>', 'Comma-separated design constraints')
766
+ .option('--validate-cmds <commands>', 'Comma-separated validation commands')
767
+ .option('--dod <items>', 'Comma-separated definition-of-done checks')
768
+ .option('--branch <name>', 'Branch name')
769
+ .option('--worktree <path>', 'Worktree path')
713
770
  .option('--json', 'Print machine-readable JSON output')
714
- .addHelpText('after', '\nExamples:\n $ sidecar task add "Ship v0.1"\n $ sidecar task add "Add tests" --priority high --by agent')
715
- .action((title, opts) => {
716
- const command = 'task add';
771
+ .addHelpText('after', '\nExamples:\n $ sidecar task create\n $ sidecar task create --title "Add import support" --summary "Support JSON import" --goal "Enable scripted import flow" --priority high\n $ sidecar task create --title "Refactor parser" --files-read src/parser.ts,src/types.ts --dod "Tests pass,Docs updated"')
772
+ .action(async (opts) => {
773
+ const command = 'task create';
717
774
  try {
718
- const priority = taskPrioritySchema.parse(opts.priority);
719
- const by = actorSchema.parse(opts.by);
720
- const { db, projectId } = requireInitialized();
721
- const result = addTask(db, { projectId, title, description: opts.description, priority, by });
722
- db.close();
723
- respondSuccess(command, Boolean(opts.json), { task: { id: result.taskId, title, description: opts.description ?? null, status: 'open', priority }, event: { id: result.eventId, type: 'task_created' } }, [`Added task #${result.taskId}.`]);
775
+ const rootPath = resolveProjectRoot();
776
+ let title = opts.title?.trim() ?? '';
777
+ let summary = opts.summary?.trim() ?? '';
778
+ let goal = opts.goal?.trim() ?? '';
779
+ if (!title || !summary || !goal) {
780
+ if (!process.stdin.isTTY) {
781
+ fail('Missing required fields. Provide --title, --summary, and --goal when not running interactively.');
782
+ }
783
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
784
+ try {
785
+ title = title || (await askWithDefault(rl, 'Title'));
786
+ summary = summary || (await askWithDefault(rl, 'Summary', title));
787
+ goal = goal || (await askWithDefault(rl, 'Goal', `Complete: ${title}`));
788
+ }
789
+ finally {
790
+ rl.close();
791
+ }
792
+ }
793
+ const type = taskPacketTypeSchema.parse(opts.type);
794
+ const status = taskPacketStatusSchema.parse(opts.status);
795
+ const priority = taskPacketPrioritySchema.parse(opts.priority);
796
+ const created = createTaskPacketRecord(rootPath, {
797
+ title,
798
+ summary,
799
+ goal,
800
+ type,
801
+ status,
802
+ priority,
803
+ scope_in_scope: parseCsvOption(opts.scopeIn),
804
+ scope_out_of_scope: parseCsvOption(opts.scopeOut),
805
+ related_decisions: parseCsvOption(opts.relatedDecisions),
806
+ related_notes: parseCsvOption(opts.relatedNotes),
807
+ files_to_read: parseCsvOption(opts.filesRead),
808
+ files_to_avoid: parseCsvOption(opts.filesAvoid),
809
+ technical_constraints: parseCsvOption(opts.constraintTech),
810
+ design_constraints: parseCsvOption(opts.constraintDesign),
811
+ validation_commands: parseCsvOption(opts.validateCmds),
812
+ dependencies: parseCsvOption(opts.dependencies).map((v) => v.toUpperCase()),
813
+ tags: parseCsvOption(opts.tags),
814
+ target_areas: parseCsvOption(opts.targetAreas),
815
+ definition_of_done: parseCsvOption(opts.dod),
816
+ branch: opts.branch?.trim(),
817
+ worktree: opts.worktree?.trim(),
818
+ });
819
+ respondSuccess(command, Boolean(opts.json), { task: created.task, path: created.path }, [`Created task ${created.task.task_id}.`, `Path: ${created.path}`]);
724
820
  }
725
821
  catch (err) {
726
822
  handleCommandError(command, Boolean(opts.json), err);
727
823
  }
728
824
  });
729
825
  task
730
- .command('done <task-id>')
731
- .description('Mark a task as done')
732
- .option('--by <actor>', 'human|agent', 'human')
826
+ .command('show <task-id>')
827
+ .description('Show a task packet by id')
733
828
  .option('--json', 'Print machine-readable JSON output')
734
- .addHelpText('after', '\nExamples:\n $ sidecar task done 3\n $ sidecar task done 3 --json')
829
+ .addHelpText('after', '\nExamples:\n $ sidecar task show T-001\n $ sidecar task show T-001 --json')
735
830
  .action((taskIdText, opts) => {
736
- const command = 'task done';
831
+ const command = 'task show';
737
832
  try {
738
- const taskId = Number.parseInt(taskIdText, 10);
739
- if (!Number.isInteger(taskId) || taskId <= 0)
740
- fail('Task id must be a positive integer');
741
- const by = actorSchema.parse(opts.by);
742
- const { db, projectId } = requireInitialized();
743
- const result = markTaskDone(db, { projectId, taskId, by });
744
- db.close();
745
- if (!result.ok)
746
- fail(result.reason);
747
- respondSuccess(command, Boolean(opts.json), { task: { id: taskId, status: 'done' }, event: { id: result.eventId, type: 'task_completed' } }, [`Completed task #${taskId}.`]);
833
+ const task = getTaskPacket(resolveProjectRoot(), taskIdText.trim().toUpperCase());
834
+ if (opts.json) {
835
+ respondSuccess(command, true, { task }, []);
836
+ return;
837
+ }
838
+ console.log(stringifyJson(task));
748
839
  }
749
840
  catch (err) {
750
841
  handleCommandError(command, Boolean(opts.json), err);
@@ -752,40 +843,327 @@ task
752
843
  });
753
844
  task
754
845
  .command('list')
755
- .description('List tasks by status')
756
- .option('--status <status>', 'open|done|all', 'open')
757
- .option('--format <format>', 'table|json', 'table')
846
+ .description('List task packets')
847
+ .option('--status <status>', 'draft|ready|queued|running|review|blocked|done|all', 'all')
758
848
  .option('--json', 'Print machine-readable JSON output')
759
- .addHelpText('after', '\nExamples:\n $ sidecar task list\n $ sidecar task list --status all --format json')
849
+ .addHelpText('after', '\nExamples:\n $ sidecar task list\n $ sidecar task list --status open\n $ sidecar task list --json')
760
850
  .action((opts) => {
761
851
  const command = 'task list';
762
852
  try {
763
- const status = taskStatusSchema.parse(opts.status);
764
- const { db, projectId } = requireInitialized();
765
- const rows = listTasks(db, { projectId, status });
766
- db.close();
767
- if (opts.format === 'json' || opts.json) {
768
- if (opts.json)
769
- printJsonEnvelope(jsonSuccess(command, { status, tasks: rows }));
770
- else
771
- console.log(stringifyJson(rows));
853
+ const status = taskListStatusSchema.parse(opts.status);
854
+ const tasks = listTaskPackets(resolveProjectRoot());
855
+ const rows = status === 'all' ? tasks : tasks.filter((task) => task.status === status);
856
+ if (opts.json) {
857
+ respondSuccess(command, true, { status, tasks: rows }, []);
772
858
  return;
773
859
  }
774
- const taskRows = rows;
775
- if (taskRows.length === 0) {
860
+ if (rows.length === 0) {
776
861
  console.log('No tasks found.');
777
862
  return;
778
863
  }
779
- const idWidth = Math.max(2, ...taskRows.map((r) => String(r.id).length));
780
- const statusWidth = Math.max(6, ...taskRows.map((r) => r.status.length));
781
- const priorityWidth = Math.max(8, ...taskRows.map((r) => (r.priority ?? 'n/a').length));
782
- console.log(`${'ID'.padEnd(idWidth)} ${'STATUS'.padEnd(statusWidth)} ${'PRIORITY'.padEnd(priorityWidth)} TITLE`);
783
- for (const row of taskRows) {
784
- const id = String(row.id).padEnd(idWidth);
785
- const statusLabel = row.status.padEnd(statusWidth);
786
- const prio = (row.priority ?? 'n/a').padEnd(priorityWidth);
787
- console.log(`${id} ${statusLabel} ${prio} ${row.title}`);
864
+ const idWidth = Math.max(6, ...rows.map((r) => r.task_id.length));
865
+ const statusWidth = Math.max(11, ...rows.map((r) => r.status.length));
866
+ const priorityWidth = Math.max(8, ...rows.map((r) => r.priority.length));
867
+ console.log(`${'TASK ID'.padEnd(idWidth)} ${'STATUS'.padEnd(statusWidth)} ${'PRIORITY'.padEnd(priorityWidth)} TITLE`);
868
+ for (const row of rows) {
869
+ console.log(`${row.task_id.padEnd(idWidth)} ${row.status.padEnd(statusWidth)} ${row.priority.padEnd(priorityWidth)} ${row.title}`);
870
+ }
871
+ }
872
+ catch (err) {
873
+ handleCommandError(command, Boolean(opts.json), err);
874
+ }
875
+ });
876
+ task
877
+ .command('assign <task-id>')
878
+ .description('Auto-assign agent role and runner for a task')
879
+ .option('--agent-role <role>', 'planner|builder-ui|builder-app|reviewer|tester')
880
+ .option('--runner <runner>', 'codex|claude')
881
+ .option('--json', 'Print machine-readable JSON output')
882
+ .addHelpText('after', '\nExamples:\n $ sidecar task assign T-001\n $ sidecar task assign T-001 --agent-role builder-ui --runner codex\n $ sidecar task assign T-001 --json')
883
+ .action((taskIdText, opts) => {
884
+ const command = 'task assign';
885
+ try {
886
+ const rootPath = resolveProjectRoot();
887
+ const result = assignTask(rootPath, taskIdText.trim().toUpperCase(), {
888
+ role: opts.agentRole ? agentRoleSchema.parse(opts.agentRole) : undefined,
889
+ runner: opts.runner ? runnerTypeSchema.parse(opts.runner) : undefined,
890
+ });
891
+ respondSuccess(command, Boolean(opts.json), result, [
892
+ `Assigned ${result.task_id}.`,
893
+ `Role: ${result.agent_role}`,
894
+ `Runner: ${result.runner}`,
895
+ `Reason: ${result.reason}`,
896
+ ]);
897
+ }
898
+ catch (err) {
899
+ handleCommandError(command, Boolean(opts.json), err);
900
+ }
901
+ });
902
+ task
903
+ .command('create-followup <run-id>')
904
+ .description('Create a follow-up task packet from a run report')
905
+ .option('--json', 'Print machine-readable JSON output')
906
+ .addHelpText('after', '\nExamples:\n $ sidecar task create-followup R-010\n $ sidecar task create-followup R-010 --json')
907
+ .action((runIdText, opts) => {
908
+ const command = 'task create-followup';
909
+ try {
910
+ const result = createFollowupTaskFromRun(resolveProjectRoot(), runIdText.trim().toUpperCase());
911
+ respondSuccess(command, Boolean(opts.json), result, [
912
+ `Created follow-up task ${result.task_id}.`,
913
+ `Source run: ${result.source_run_id}`,
914
+ `Title: ${result.title}`,
915
+ ]);
916
+ }
917
+ catch (err) {
918
+ handleCommandError(command, Boolean(opts.json), err);
919
+ }
920
+ });
921
+ const prompt = program.command('prompt').description('Prompt compilation commands');
922
+ prompt
923
+ .command('compile <task-id>')
924
+ .description('Compile a markdown execution brief from a task packet')
925
+ .requiredOption('--runner <runner>', 'codex|claude')
926
+ .requiredOption('--agent-role <role>', 'Agent role, for example builder')
927
+ .option('--preview', 'Print compiled prompt content after writing file')
928
+ .option('--json', 'Print machine-readable JSON output')
929
+ .addHelpText('after', '\nExamples:\n $ sidecar prompt compile T-001 --runner codex --agent-role builder\n $ sidecar prompt compile T-001 --runner claude --agent-role builder --preview\n $ sidecar prompt compile T-001 --runner codex --agent-role reviewer --json')
930
+ .action((taskIdText, opts) => {
931
+ const command = 'prompt compile';
932
+ try {
933
+ const rootPath = resolveProjectRoot();
934
+ const taskId = taskIdText.trim().toUpperCase();
935
+ const runner = runnerTypeSchema.parse(opts.runner);
936
+ const agentRole = String(opts.agentRole ?? '').trim();
937
+ if (!agentRole)
938
+ fail('Agent role is required');
939
+ const compiled = compileTaskPrompt({
940
+ rootPath,
941
+ taskId,
942
+ runner,
943
+ agentRole,
944
+ });
945
+ respondSuccess(command, Boolean(opts.json), {
946
+ run_id: compiled.run_id,
947
+ task_id: compiled.task_id,
948
+ runner_type: compiled.runner_type,
949
+ agent_role: compiled.agent_role,
950
+ prompt_path: compiled.prompt_path,
951
+ preview: opts.preview ? compiled.prompt_markdown : null,
952
+ }, [
953
+ `Compiled prompt for ${compiled.task_id}.`,
954
+ `Run: ${compiled.run_id}`,
955
+ `Path: ${compiled.prompt_path}`,
956
+ ...(opts.preview ? ['', compiled.prompt_markdown] : []),
957
+ ]);
958
+ }
959
+ catch (err) {
960
+ handleCommandError(command, Boolean(opts.json), err);
961
+ }
962
+ });
963
+ program
964
+ .command('run-exec <task-id>')
965
+ .description('Internal command backing `sidecar run <task-id>`')
966
+ .option('--runner <runner>', 'codex|claude')
967
+ .option('--agent-role <role>', 'planner|builder-ui|builder-app|reviewer|tester')
968
+ .option('--dry-run', 'Prepare and compile only without executing external runner')
969
+ .option('--json', 'Print machine-readable JSON output')
970
+ .action((taskIdText, opts) => {
971
+ const command = 'run';
972
+ try {
973
+ const rootPath = resolveProjectRoot();
974
+ const defaults = loadRunnerPreferences(rootPath);
975
+ const selectedRunner = opts.runner ? runnerTypeSchema.parse(opts.runner) : defaults.default_runner;
976
+ const selectedAgentRole = opts.agentRole
977
+ ? agentRoleSchema.parse(opts.agentRole)
978
+ : defaults.default_agent_role;
979
+ const result = runTaskExecution({
980
+ rootPath,
981
+ taskId: String(taskIdText).trim().toUpperCase(),
982
+ runner: selectedRunner,
983
+ agentRole: selectedAgentRole,
984
+ dryRun: Boolean(opts.dryRun),
985
+ });
986
+ respondSuccess(command, Boolean(opts.json), result, [
987
+ `Prepared run ${result.run_id} for ${result.task_id}.`,
988
+ `Runner: ${result.runner_type} (${result.agent_role})`,
989
+ `Prompt: ${result.prompt_path}`,
990
+ `Command: ${result.shell_command}`,
991
+ `Status: ${result.status}`,
992
+ `Summary: ${result.summary}`,
993
+ ]);
994
+ }
995
+ catch (err) {
996
+ handleCommandError(command, Boolean(opts.json), err);
997
+ }
998
+ });
999
+ const run = program
1000
+ .command('run')
1001
+ .description('Run task execution or inspect run records')
1002
+ .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');
1003
+ run
1004
+ .command('queue')
1005
+ .description('Queue all ready tasks with satisfied dependencies')
1006
+ .option('--json', 'Print machine-readable JSON output')
1007
+ .addHelpText('after', '\nExamples:\n $ sidecar run queue\n $ sidecar run queue --json')
1008
+ .action((opts) => {
1009
+ const command = 'run queue';
1010
+ try {
1011
+ const rootPath = resolveProjectRoot();
1012
+ const decisions = queueReadyTasks(rootPath);
1013
+ respondSuccess(command, Boolean(opts.json), { decisions }, [
1014
+ `Processed ${decisions.length} ready task(s).`,
1015
+ ...decisions.map((d) => `- ${d.task_id}: ${d.reason}`),
1016
+ ]);
1017
+ }
1018
+ catch (err) {
1019
+ handleCommandError(command, Boolean(opts.json), err);
1020
+ }
1021
+ });
1022
+ run
1023
+ .command('start-ready')
1024
+ .description('Queue and start all runnable ready tasks')
1025
+ .option('--dry-run', 'Prepare and compile only without executing external runners')
1026
+ .option('--json', 'Print machine-readable JSON output')
1027
+ .addHelpText('after', '\nExamples:\n $ sidecar run start-ready\n $ sidecar run start-ready --dry-run --json')
1028
+ .action((opts) => {
1029
+ const command = 'run start-ready';
1030
+ try {
1031
+ const rootPath = resolveProjectRoot();
1032
+ const queueDecisions = queueReadyTasks(rootPath);
1033
+ const queuedTasks = listTaskPackets(rootPath).filter((task) => task.status === 'queued');
1034
+ const results = queuedTasks.map((task) => runTaskExecution({ rootPath, taskId: task.task_id, dryRun: Boolean(opts.dryRun) }));
1035
+ respondSuccess(command, Boolean(opts.json), { queued: queueDecisions, results }, [
1036
+ `Queued in this pass: ${queueDecisions.filter((d) => d.queued).length}`,
1037
+ `Started: ${results.length}`,
1038
+ ...results.map((r) => `- ${r.task_id} -> ${r.run_id} (${r.status})`),
1039
+ ]);
1040
+ }
1041
+ catch (err) {
1042
+ handleCommandError(command, Boolean(opts.json), err);
1043
+ }
1044
+ });
1045
+ run
1046
+ .command('approve <run-id>')
1047
+ .description('Review a completed run as approved, needs changes, or merged')
1048
+ .option('--state <state>', 'approved|needs_changes|merged', 'approved')
1049
+ .option('--note <text>', 'Review note')
1050
+ .option('--by <name>', 'Reviewer name', 'human')
1051
+ .option('--json', 'Print machine-readable JSON output')
1052
+ .addHelpText('after', '\nExamples:\n $ sidecar run approve R-010\n $ sidecar run approve R-010 --state needs_changes --note "Address test failures"\n $ sidecar run approve R-010 --state merged --json')
1053
+ .action((runIdText, opts) => {
1054
+ const command = 'run approve';
1055
+ try {
1056
+ const state = String(opts.state);
1057
+ if (state !== 'approved' && state !== 'needs_changes' && state !== 'merged') {
1058
+ fail('State must be one of: approved, needs_changes, merged');
1059
+ }
1060
+ const result = reviewRun(resolveProjectRoot(), runIdText.trim().toUpperCase(), state, {
1061
+ note: opts.note,
1062
+ by: opts.by,
1063
+ });
1064
+ respondSuccess(command, Boolean(opts.json), result, [
1065
+ `Run ${result.run_id} marked ${result.review_state}.`,
1066
+ `Task ${result.task_id} -> ${result.task_status}`,
1067
+ ]);
1068
+ }
1069
+ catch (err) {
1070
+ handleCommandError(command, Boolean(opts.json), err);
1071
+ }
1072
+ });
1073
+ run
1074
+ .command('block <run-id>')
1075
+ .description('Mark a completed run as blocked and set linked task blocked')
1076
+ .option('--note <text>', 'Blocking reason')
1077
+ .option('--by <name>', 'Reviewer name', 'human')
1078
+ .option('--json', 'Print machine-readable JSON output')
1079
+ .addHelpText('after', '\nExamples:\n $ sidecar run block R-010 --note "Migration failed"\n $ sidecar run block R-010 --json')
1080
+ .action((runIdText, opts) => {
1081
+ const command = 'run block';
1082
+ try {
1083
+ const result = reviewRun(resolveProjectRoot(), runIdText.trim().toUpperCase(), 'blocked', {
1084
+ note: opts.note,
1085
+ by: opts.by,
1086
+ });
1087
+ respondSuccess(command, Boolean(opts.json), result, [
1088
+ `Run ${result.run_id} marked blocked.`,
1089
+ `Task ${result.task_id} -> ${result.task_status}`,
1090
+ ]);
1091
+ }
1092
+ catch (err) {
1093
+ handleCommandError(command, Boolean(opts.json), err);
1094
+ }
1095
+ });
1096
+ run
1097
+ .command('list')
1098
+ .description('List execution run records')
1099
+ .option('--task <task-id>', 'Filter by task id (for example T-001)')
1100
+ .option('--status <status>', 'queued|preparing|running|review|blocked|completed|failed|all', 'all')
1101
+ .option('--json', 'Print machine-readable JSON output')
1102
+ .addHelpText('after', '\nExamples:\n $ sidecar run list\n $ sidecar run list --task T-001\n $ sidecar run list --status completed --json')
1103
+ .action((opts) => {
1104
+ const command = 'run list';
1105
+ try {
1106
+ const rootPath = resolveProjectRoot();
1107
+ const status = runListStatusSchema.parse(opts.status);
1108
+ const base = opts.task ? listRunRecordsForTask(rootPath, String(opts.task).trim().toUpperCase()) : listRunRecords(rootPath);
1109
+ const rows = status === 'all' ? base : base.filter((entry) => entry.status === status);
1110
+ if (opts.json) {
1111
+ respondSuccess(command, true, { status, task_id: opts.task ? String(opts.task).trim().toUpperCase() : null, runs: rows }, []);
1112
+ return;
1113
+ }
1114
+ if (rows.length === 0) {
1115
+ console.log('No run records found.');
1116
+ return;
1117
+ }
1118
+ const idWidth = Math.max(6, ...rows.map((r) => r.run_id.length));
1119
+ const taskWidth = Math.max(7, ...rows.map((r) => r.task_id.length));
1120
+ const statusWidth = Math.max(10, ...rows.map((r) => r.status.length));
1121
+ console.log(`${'RUN ID'.padEnd(idWidth)} ${'TASK ID'.padEnd(taskWidth)} ${'STATUS'.padEnd(statusWidth)} STARTED`);
1122
+ for (const row of rows) {
1123
+ console.log(`${row.run_id.padEnd(idWidth)} ${row.task_id.padEnd(taskWidth)} ${row.status.padEnd(statusWidth)} ${humanTime(row.started_at)}`);
1124
+ }
1125
+ }
1126
+ catch (err) {
1127
+ handleCommandError(command, Boolean(opts.json), err);
1128
+ }
1129
+ });
1130
+ run
1131
+ .command('summary')
1132
+ .description('Show project-level run review summary')
1133
+ .option('--json', 'Print machine-readable JSON output')
1134
+ .addHelpText('after', '\nExamples:\n $ sidecar run summary\n $ sidecar run summary --json')
1135
+ .action((opts) => {
1136
+ const command = 'run summary';
1137
+ try {
1138
+ const data = buildReviewSummary(resolveProjectRoot());
1139
+ respondSuccess(command, Boolean(opts.json), data, [
1140
+ `Completed runs: ${data.completed_runs}`,
1141
+ `Blocked runs: ${data.blocked_runs}`,
1142
+ `Suggested follow-ups: ${data.suggested_follow_ups}`,
1143
+ 'Recently merged:',
1144
+ ...(data.recently_merged.length
1145
+ ? data.recently_merged.map((r) => `- ${r.run_id} (${r.task_id}) at ${humanTime(r.reviewed_at)}`)
1146
+ : ['- none']),
1147
+ ]);
1148
+ }
1149
+ catch (err) {
1150
+ handleCommandError(command, Boolean(opts.json), err);
1151
+ }
1152
+ });
1153
+ run
1154
+ .command('show <run-id>')
1155
+ .description('Show a run record by id')
1156
+ .option('--json', 'Print machine-readable JSON output')
1157
+ .addHelpText('after', '\nExamples:\n $ sidecar run show R-001\n $ sidecar run show R-001 --json')
1158
+ .action((runIdText, opts) => {
1159
+ const command = 'run show';
1160
+ try {
1161
+ const runRecord = getRunRecord(resolveProjectRoot(), runIdText.trim().toUpperCase());
1162
+ if (opts.json) {
1163
+ respondSuccess(command, true, { run: runRecord }, []);
1164
+ return;
788
1165
  }
1166
+ console.log(stringifyJson(runRecord));
789
1167
  }
790
1168
  catch (err) {
791
1169
  handleCommandError(command, Boolean(opts.json), err);
@@ -952,5 +1330,17 @@ if (process.argv.length === 2) {
952
1330
  maybePrintUpdateNotice();
953
1331
  process.exit(0);
954
1332
  }
1333
+ if (process.argv[2] === 'run' &&
1334
+ process.argv[3] &&
1335
+ !process.argv[3].startsWith('-') &&
1336
+ process.argv[3] !== 'list' &&
1337
+ process.argv[3] !== 'show' &&
1338
+ process.argv[3] !== 'queue' &&
1339
+ process.argv[3] !== 'start-ready' &&
1340
+ process.argv[3] !== 'approve' &&
1341
+ process.argv[3] !== 'block' &&
1342
+ process.argv[3] !== 'summary') {
1343
+ process.argv.splice(2, 1, 'run-exec');
1344
+ }
955
1345
  program.parse(process.argv);
956
1346
  maybePrintUpdateNotice();
package/dist/lib/paths.js CHANGED
@@ -6,6 +6,9 @@ export function getSidecarPaths(rootPath) {
6
6
  return {
7
7
  rootPath,
8
8
  sidecarPath,
9
+ tasksPath: path.join(sidecarPath, 'tasks'),
10
+ runsPath: path.join(sidecarPath, 'runs'),
11
+ promptsPath: path.join(sidecarPath, 'prompts'),
9
12
  rootAgentsPath: path.join(rootPath, 'AGENTS.md'),
10
13
  rootClaudePath: path.join(rootPath, 'CLAUDE.md'),
11
14
  dbPath: path.join(sidecarPath, 'sidecar.db'),
package/dist/lib/ui.js CHANGED
@@ -84,7 +84,10 @@ export function launchUiServer(options) {
84
84
  }
85
85
  const child = spawn(process.execPath, [serverPath, '--project', options.projectPath, '--port', String(options.port)], {
86
86
  stdio: 'inherit',
87
- env: { ...process.env },
87
+ env: {
88
+ ...process.env,
89
+ SIDECAR_CLI_JS: process.argv[1] || '',
90
+ },
88
91
  });
89
92
  const url = `http://localhost:${options.port}`;
90
93
  if (options.openBrowser) {