sidecar-cli 0.1.1 → 0.1.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -1
- package/dist/cli.js +656 -68
- package/dist/lib/output.js +3 -0
- package/dist/lib/paths.js +3 -0
- package/dist/lib/ui.js +109 -0
- package/dist/prompts/prompt-compiler.js +88 -0
- package/dist/prompts/prompt-service.js +35 -0
- package/dist/runners/claude-runner.js +38 -0
- package/dist/runners/codex-runner.js +38 -0
- package/dist/runners/config.js +39 -0
- package/dist/runners/factory.js +10 -0
- package/dist/runners/runner-adapter.js +1 -0
- package/dist/runs/run-record.js +97 -0
- package/dist/runs/run-repository.js +99 -0
- package/dist/runs/run-service.js +27 -0
- package/dist/services/capabilities-service.js +193 -14
- package/dist/services/event-ingest-service.js +72 -0
- package/dist/services/export-service.js +79 -0
- package/dist/services/run-orchestrator-service.js +59 -0
- package/dist/services/run-review-service.js +76 -0
- package/dist/services/task-orchestration-service.js +94 -0
- package/dist/tasks/task-packet.js +132 -0
- package/dist/tasks/task-repository.js +78 -0
- package/dist/tasks/task-service.js +79 -0
- package/dist/types/api.js +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
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';
|
|
7
8
|
import { initializeSchema } from './db/schema.js';
|
|
8
|
-
import { getSidecarPaths } from './lib/paths.js';
|
|
9
|
+
import { findSidecarRoot, getSidecarPaths } from './lib/paths.js';
|
|
9
10
|
import { nowIso, humanTime, stringifyJson } from './lib/format.js';
|
|
10
11
|
import { SidecarError } from './lib/errors.js';
|
|
11
12
|
import { jsonFailure, jsonSuccess, printJsonEnvelope } from './lib/output.js';
|
|
12
13
|
import { bannerDisabled, renderBanner } from './lib/banner.js';
|
|
13
14
|
import { getUpdateNotice } from './lib/update-check.js';
|
|
15
|
+
import { ensureUiInstalled, launchUiServer } from './lib/ui.js';
|
|
14
16
|
import { requireInitialized } from './db/client.js';
|
|
15
17
|
import { renderAgentsMarkdown, renderClaudeMarkdown } from './templates/agents.js';
|
|
16
18
|
import { refreshSummaryFile } from './services/summary-service.js';
|
|
@@ -18,13 +20,25 @@ import { buildContext } from './services/context-service.js';
|
|
|
18
20
|
import { getCapabilitiesManifest } from './services/capabilities-service.js';
|
|
19
21
|
import { addArtifact, listArtifacts } from './services/artifact-service.js';
|
|
20
22
|
import { addDecision, addNote, addWorklog, getActiveSessionId, listRecentEvents } from './services/event-service.js';
|
|
21
|
-
import { addTask, listTasks, markTaskDone } from './services/task-service.js';
|
|
22
23
|
import { currentSession, endSession, startSession, verifySessionHygiene } from './services/session-service.js';
|
|
24
|
+
import { eventIngestSchema, ingestEvent } from './services/event-ingest-service.js';
|
|
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';
|
|
23
35
|
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
24
36
|
const actorSchema = z.enum(['human', 'agent']);
|
|
25
|
-
const taskPrioritySchema = z.enum(['low', 'medium', 'high']);
|
|
26
37
|
const artifactKindSchema = z.enum(['file', 'doc', 'screenshot', 'other']);
|
|
27
|
-
const
|
|
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']);
|
|
41
|
+
const exportFormatSchema = z.enum(['json', 'jsonl']);
|
|
28
42
|
const NOT_INITIALIZED_MSG = 'Sidecar is not initialized in this directory or any parent directory';
|
|
29
43
|
function fail(message) {
|
|
30
44
|
throw new SidecarError(message);
|
|
@@ -166,6 +180,36 @@ function renderContextMarkdown(data) {
|
|
|
166
180
|
}
|
|
167
181
|
return lines.join('\n');
|
|
168
182
|
}
|
|
183
|
+
async function readStdinText() {
|
|
184
|
+
const chunks = [];
|
|
185
|
+
for await (const chunk of process.stdin) {
|
|
186
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
187
|
+
}
|
|
188
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
189
|
+
}
|
|
190
|
+
function resolveProjectRoot(projectPath) {
|
|
191
|
+
const basePath = projectPath ? path.resolve(projectPath) : process.cwd();
|
|
192
|
+
const root = findSidecarRoot(basePath);
|
|
193
|
+
if (!root) {
|
|
194
|
+
throw new SidecarError(NOT_INITIALIZED_MSG);
|
|
195
|
+
}
|
|
196
|
+
return root;
|
|
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
|
+
}
|
|
169
213
|
const program = new Command();
|
|
170
214
|
program.name('sidecar').description('Local-first project memory and recording CLI').version(pkg.version);
|
|
171
215
|
program.option('--no-banner', 'Disable Sidecar banner output');
|
|
@@ -183,6 +227,53 @@ function maybePrintUpdateNotice() {
|
|
|
183
227
|
console.log(`Update available: ${pkg.version} -> ${notice.latestVersion}`);
|
|
184
228
|
console.log(`Run: npm install -g sidecar-cli@${installTag}`);
|
|
185
229
|
}
|
|
230
|
+
program
|
|
231
|
+
.command('ui')
|
|
232
|
+
.description('Launch the optional local Sidecar UI')
|
|
233
|
+
.option('--no-open', 'Do not open the browser automatically')
|
|
234
|
+
.option('--port <port>', 'Port to run the UI on', (v) => Number.parseInt(v, 10), 4310)
|
|
235
|
+
.option('--install-only', 'Install/update UI package but do not launch')
|
|
236
|
+
.option('--project <path>', 'Project path (defaults to nearest Sidecar root)')
|
|
237
|
+
.option('--reinstall', 'Force reinstall UI package')
|
|
238
|
+
.addHelpText('after', '\nExamples:\n $ sidecar ui\n $ sidecar ui --no-open --port 4311\n $ sidecar ui --project ../my-repo --install-only')
|
|
239
|
+
.action((opts) => {
|
|
240
|
+
const command = 'ui';
|
|
241
|
+
try {
|
|
242
|
+
const projectRoot = resolveProjectRoot(opts.project);
|
|
243
|
+
const port = Number(opts.port);
|
|
244
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
245
|
+
fail('Port must be an integer between 1 and 65535');
|
|
246
|
+
}
|
|
247
|
+
if (!bannerDisabled()) {
|
|
248
|
+
console.log(renderBanner());
|
|
249
|
+
console.log('');
|
|
250
|
+
}
|
|
251
|
+
console.log('Launching Sidecar UI');
|
|
252
|
+
console.log(`Project: ${projectRoot}`);
|
|
253
|
+
const { installedVersion } = ensureUiInstalled({
|
|
254
|
+
cliVersion: pkg.version,
|
|
255
|
+
reinstall: Boolean(opts.reinstall),
|
|
256
|
+
onStatus: (line) => console.log(line),
|
|
257
|
+
});
|
|
258
|
+
console.log(`UI version: ${installedVersion}`);
|
|
259
|
+
if (opts.installOnly) {
|
|
260
|
+
console.log('Install-only mode complete.');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const { url } = launchUiServer({
|
|
264
|
+
projectPath: projectRoot,
|
|
265
|
+
port,
|
|
266
|
+
openBrowser: opts.open !== false,
|
|
267
|
+
});
|
|
268
|
+
console.log(`URL: ${url}`);
|
|
269
|
+
if (opts.open === false) {
|
|
270
|
+
console.log('Browser auto-open disabled.');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
handleCommandError(command, false, err);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
186
277
|
program
|
|
187
278
|
.command('init')
|
|
188
279
|
.description('Initialize Sidecar in the current directory')
|
|
@@ -209,6 +300,9 @@ program
|
|
|
209
300
|
fail('Sidecar is already initialized in this project. Re-run with --force to recreate .sidecar files.');
|
|
210
301
|
}
|
|
211
302
|
const files = [
|
|
303
|
+
sidecar.tasksPath,
|
|
304
|
+
sidecar.runsPath,
|
|
305
|
+
sidecar.promptsPath,
|
|
212
306
|
sidecar.dbPath,
|
|
213
307
|
sidecar.configPath,
|
|
214
308
|
sidecar.preferencesPath,
|
|
@@ -231,9 +325,12 @@ program
|
|
|
231
325
|
sidecar.preferencesPath,
|
|
232
326
|
sidecar.agentsPath,
|
|
233
327
|
sidecar.summaryPath,
|
|
328
|
+
sidecar.tasksPath,
|
|
329
|
+
sidecar.runsPath,
|
|
330
|
+
sidecar.promptsPath,
|
|
234
331
|
]) {
|
|
235
332
|
if (fs.existsSync(file))
|
|
236
|
-
fs.rmSync(file);
|
|
333
|
+
fs.rmSync(file, { recursive: true, force: true });
|
|
237
334
|
}
|
|
238
335
|
}
|
|
239
336
|
const db = new Database(sidecar.dbPath);
|
|
@@ -249,9 +346,17 @@ program
|
|
|
249
346
|
settings: {},
|
|
250
347
|
};
|
|
251
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 });
|
|
252
352
|
fs.writeFileSync(sidecar.preferencesPath, stringifyJson({
|
|
253
353
|
summary: { format: 'markdown', recentLimit: 8 },
|
|
254
354
|
output: { humanTime: true },
|
|
355
|
+
runner: {
|
|
356
|
+
defaultRunner: 'codex',
|
|
357
|
+
preferredRunners: ['codex', 'claude'],
|
|
358
|
+
defaultAgentRole: 'builder-app',
|
|
359
|
+
},
|
|
255
360
|
}));
|
|
256
361
|
fs.writeFileSync(sidecar.agentsPath, renderAgentsMarkdown(projectName));
|
|
257
362
|
if (shouldWriteRootAgents) {
|
|
@@ -307,7 +412,7 @@ program
|
|
|
307
412
|
};
|
|
308
413
|
const recent = db.prepare(`SELECT type, title, created_at FROM events WHERE project_id = ? ORDER BY created_at DESC LIMIT 5`).all(projectId);
|
|
309
414
|
db.close();
|
|
310
|
-
const data = {
|
|
415
|
+
const data = { project, counts, recent_events: recent };
|
|
311
416
|
respondSuccess(command, Boolean(opts.json), data, [
|
|
312
417
|
`Project: ${project.name}`,
|
|
313
418
|
`Root: ${project.root_path}`,
|
|
@@ -323,6 +428,24 @@ program
|
|
|
323
428
|
handleCommandError(command, Boolean(opts.json), normalized);
|
|
324
429
|
}
|
|
325
430
|
});
|
|
431
|
+
const preferences = program.command('preferences').description('Preferences commands');
|
|
432
|
+
preferences
|
|
433
|
+
.command('show')
|
|
434
|
+
.description('Show project preferences')
|
|
435
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
436
|
+
.addHelpText('after', '\nExamples:\n $ sidecar preferences show\n $ sidecar preferences show --json')
|
|
437
|
+
.action((opts) => {
|
|
438
|
+
const command = 'preferences show';
|
|
439
|
+
try {
|
|
440
|
+
const { rootPath } = requireInitialized();
|
|
441
|
+
const prefsPath = getSidecarPaths(rootPath).preferencesPath;
|
|
442
|
+
const preferencesData = fs.existsSync(prefsPath) ? JSON.parse(fs.readFileSync(prefsPath, 'utf8')) : {};
|
|
443
|
+
respondSuccess(command, Boolean(opts.json), { project: { root_path: rootPath }, preferences: preferencesData, path: prefsPath }, [`Preferences path: ${prefsPath}`, stringifyJson(preferencesData)]);
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
handleCommandError(command, Boolean(opts.json), err);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
326
449
|
program
|
|
327
450
|
.command('capabilities')
|
|
328
451
|
.description('Output a machine-readable manifest of Sidecar commands')
|
|
@@ -336,6 +459,120 @@ program
|
|
|
336
459
|
else
|
|
337
460
|
console.log(stringifyJson(manifest));
|
|
338
461
|
});
|
|
462
|
+
const event = program.command('event').description('Generic event ingest commands');
|
|
463
|
+
event
|
|
464
|
+
.command('add')
|
|
465
|
+
.description('Add a validated generic Sidecar event')
|
|
466
|
+
.option('--type <type>', 'note|decision|worklog|task_created|task_completed|summary_generated')
|
|
467
|
+
.option('--title <title>', 'Event title')
|
|
468
|
+
.option('--summary <summary>', 'Event summary')
|
|
469
|
+
.option('--details-json <json>', 'JSON object for details_json')
|
|
470
|
+
.option('--created-by <by>', 'human|agent|system')
|
|
471
|
+
.option('--source <source>', 'cli|imported|generated')
|
|
472
|
+
.option('--session-id <id>', 'Optional session id', (v) => Number.parseInt(v, 10))
|
|
473
|
+
.option('--json-input <json>', 'Raw JSON event payload')
|
|
474
|
+
.option('--stdin', 'Read JSON event payload from stdin')
|
|
475
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
476
|
+
.addHelpText('after', '\nExamples:\n $ sidecar event add --type note --summary "Captured context"\n $ sidecar event add --json-input \'{"type":"decision","title":"Use SQLite","summary":"Local-first"}\' --json\n $ cat event.json | sidecar event add --stdin --json')
|
|
477
|
+
.action(async (opts) => {
|
|
478
|
+
const command = 'event add';
|
|
479
|
+
try {
|
|
480
|
+
const payloadSources = [Boolean(opts.jsonInput), Boolean(opts.stdin), Boolean(opts.type || opts.title || opts.summary || opts.detailsJson || opts.createdBy || opts.source || opts.sessionId)];
|
|
481
|
+
if (payloadSources.filter(Boolean).length !== 1) {
|
|
482
|
+
fail('Provide exactly one payload source: structured flags OR --json-input OR --stdin');
|
|
483
|
+
}
|
|
484
|
+
let payloadRaw;
|
|
485
|
+
if (opts.jsonInput) {
|
|
486
|
+
payloadRaw = JSON.parse(opts.jsonInput);
|
|
487
|
+
}
|
|
488
|
+
else if (opts.stdin) {
|
|
489
|
+
const raw = (await readStdinText()).trim();
|
|
490
|
+
if (!raw)
|
|
491
|
+
fail('STDIN payload is empty');
|
|
492
|
+
payloadRaw = JSON.parse(raw);
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
payloadRaw = {
|
|
496
|
+
type: opts.type,
|
|
497
|
+
title: opts.title,
|
|
498
|
+
summary: opts.summary,
|
|
499
|
+
details_json: opts.detailsJson ? JSON.parse(opts.detailsJson) : undefined,
|
|
500
|
+
created_by: opts.createdBy,
|
|
501
|
+
source: opts.source,
|
|
502
|
+
session_id: Number.isInteger(opts.sessionId) ? opts.sessionId : undefined,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const payload = eventIngestSchema.parse(payloadRaw);
|
|
506
|
+
const { db, projectId } = requireInitialized();
|
|
507
|
+
const created = ingestEvent(db, { project_id: projectId, payload });
|
|
508
|
+
db.close();
|
|
509
|
+
respondSuccess(command, Boolean(opts.json), { event: { ...created, created_at: nowIso() } }, [`Recorded ${created.type} event #${created.id}.`]);
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
handleCommandError(command, Boolean(opts.json), err);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
program
|
|
516
|
+
.command('export')
|
|
517
|
+
.description('Export project memory in JSON or JSONL')
|
|
518
|
+
.option('--format <format>', 'json|jsonl', 'json')
|
|
519
|
+
.option('--limit <n>', 'Limit exported events', (v) => Number.parseInt(v, 10))
|
|
520
|
+
.option('--type <event-type>', 'Filter exported events by type')
|
|
521
|
+
.option('--since <iso-date>', 'Filter events created_at >= since')
|
|
522
|
+
.option('--until <iso-date>', 'Filter events created_at <= until')
|
|
523
|
+
.option('--output <path>', 'Write export to file path instead of stdout')
|
|
524
|
+
.option('--json', 'Wrap command metadata in JSON envelope when writing to file')
|
|
525
|
+
.addHelpText('after', '\nExamples:\n $ sidecar export --format json\n $ sidecar export --format jsonl --output sidecar-events.jsonl\n $ sidecar export --type decision --since 2026-01-01T00:00:00Z')
|
|
526
|
+
.action((opts) => {
|
|
527
|
+
const command = 'export';
|
|
528
|
+
try {
|
|
529
|
+
const format = exportFormatSchema.parse(opts.format);
|
|
530
|
+
if (opts.since && Number.isNaN(Date.parse(opts.since)))
|
|
531
|
+
fail('--since must be a valid ISO date');
|
|
532
|
+
if (opts.until && Number.isNaN(Date.parse(opts.until)))
|
|
533
|
+
fail('--until must be a valid ISO date');
|
|
534
|
+
const { db, projectId, rootPath } = requireInitialized();
|
|
535
|
+
if (format === 'json') {
|
|
536
|
+
const payload = buildExportJson(db, {
|
|
537
|
+
projectId,
|
|
538
|
+
rootPath,
|
|
539
|
+
limit: opts.limit,
|
|
540
|
+
type: opts.type,
|
|
541
|
+
since: opts.since,
|
|
542
|
+
until: opts.until,
|
|
543
|
+
});
|
|
544
|
+
db.close();
|
|
545
|
+
const rendered = stringifyJson(payload);
|
|
546
|
+
if (opts.output) {
|
|
547
|
+
const filePath = writeOutputFile(opts.output, `${rendered}\n`);
|
|
548
|
+
respondSuccess(command, Boolean(opts.json), { format, output_path: filePath }, [`Export written: ${filePath}`]);
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
console.log(rendered);
|
|
552
|
+
}
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const lines = buildExportJsonlEvents(db, {
|
|
556
|
+
projectId,
|
|
557
|
+
limit: opts.limit,
|
|
558
|
+
type: opts.type,
|
|
559
|
+
since: opts.since,
|
|
560
|
+
until: opts.until,
|
|
561
|
+
});
|
|
562
|
+
db.close();
|
|
563
|
+
const rendered = `${lines.join('\n')}${lines.length > 0 ? '\n' : ''}`;
|
|
564
|
+
if (opts.output) {
|
|
565
|
+
const filePath = writeOutputFile(opts.output, rendered);
|
|
566
|
+
respondSuccess(command, Boolean(opts.json), { format, output_path: filePath, records: lines.length }, [`Export written: ${filePath}`]);
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
process.stdout.write(rendered);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch (err) {
|
|
573
|
+
handleCommandError(command, Boolean(opts.json), err);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
339
576
|
program
|
|
340
577
|
.command('context')
|
|
341
578
|
.description('Generate a compact context snapshot for a work session')
|
|
@@ -380,7 +617,7 @@ summary
|
|
|
380
617
|
const { db, projectId, rootPath } = requireInitialized();
|
|
381
618
|
const out = refreshSummaryFile(db, rootPath, projectId, Math.max(1, opts.limit));
|
|
382
619
|
db.close();
|
|
383
|
-
respondSuccess(command, Boolean(opts.json), {
|
|
620
|
+
respondSuccess(command, Boolean(opts.json), { summary: { path: out.path, generated_at: out.generatedAt } }, ['Summary refreshed.', `Path: ${out.path}`]);
|
|
384
621
|
}
|
|
385
622
|
catch (err) {
|
|
386
623
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -400,7 +637,7 @@ program
|
|
|
400
637
|
const rows = listRecentEvents(db, { projectId, type: opts.type, limit: Math.max(1, opts.limit) });
|
|
401
638
|
db.close();
|
|
402
639
|
if (opts.json)
|
|
403
|
-
printJsonEnvelope(jsonSuccess(command, rows));
|
|
640
|
+
printJsonEnvelope(jsonSuccess(command, { events: rows }));
|
|
404
641
|
else {
|
|
405
642
|
if (rows.length === 0) {
|
|
406
643
|
console.log('No events found.');
|
|
@@ -432,7 +669,7 @@ program
|
|
|
432
669
|
const sessionId = maybeSessionId(db, projectId, opts.session);
|
|
433
670
|
const eventId = addNote(db, { projectId, text, title: opts.title, by, sessionId });
|
|
434
671
|
db.close();
|
|
435
|
-
respondSuccess(command, Boolean(opts.json), { eventId,
|
|
672
|
+
respondSuccess(command, Boolean(opts.json), { event: { id: eventId, type: 'note', title: opts.title?.trim() || 'Note', summary: text, created_by: by, session_id: sessionId, created_at: nowIso() } }, [`Recorded note event #${eventId}.`]);
|
|
436
673
|
}
|
|
437
674
|
catch (err) {
|
|
438
675
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -457,7 +694,7 @@ decision
|
|
|
457
694
|
const sessionId = maybeSessionId(db, projectId, opts.session);
|
|
458
695
|
const eventId = addDecision(db, { projectId, title: opts.title, summary: opts.summary, details: opts.details, by, sessionId });
|
|
459
696
|
db.close();
|
|
460
|
-
respondSuccess(command, Boolean(opts.json), { eventId,
|
|
697
|
+
respondSuccess(command, Boolean(opts.json), { event: { id: eventId, type: 'decision', title: opts.title, summary: opts.summary, created_by: by, session_id: sessionId, created_at: nowIso() } }, [`Recorded decision event #${eventId}.`]);
|
|
461
698
|
}
|
|
462
699
|
catch (err) {
|
|
463
700
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -496,7 +733,7 @@ worklog
|
|
|
496
733
|
addArtifact(db, { projectId, path: filePath, kind: 'file' });
|
|
497
734
|
}
|
|
498
735
|
db.close();
|
|
499
|
-
respondSuccess(command, Boolean(opts.json), {
|
|
736
|
+
respondSuccess(command, Boolean(opts.json), { event: { id: result.eventId, type: 'worklog', summary: opts.done, created_by: by, session_id: sessionId, created_at: nowIso() }, artifacts: result.files.map((p) => ({ path: p, kind: 'file' })) }, [
|
|
500
737
|
`Recorded worklog event #${result.eventId}.`,
|
|
501
738
|
`Artifacts linked: ${result.files.length}`,
|
|
502
739
|
]);
|
|
@@ -507,46 +744,98 @@ worklog
|
|
|
507
744
|
});
|
|
508
745
|
const task = program.command('task').description('Task commands');
|
|
509
746
|
task
|
|
510
|
-
.command('
|
|
511
|
-
.description('Create
|
|
512
|
-
.option('--
|
|
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')
|
|
513
752
|
.option('--priority <priority>', 'low|medium|high', 'medium')
|
|
514
|
-
.option('--
|
|
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')
|
|
515
770
|
.option('--json', 'Print machine-readable JSON output')
|
|
516
|
-
.addHelpText('after', '\nExamples:\n $ sidecar task
|
|
517
|
-
.action((
|
|
518
|
-
const command = 'task
|
|
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';
|
|
519
774
|
try {
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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}`]);
|
|
526
820
|
}
|
|
527
821
|
catch (err) {
|
|
528
822
|
handleCommandError(command, Boolean(opts.json), err);
|
|
529
823
|
}
|
|
530
824
|
});
|
|
531
825
|
task
|
|
532
|
-
.command('
|
|
533
|
-
.description('
|
|
534
|
-
.option('--by <actor>', 'human|agent', 'human')
|
|
826
|
+
.command('show <task-id>')
|
|
827
|
+
.description('Show a task packet by id')
|
|
535
828
|
.option('--json', 'Print machine-readable JSON output')
|
|
536
|
-
.addHelpText('after', '\nExamples:\n $ sidecar task
|
|
829
|
+
.addHelpText('after', '\nExamples:\n $ sidecar task show T-001\n $ sidecar task show T-001 --json')
|
|
537
830
|
.action((taskIdText, opts) => {
|
|
538
|
-
const command = 'task
|
|
831
|
+
const command = 'task show';
|
|
539
832
|
try {
|
|
540
|
-
const
|
|
541
|
-
if (
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
db.close();
|
|
547
|
-
if (!result.ok)
|
|
548
|
-
fail(result.reason);
|
|
549
|
-
respondSuccess(command, Boolean(opts.json), { taskId, eventId: result.eventId, timestamp: nowIso() }, [`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));
|
|
550
839
|
}
|
|
551
840
|
catch (err) {
|
|
552
841
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -554,40 +843,327 @@ task
|
|
|
554
843
|
});
|
|
555
844
|
task
|
|
556
845
|
.command('list')
|
|
557
|
-
.description('List
|
|
558
|
-
.option('--status <status>', '
|
|
559
|
-
.option('--format <format>', 'table|json', 'table')
|
|
846
|
+
.description('List task packets')
|
|
847
|
+
.option('--status <status>', 'draft|ready|queued|running|review|blocked|done|all', 'all')
|
|
560
848
|
.option('--json', 'Print machine-readable JSON output')
|
|
561
|
-
.addHelpText('after', '\nExamples:\n $ sidecar task list\n $ sidecar task list --status
|
|
849
|
+
.addHelpText('after', '\nExamples:\n $ sidecar task list\n $ sidecar task list --status open\n $ sidecar task list --json')
|
|
562
850
|
.action((opts) => {
|
|
563
851
|
const command = 'task list';
|
|
564
852
|
try {
|
|
565
|
-
const status =
|
|
566
|
-
const
|
|
567
|
-
const rows =
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (opts.json)
|
|
571
|
-
printJsonEnvelope(jsonSuccess(command, rows));
|
|
572
|
-
else
|
|
573
|
-
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 }, []);
|
|
574
858
|
return;
|
|
575
859
|
}
|
|
576
|
-
|
|
577
|
-
if (taskRows.length === 0) {
|
|
860
|
+
if (rows.length === 0) {
|
|
578
861
|
console.log('No tasks found.');
|
|
579
862
|
return;
|
|
580
863
|
}
|
|
581
|
-
const idWidth = Math.max(
|
|
582
|
-
const statusWidth = Math.max(
|
|
583
|
-
const priorityWidth = Math.max(8, ...
|
|
584
|
-
console.log(`${'ID'.padEnd(idWidth)} ${'STATUS'.padEnd(statusWidth)} ${'PRIORITY'.padEnd(priorityWidth)} TITLE`);
|
|
585
|
-
for (const row of
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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;
|
|
590
1165
|
}
|
|
1166
|
+
console.log(stringifyJson(runRecord));
|
|
591
1167
|
}
|
|
592
1168
|
catch (err) {
|
|
593
1169
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -610,7 +1186,7 @@ session
|
|
|
610
1186
|
db.close();
|
|
611
1187
|
if (!result.ok)
|
|
612
1188
|
fail(result.reason);
|
|
613
|
-
respondSuccess(command, Boolean(opts.json), {
|
|
1189
|
+
respondSuccess(command, Boolean(opts.json), { session: { id: result.sessionId, actor_type: actor, actor_name: opts.name ?? null, started_at: nowIso() } }, [`Started session #${result.sessionId}.`]);
|
|
614
1190
|
}
|
|
615
1191
|
catch (err) {
|
|
616
1192
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -630,7 +1206,7 @@ session
|
|
|
630
1206
|
db.close();
|
|
631
1207
|
if (!result.ok)
|
|
632
1208
|
fail(result.reason);
|
|
633
|
-
respondSuccess(command, Boolean(opts.json), {
|
|
1209
|
+
respondSuccess(command, Boolean(opts.json), { session: { id: result.sessionId, ended_at: nowIso(), summary: opts.summary ?? null } }, [`Ended session #${result.sessionId}.`]);
|
|
634
1210
|
}
|
|
635
1211
|
catch (err) {
|
|
636
1212
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -648,7 +1224,7 @@ session
|
|
|
648
1224
|
const current = currentSession(db, projectId);
|
|
649
1225
|
db.close();
|
|
650
1226
|
if (opts.json)
|
|
651
|
-
printJsonEnvelope(jsonSuccess(command, {
|
|
1227
|
+
printJsonEnvelope(jsonSuccess(command, { session: current ?? null }));
|
|
652
1228
|
else if (!current) {
|
|
653
1229
|
console.log('No active session.');
|
|
654
1230
|
}
|
|
@@ -712,7 +1288,7 @@ artifact
|
|
|
712
1288
|
const { db, projectId } = requireInitialized();
|
|
713
1289
|
const artifactId = addArtifact(db, { projectId, path: artifactPath, kind, note: opts.note });
|
|
714
1290
|
db.close();
|
|
715
|
-
respondSuccess(command, Boolean(opts.json), { artifactId,
|
|
1291
|
+
respondSuccess(command, Boolean(opts.json), { artifact: { id: artifactId, path: artifactPath, kind, note: opts.note ?? null, created_at: nowIso() } }, [`Added artifact #${artifactId}.`]);
|
|
716
1292
|
}
|
|
717
1293
|
catch (err) {
|
|
718
1294
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -730,7 +1306,7 @@ artifact
|
|
|
730
1306
|
const rows = listArtifacts(db, projectId);
|
|
731
1307
|
db.close();
|
|
732
1308
|
if (opts.json)
|
|
733
|
-
printJsonEnvelope(jsonSuccess(command, rows));
|
|
1309
|
+
printJsonEnvelope(jsonSuccess(command, { artifacts: rows }));
|
|
734
1310
|
else {
|
|
735
1311
|
if (rows.length === 0) {
|
|
736
1312
|
console.log('No artifacts found.');
|
|
@@ -754,5 +1330,17 @@ if (process.argv.length === 2) {
|
|
|
754
1330
|
maybePrintUpdateNotice();
|
|
755
1331
|
process.exit(0);
|
|
756
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
|
+
}
|
|
757
1345
|
program.parse(process.argv);
|
|
758
1346
|
maybePrintUpdateNotice();
|