sidecar-cli 0.1.5-rc.1 → 0.1.6-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 +16 -42
- package/dist/cli.js +115 -65
- package/dist/prompts/packet-sections.js +41 -103
- package/dist/prompts/prompt-compiler.js +4 -4
- package/dist/prompts/prompt-service.js +2 -2
- package/dist/services/capabilities-service.js +18 -22
- package/dist/services/run-orchestrator-service.js +8 -13
- package/dist/services/run-review-service.js +14 -15
- package/dist/services/task-orchestration-service.js +43 -56
- package/dist/services/task-status-service.js +29 -0
- package/dist/tasks/task-packet.js +150 -119
- package/dist/tasks/task-repository.js +64 -26
- package/dist/tasks/task-service.js +12 -46
- package/dist/templates/agents.js +118 -52
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -200,9 +200,9 @@ Notes, decisions, worklogs:
|
|
|
200
200
|
|
|
201
201
|
Tasks:
|
|
202
202
|
|
|
203
|
-
- `sidecar task
|
|
204
|
-
- `sidecar task
|
|
205
|
-
- `sidecar task list [--status
|
|
203
|
+
- `sidecar task create --title "<title>" --summary "<summary>" --trigger "<condition>" --entry-points <path1,path2> --done-condition "<done>" --validate-cmd "<cmd>" [--trigger-check <command>] [--depends-on <task-ids>] [--status active|blocked|done] [--priority low|medium|high] [--json]`
|
|
204
|
+
- `sidecar task set-status <task-id> --to active|blocked|done --reason "<text>" [--by human|agent] [--session <id>] [--json]`
|
|
205
|
+
- `sidecar task list [--status active|blocked|done|all] [--json]`
|
|
206
206
|
|
|
207
207
|
Sessions:
|
|
208
208
|
|
|
@@ -217,47 +217,21 @@ Artifacts:
|
|
|
217
217
|
- `sidecar artifact add <path> [--kind file|doc|screenshot|other] [--note <text>] [--json]`
|
|
218
218
|
- `sidecar artifact list [--json]`
|
|
219
219
|
|
|
220
|
-
## Validation
|
|
220
|
+
## Validation and auto-approve
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
### Kinds
|
|
225
|
-
|
|
226
|
-
| Kind | Default timeout | Intended for |
|
|
227
|
-
| --- | --- | --- |
|
|
228
|
-
| `typecheck` | 3 min | `tsc --noEmit`, `mypy`, `pyright` |
|
|
229
|
-
| `lint` | 3 min | `eslint`, `ruff`, `golangci-lint` |
|
|
230
|
-
| `test` | 10 min | unit/integration suites |
|
|
231
|
-
| `build` | 10 min | bundlers, compilers, image builds |
|
|
232
|
-
| `custom` | 5 min | anything else (the legacy default) |
|
|
233
|
-
|
|
234
|
-
### Authoring
|
|
235
|
-
|
|
236
|
-
On the CLI, prefix a command with `kind:`. Entries without a prefix default to `custom`.
|
|
222
|
+
Queue tasks now store one required `validation_command` that agents run to
|
|
223
|
+
verify the done condition.
|
|
237
224
|
|
|
238
225
|
```bash
|
|
239
226
|
sidecar task create \
|
|
240
227
|
--title "Add import flow" \
|
|
241
|
-
--summary "..."
|
|
242
|
-
--
|
|
228
|
+
--summary "..." \
|
|
229
|
+
--trigger "After T-012 lands" --depends-on T-012 \
|
|
230
|
+
--entry-points src/cli.ts,src/tasks/task-packet.ts \
|
|
231
|
+
--done-condition "Task packets persist in active/blocked/done folders" \
|
|
232
|
+
--validate-cmd "npm run build"
|
|
243
233
|
```
|
|
244
234
|
|
|
245
|
-
In a task packet JSON file, use the object form:
|
|
246
|
-
|
|
247
|
-
```json
|
|
248
|
-
"execution": {
|
|
249
|
-
"commands": {
|
|
250
|
-
"validation": [
|
|
251
|
-
{ "kind": "typecheck", "command": "tsc --noEmit" },
|
|
252
|
-
{ "kind": "test", "command": "npm test", "timeout_ms": 900000 },
|
|
253
|
-
"bash scripts/smoke.sh"
|
|
254
|
-
]
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
String entries are accepted for back-compat (promoted to `{ kind: "custom", command }` on load).
|
|
260
|
-
|
|
261
235
|
### Auto-approve on all-green
|
|
262
236
|
|
|
263
237
|
When every validation step passes for a run, Sidecar can auto-approve the run so you don't have to click through the review queue for a strictly-green outcome. It's opt-in:
|
|
@@ -434,7 +408,7 @@ sidecar context --format markdown
|
|
|
434
408
|
sidecar session start --actor agent --name codex
|
|
435
409
|
sidecar decision record --title "Use SQLite" --summary "Local-first persistence"
|
|
436
410
|
sidecar worklog record --goal "init flow" --done "Implemented schema and command surface" --files src/cli.ts,src/db/schema.ts
|
|
437
|
-
sidecar task
|
|
411
|
+
sidecar task create --title "Add integration tests" --summary "Add integration coverage for init flow" --trigger "When test matrix is finalized" --entry-points src/cli.ts --done-condition "Integration suite covers init flow" --validate-cmd "npm test" --priority medium
|
|
438
412
|
sidecar summary refresh
|
|
439
413
|
sidecar session end --summary "Initialization and recording flow implemented"
|
|
440
414
|
```
|
|
@@ -449,7 +423,7 @@ Required minimum for any code change:
|
|
|
449
423
|
1. `sidecar context --format markdown`
|
|
450
424
|
2. `sidecar worklog record --done "<what changed>" --files <paths> --by agent`
|
|
451
425
|
3. if behavior/design changed: `sidecar decision record ...`
|
|
452
|
-
4. if follow-up exists: `sidecar task
|
|
426
|
+
4. if follow-up exists: `sidecar task create ...`
|
|
453
427
|
5. `sidecar summary refresh`
|
|
454
428
|
|
|
455
429
|
Optional local enforcement:
|
|
@@ -489,7 +463,7 @@ When changes are made in this repo, document them in Sidecar:
|
|
|
489
463
|
1. `sidecar context --format markdown`
|
|
490
464
|
2. `sidecar worklog record --done "<what changed>" --files <paths> --by human|agent`
|
|
491
465
|
3. `sidecar decision record ...` when behavior/design changes
|
|
492
|
-
4. `sidecar task
|
|
466
|
+
4. `sidecar task create ...` for follow-up work
|
|
493
467
|
5. `sidecar summary refresh`
|
|
494
468
|
|
|
495
469
|
## Local storage details
|
|
@@ -579,7 +553,7 @@ Standard JSON envelope:
|
|
|
579
553
|
{
|
|
580
554
|
"ok": true,
|
|
581
555
|
"version": "1.0",
|
|
582
|
-
"command": "task
|
|
556
|
+
"command": "task create",
|
|
583
557
|
"data": {},
|
|
584
558
|
"errors": []
|
|
585
559
|
}
|
|
@@ -591,7 +565,7 @@ Failure envelope:
|
|
|
591
565
|
{
|
|
592
566
|
"ok": false,
|
|
593
567
|
"version": "1.0",
|
|
594
|
-
"command": "task
|
|
568
|
+
"command": "task create",
|
|
595
569
|
"data": null,
|
|
596
570
|
"errors": ["..."]
|
|
597
571
|
}
|
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ import { refreshSummaryFile } from './services/summary-service.js';
|
|
|
22
22
|
import { buildContext } from './services/context-service.js';
|
|
23
23
|
import { getCapabilitiesManifest } from './services/capabilities-service.js';
|
|
24
24
|
import { addArtifact, listArtifacts } from './services/artifact-service.js';
|
|
25
|
-
import { addDecision, addNote, addWorklog, getActiveSessionId, listRecentEvents } from './services/event-service.js';
|
|
25
|
+
import { addDecision, addNote, addWorklog, createEvent, getActiveSessionId, listRecentEvents } from './services/event-service.js';
|
|
26
26
|
import { currentSession, endSession, startSession, verifySessionHygiene } from './services/session-service.js';
|
|
27
27
|
import { HOOK_EVENTS, handleHookEvent, hookEventSchema, hookPayloadSchema } from './services/hook-service.js';
|
|
28
28
|
import { loadPromptSpec } from './prompts/prompt-spec.js';
|
|
@@ -32,30 +32,27 @@ import { renderClaudeCodeHooksJson } from './templates/hooks.js';
|
|
|
32
32
|
import { eventIngestSchema, ingestEvent } from './services/event-ingest-service.js';
|
|
33
33
|
import { buildExportJson, buildExportJsonlEvents, writeOutputFile } from './services/export-service.js';
|
|
34
34
|
import { createTaskPacketRecord, getTaskPacket, listTaskPackets } from './tasks/task-service.js';
|
|
35
|
-
import { taskPacketPrioritySchema, taskPacketStatusSchema
|
|
35
|
+
import { taskPacketPrioritySchema, taskPacketStatusSchema } from './tasks/task-packet.js';
|
|
36
36
|
import { getRunRecord, listRunRecords, listRunRecordsForTask } from './runs/run-service.js';
|
|
37
37
|
import { runStatusSchema, runnerTypeSchema } from './runs/run-record.js';
|
|
38
38
|
import { compileTaskPrompt } from './prompts/prompt-service.js';
|
|
39
39
|
import { runPipelineExecution, runTaskExecution } from './services/run-orchestrator-service.js';
|
|
40
40
|
import { loadRunnerPreferences } from './runners/config.js';
|
|
41
41
|
import { assignTask, queueReadyTasks } from './services/task-orchestration-service.js';
|
|
42
|
+
import { transitionTaskStatus } from './services/task-status-service.js';
|
|
42
43
|
import { buildReviewSummary, createFollowupTaskFromRun, reviewRun } from './services/run-review-service.js';
|
|
43
44
|
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
44
45
|
const actorSchema = z.enum(['human', 'agent']);
|
|
45
46
|
const artifactKindSchema = z.enum(['file', 'doc', 'screenshot', 'other']);
|
|
46
|
-
const taskListStatusSchema = z.enum(['
|
|
47
|
+
const taskListStatusSchema = z.enum(['active', 'blocked', 'done', 'all']);
|
|
47
48
|
const runListStatusSchema = runStatusSchema.or(z.literal('all'));
|
|
48
49
|
const agentRoleSchema = z.enum(['planner', 'builder-ui', 'builder-app', 'reviewer', 'tester']);
|
|
49
50
|
const exportFormatSchema = z.enum(['json', 'jsonl']);
|
|
50
51
|
const NOT_INITIALIZED_MSG = 'Sidecar is not initialized in this directory or any parent directory';
|
|
51
52
|
function formatStatus(value) {
|
|
52
53
|
const v = value.toLowerCase();
|
|
53
|
-
if (v === '
|
|
54
|
+
if (v === 'active')
|
|
54
55
|
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
56
|
if (v === 'blocked')
|
|
60
57
|
return c.red(value);
|
|
61
58
|
if (v === 'done' || v === 'merged' || v === 'approved')
|
|
@@ -544,15 +541,12 @@ program
|
|
|
544
541
|
const created = createTaskPacketRecord(demoRoot, {
|
|
545
542
|
title: 'Add welcome banner',
|
|
546
543
|
summary: 'Show a friendly greeting on first launch so new users know the tool is working.',
|
|
547
|
-
|
|
548
|
-
type: 'feature',
|
|
549
|
-
status: 'ready',
|
|
544
|
+
status: 'active',
|
|
550
545
|
priority: 'medium',
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
validation_commands: ['typecheck@30s:tsc --noEmit', 'test@2m:npm test'],
|
|
546
|
+
trigger_condition: 'PM approved onboarding polish for next release',
|
|
547
|
+
entry_points: ['src/pages/home.tsx', 'src/components/Banner.tsx'],
|
|
548
|
+
done_condition: 'Banner is visible on first launch with no layout shift.',
|
|
549
|
+
validation_command: 'npm run build',
|
|
556
550
|
});
|
|
557
551
|
log(c.green(' ✓ ') + `${created.task.task_id} — ${created.task.title}`);
|
|
558
552
|
log(c.dim(` packet: ${path.relative(demoRoot, created.path)}`));
|
|
@@ -999,76 +993,70 @@ worklog
|
|
|
999
993
|
const task = program.command('task').description('Task commands');
|
|
1000
994
|
task
|
|
1001
995
|
.command('create')
|
|
1002
|
-
.description('Create
|
|
996
|
+
.description('Create an agent-ready queue task')
|
|
1003
997
|
.option('--title <title>', 'Task title')
|
|
1004
|
-
.option('--
|
|
1005
|
-
.option('--status <status>', 'draft|ready|queued|running|review|blocked|done', 'draft')
|
|
998
|
+
.option('--status <status>', 'active|blocked|done', 'active')
|
|
1006
999
|
.option('--priority <priority>', 'low|medium|high', 'medium')
|
|
1007
1000
|
.option('--summary <summary>', 'Task summary')
|
|
1008
|
-
.option('--
|
|
1009
|
-
.option('--
|
|
1010
|
-
.option('--
|
|
1011
|
-
.option('--
|
|
1012
|
-
.option('--
|
|
1013
|
-
.option('--
|
|
1014
|
-
.option('--related-decisions <items>', 'Comma-separated related decision IDs/titles')
|
|
1015
|
-
.option('--related-notes <items>', 'Comma-separated related notes')
|
|
1016
|
-
.option('--files-read <paths>', 'Comma-separated files to read')
|
|
1017
|
-
.option('--files-avoid <paths>', 'Comma-separated files to avoid')
|
|
1018
|
-
.option('--constraint-tech <items>', 'Comma-separated technical constraints')
|
|
1019
|
-
.option('--constraint-design <items>', 'Comma-separated design constraints')
|
|
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".')
|
|
1021
|
-
.option('--dod <items>', 'Comma-separated definition-of-done checks')
|
|
1022
|
-
.option('--branch <name>', 'Branch name')
|
|
1023
|
-
.option('--worktree <path>', 'Worktree path')
|
|
1001
|
+
.option('--trigger <condition>', 'Concrete trigger condition that makes this task ready')
|
|
1002
|
+
.option('--trigger-check <command>', 'Command that returns 0 when the trigger is satisfied')
|
|
1003
|
+
.option('--depends-on <task-ids>', 'Comma-separated dependency task IDs for trigger gating')
|
|
1004
|
+
.option('--entry-points <paths>', 'Comma-separated 1-3 files to open first')
|
|
1005
|
+
.option('--done-condition <text>', 'Observable condition that proves this task is done')
|
|
1006
|
+
.option('--validate-cmd <command>', 'Command to validate the done condition')
|
|
1024
1007
|
.option('--json', 'Print machine-readable JSON output')
|
|
1025
|
-
.addHelpText('after', '\nExamples:\n
|
|
1008
|
+
.addHelpText('after', '\nExamples:\n' +
|
|
1009
|
+
' $ sidecar task create --title "Ship docs update" --summary "Update README hero"\n' +
|
|
1010
|
+
' --trigger "When >=6 hubs are published" --trigger-check "node scripts/check-hubs.mjs --min 6"\n' +
|
|
1011
|
+
' --entry-points README.md,POSITIONING.md --done-condition "README hero matches retrieval positioning"\n' +
|
|
1012
|
+
' --validate-cmd "npm run build"\n' +
|
|
1013
|
+
' $ sidecar task create --title "Follow-up patch" --summary "Apply merged run feedback"\n' +
|
|
1014
|
+
' --trigger "After T-012 is done" --depends-on T-012 --entry-points src/cli.ts\n' +
|
|
1015
|
+
' --done-condition "Regression no longer reproduces" --validate-cmd "npm test"')
|
|
1026
1016
|
.action(async (opts) => {
|
|
1027
1017
|
const command = 'task create';
|
|
1028
1018
|
try {
|
|
1029
1019
|
const rootPath = resolveProjectRoot();
|
|
1030
1020
|
let title = opts.title?.trim() ?? '';
|
|
1031
1021
|
let summary = opts.summary?.trim() ?? '';
|
|
1032
|
-
let
|
|
1033
|
-
|
|
1022
|
+
let triggerCondition = opts.trigger?.trim() ?? '';
|
|
1023
|
+
let entryPointsRaw = opts.entryPoints?.trim() ?? '';
|
|
1024
|
+
let doneCondition = opts.doneCondition?.trim() ?? '';
|
|
1025
|
+
let validationCommand = opts.validateCmd?.trim() ?? '';
|
|
1026
|
+
if (!title || !summary || !triggerCondition || !entryPointsRaw || !doneCondition || !validationCommand) {
|
|
1034
1027
|
if (!process.stdin.isTTY) {
|
|
1035
|
-
fail('Missing required fields. Provide --title, --summary, and --
|
|
1028
|
+
fail('Missing required fields. Provide --title, --summary, --trigger, --entry-points, --done-condition, and --validate-cmd when not running interactively.');
|
|
1036
1029
|
}
|
|
1037
1030
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1038
1031
|
try {
|
|
1039
1032
|
title = title || (await askWithDefault(rl, 'Title'));
|
|
1040
1033
|
summary = summary || (await askWithDefault(rl, 'Summary', title));
|
|
1041
|
-
|
|
1034
|
+
triggerCondition = triggerCondition || (await askWithDefault(rl, 'Trigger condition'));
|
|
1035
|
+
entryPointsRaw = entryPointsRaw || (await askWithDefault(rl, 'Entry points (comma-separated files)'));
|
|
1036
|
+
doneCondition = doneCondition || (await askWithDefault(rl, 'Done condition', `Complete: ${title}`));
|
|
1037
|
+
validationCommand = validationCommand || (await askWithDefault(rl, 'Validation command', 'npm run build'));
|
|
1042
1038
|
}
|
|
1043
1039
|
finally {
|
|
1044
1040
|
rl.close();
|
|
1045
1041
|
}
|
|
1046
1042
|
}
|
|
1047
|
-
const type = taskPacketTypeSchema.parse(opts.type);
|
|
1048
1043
|
const status = taskPacketStatusSchema.parse(opts.status);
|
|
1049
1044
|
const priority = taskPacketPrioritySchema.parse(opts.priority);
|
|
1045
|
+
const entryPoints = parseCsvOption(entryPointsRaw);
|
|
1046
|
+
if (entryPoints.length === 0 || entryPoints.length > 3) {
|
|
1047
|
+
fail('Entry points must include between 1 and 3 paths');
|
|
1048
|
+
}
|
|
1050
1049
|
const created = createTaskPacketRecord(rootPath, {
|
|
1051
1050
|
title,
|
|
1052
1051
|
summary,
|
|
1053
|
-
goal,
|
|
1054
|
-
type,
|
|
1055
1052
|
status,
|
|
1056
1053
|
priority,
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
technical_constraints: parseCsvOption(opts.constraintTech),
|
|
1064
|
-
design_constraints: parseCsvOption(opts.constraintDesign),
|
|
1065
|
-
validation_commands: parseCsvOption(opts.validateCmds),
|
|
1066
|
-
dependencies: parseCsvOption(opts.dependencies).map((v) => v.toUpperCase()),
|
|
1067
|
-
tags: parseCsvOption(opts.tags),
|
|
1068
|
-
target_areas: parseCsvOption(opts.targetAreas),
|
|
1069
|
-
definition_of_done: parseCsvOption(opts.dod),
|
|
1070
|
-
branch: opts.branch?.trim(),
|
|
1071
|
-
worktree: opts.worktree?.trim(),
|
|
1054
|
+
trigger_condition: triggerCondition,
|
|
1055
|
+
trigger_check_command: opts.triggerCheck?.trim() || undefined,
|
|
1056
|
+
trigger_depends_on: parseCsvOption(opts.dependsOn).map((v) => v.toUpperCase()),
|
|
1057
|
+
entry_points: entryPoints,
|
|
1058
|
+
done_condition: doneCondition,
|
|
1059
|
+
validation_command: validationCommand,
|
|
1072
1060
|
});
|
|
1073
1061
|
respondSuccess(command, Boolean(opts.json), { task: created.task, path: created.path }, [`Created task ${created.task.task_id}.`, `Path: ${created.path}`]);
|
|
1074
1062
|
}
|
|
@@ -1098,9 +1086,9 @@ task
|
|
|
1098
1086
|
task
|
|
1099
1087
|
.command('list')
|
|
1100
1088
|
.description('List task packets')
|
|
1101
|
-
.option('--status <status>', '
|
|
1089
|
+
.option('--status <status>', 'active|blocked|done|all', 'all')
|
|
1102
1090
|
.option('--json', 'Print machine-readable JSON output')
|
|
1103
|
-
.addHelpText('after', '\nExamples:\n $ sidecar task list\n $ sidecar task list --status
|
|
1091
|
+
.addHelpText('after', '\nExamples:\n $ sidecar task list\n $ sidecar task list --status active\n $ sidecar task list --json')
|
|
1104
1092
|
.action((opts) => {
|
|
1105
1093
|
const command = 'task list';
|
|
1106
1094
|
try {
|
|
@@ -1158,6 +1146,64 @@ task
|
|
|
1158
1146
|
handleCommandError(command, Boolean(opts.json), err);
|
|
1159
1147
|
}
|
|
1160
1148
|
});
|
|
1149
|
+
task
|
|
1150
|
+
.command('set-status <task-id>')
|
|
1151
|
+
.description('Transition task packet status with validation and an audit note')
|
|
1152
|
+
.requiredOption('--to <status>', 'active|blocked|done')
|
|
1153
|
+
.requiredOption('--reason <text>', 'Why this manual transition is needed')
|
|
1154
|
+
.option('--by <actor>', 'human|agent', 'human')
|
|
1155
|
+
.option('--session <session-id>', 'Session id override')
|
|
1156
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
1157
|
+
.addHelpText('after', '\nExamples:\n $ sidecar task set-status T-001 --to active --reason "Trigger is now satisfied"\n $ sidecar task set-status T-001 --to done --reason "Administrative cleanup" --by agent --json')
|
|
1158
|
+
.action((taskIdText, opts) => {
|
|
1159
|
+
const command = 'task set-status';
|
|
1160
|
+
try {
|
|
1161
|
+
const taskId = taskIdText.trim().toUpperCase();
|
|
1162
|
+
const toStatus = taskPacketStatusSchema.parse(opts.to);
|
|
1163
|
+
const reason = String(opts.reason ?? '').trim();
|
|
1164
|
+
if (!reason)
|
|
1165
|
+
fail('Reason is required');
|
|
1166
|
+
const by = actorSchema.parse(opts.by);
|
|
1167
|
+
const { db, projectId } = requireInitialized();
|
|
1168
|
+
const sessionId = maybeSessionId(db, projectId, opts.session);
|
|
1169
|
+
const result = transitionTaskStatus(resolveProjectRoot(), taskId, toStatus);
|
|
1170
|
+
const eventId = createEvent(db, {
|
|
1171
|
+
projectId,
|
|
1172
|
+
type: 'note',
|
|
1173
|
+
title: `Task ${result.task_id} status changed`,
|
|
1174
|
+
summary: `${result.from_status} -> ${result.to_status}: ${reason}`,
|
|
1175
|
+
details: {
|
|
1176
|
+
task_id: result.task_id,
|
|
1177
|
+
from_status: result.from_status,
|
|
1178
|
+
to_status: result.to_status,
|
|
1179
|
+
reason,
|
|
1180
|
+
command: 'task set-status',
|
|
1181
|
+
},
|
|
1182
|
+
createdBy: by,
|
|
1183
|
+
sessionId,
|
|
1184
|
+
});
|
|
1185
|
+
db.close();
|
|
1186
|
+
respondSuccess(command, Boolean(opts.json), {
|
|
1187
|
+
task: result,
|
|
1188
|
+
event: {
|
|
1189
|
+
id: eventId,
|
|
1190
|
+
type: 'note',
|
|
1191
|
+
title: `Task ${result.task_id} status changed`,
|
|
1192
|
+
summary: `${result.from_status} -> ${result.to_status}: ${reason}`,
|
|
1193
|
+
created_by: by,
|
|
1194
|
+
session_id: sessionId,
|
|
1195
|
+
created_at: nowIso(),
|
|
1196
|
+
},
|
|
1197
|
+
}, [
|
|
1198
|
+
`Updated ${result.task_id}: ${result.from_status} -> ${result.to_status}.`,
|
|
1199
|
+
`Reason: ${reason}`,
|
|
1200
|
+
`Recorded note event #${eventId}.`,
|
|
1201
|
+
]);
|
|
1202
|
+
}
|
|
1203
|
+
catch (err) {
|
|
1204
|
+
handleCommandError(command, Boolean(opts.json), err);
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1161
1207
|
task
|
|
1162
1208
|
.command('create-followup <run-id>')
|
|
1163
1209
|
.description('Create a follow-up task packet from a run report')
|
|
@@ -1404,7 +1450,7 @@ const run = program
|
|
|
1404
1450
|
.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');
|
|
1405
1451
|
run
|
|
1406
1452
|
.command('queue')
|
|
1407
|
-
.description('
|
|
1453
|
+
.description('Evaluate active tasks and mark dependency-blocked tasks')
|
|
1408
1454
|
.option('--json', 'Print machine-readable JSON output')
|
|
1409
1455
|
.addHelpText('after', '\nExamples:\n $ sidecar run queue\n $ sidecar run queue --json')
|
|
1410
1456
|
.action((opts) => {
|
|
@@ -1413,7 +1459,7 @@ run
|
|
|
1413
1459
|
const rootPath = resolveProjectRoot();
|
|
1414
1460
|
const decisions = queueReadyTasks(rootPath);
|
|
1415
1461
|
respondSuccess(command, Boolean(opts.json), { decisions }, [
|
|
1416
|
-
`Processed ${decisions.length}
|
|
1462
|
+
`Processed ${decisions.length} active task(s).`,
|
|
1417
1463
|
...decisions.map((d) => `- ${d.task_id}: ${d.reason}`),
|
|
1418
1464
|
]);
|
|
1419
1465
|
}
|
|
@@ -1423,7 +1469,7 @@ run
|
|
|
1423
1469
|
});
|
|
1424
1470
|
run
|
|
1425
1471
|
.command('start-ready')
|
|
1426
|
-
.description('
|
|
1472
|
+
.description('Evaluate active tasks and run the ones with satisfied triggers')
|
|
1427
1473
|
.option('--dry-run', 'Prepare and compile only without executing external runners')
|
|
1428
1474
|
.option('--json', 'Print machine-readable JSON output')
|
|
1429
1475
|
.addHelpText('after', '\nExamples:\n $ sidecar run start-ready\n $ sidecar run start-ready --dry-run --json')
|
|
@@ -1432,9 +1478,13 @@ run
|
|
|
1432
1478
|
try {
|
|
1433
1479
|
const rootPath = resolveProjectRoot();
|
|
1434
1480
|
const queueDecisions = queueReadyTasks(rootPath);
|
|
1435
|
-
const
|
|
1481
|
+
const runnableTasks = queueDecisions
|
|
1482
|
+
.filter((d) => d.queued)
|
|
1483
|
+
.map((d) => d.task_id)
|
|
1484
|
+
.map((taskId) => getTaskPacket(rootPath, taskId))
|
|
1485
|
+
.filter((task) => task.status === 'active');
|
|
1436
1486
|
const results = [];
|
|
1437
|
-
for (const task of
|
|
1487
|
+
for (const task of runnableTasks) {
|
|
1438
1488
|
const result = await runTaskExecution({
|
|
1439
1489
|
rootPath,
|
|
1440
1490
|
taskId: task.task_id,
|
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
// Adapter from TaskPacket → CompileSectionsInput. Mirrors the legacy packet
|
|
2
|
-
// layout exactly so `sidecar run <task-id>` produces byte-identical prompts
|
|
3
|
-
// after the compiler refactor. Snapshot-guarded in `prompts.compat.test`.
|
|
4
1
|
import { nowIso } from '../lib/format.js';
|
|
5
2
|
import { PROMPT_PREFERENCE_DEFAULTS } from '../runners/config.js';
|
|
3
|
+
function textSection(id, title, content) {
|
|
4
|
+
return { id, title, kind: 'text', content, trim: 'keep' };
|
|
5
|
+
}
|
|
6
|
+
function listSection(id, title, items, options) {
|
|
7
|
+
return {
|
|
8
|
+
id,
|
|
9
|
+
title,
|
|
10
|
+
kind: 'list',
|
|
11
|
+
items,
|
|
12
|
+
...(options?.empty_placeholder ? { empty_placeholder: options.empty_placeholder } : {}),
|
|
13
|
+
...(options?.trim ? { trim: options.trim } : { trim: { policy: 'keep' } }),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
6
16
|
function finalResponseFormat(runner) {
|
|
7
17
|
if (runner === 'codex') {
|
|
8
18
|
return [
|
|
9
19
|
'- Start with a one-line outcome summary.',
|
|
10
20
|
'- List files changed with concise reasons.',
|
|
11
|
-
'- Include validation
|
|
21
|
+
'- Include validation command output.',
|
|
12
22
|
'- Note risks, blockers, or follow-up tasks.',
|
|
13
23
|
];
|
|
14
24
|
}
|
|
@@ -33,59 +43,32 @@ function runnerGuidance(runner) {
|
|
|
33
43
|
'Provide a clear summary with validation and follow-up notes at the end.',
|
|
34
44
|
];
|
|
35
45
|
}
|
|
36
|
-
function textSection(id, title, content) {
|
|
37
|
-
return { id, title, kind: 'text', content, trim: 'keep' };
|
|
38
|
-
}
|
|
39
|
-
function listSection(id, title, items, options) {
|
|
40
|
-
return {
|
|
41
|
-
id,
|
|
42
|
-
title,
|
|
43
|
-
kind: 'list',
|
|
44
|
-
items,
|
|
45
|
-
...(options?.empty_placeholder ? { empty_placeholder: options.empty_placeholder } : {}),
|
|
46
|
-
...(options?.trim ? { trim: options.trim } : { trim: { policy: 'keep' } }),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function validationLine(v) {
|
|
50
|
-
const label = v.name ? `${v.kind}:${v.name}` : v.kind;
|
|
51
|
-
return v.kind === 'custom' ? v.command : `${label} — \`${v.command}\``;
|
|
52
|
-
}
|
|
53
|
-
// Linked context uses two sub-lists (decisions + notes) under one heading. The
|
|
54
|
-
// legacy layout merged them so we render as a single `text` section whose
|
|
55
|
-
// content is the pre-formatted bullet list, and trim by hand before handing it
|
|
56
|
-
// to the core. That keeps byte-identical output without teaching the core
|
|
57
|
-
// about multi-list sections.
|
|
58
46
|
function renderLinkedContext(relatedDecisions, relatedNotes, mode) {
|
|
59
47
|
const lines = [];
|
|
60
48
|
const decisions = mode === 'full'
|
|
61
49
|
? relatedDecisions
|
|
62
50
|
: mode === 'trim'
|
|
63
|
-
?
|
|
64
|
-
:
|
|
51
|
+
? relatedDecisions.slice(0, 3)
|
|
52
|
+
: relatedDecisions.slice(0, 1);
|
|
65
53
|
const notes = mode === 'full'
|
|
66
54
|
? relatedNotes
|
|
67
55
|
: mode === 'trim'
|
|
68
|
-
?
|
|
69
|
-
:
|
|
70
|
-
if (decisions.length
|
|
71
|
-
lines.push('
|
|
72
|
-
else
|
|
56
|
+
? relatedNotes.slice(0, 2)
|
|
57
|
+
: [];
|
|
58
|
+
if (decisions.length > 0) {
|
|
59
|
+
lines.push('Decisions:');
|
|
73
60
|
for (const d of decisions)
|
|
74
61
|
lines.push(`- ${d}`);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
62
|
+
}
|
|
63
|
+
if (notes.length > 0) {
|
|
64
|
+
lines.push('Notes:');
|
|
78
65
|
for (const n of notes)
|
|
79
66
|
lines.push(`- ${n}`);
|
|
67
|
+
}
|
|
68
|
+
if (lines.length === 0)
|
|
69
|
+
lines.push('- no linked context');
|
|
80
70
|
return lines;
|
|
81
71
|
}
|
|
82
|
-
function sliceWithOverflow(items, limit, label) {
|
|
83
|
-
if (items.length <= limit)
|
|
84
|
-
return items;
|
|
85
|
-
const kept = items.slice(0, limit);
|
|
86
|
-
kept.push(`+ ${items.length - limit} more ${label} (see task packet for full list)`);
|
|
87
|
-
return kept;
|
|
88
|
-
}
|
|
89
72
|
function renderPreviousRuns(runs) {
|
|
90
73
|
const lines = [];
|
|
91
74
|
runs.forEach((prev, idx) => {
|
|
@@ -102,34 +85,12 @@ function renderPreviousRuns(runs) {
|
|
|
102
85
|
lines.push(`- Changed files (${prev.changed_files.length}):`);
|
|
103
86
|
for (const f of limited)
|
|
104
87
|
lines.push(` - ${f}`);
|
|
105
|
-
if (prev.changed_files.length > limited.length)
|
|
106
|
-
lines.push(` - + ${prev.changed_files.length - limited.length} more
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
if (prev.log_tail) {
|
|
110
|
-
lines.push('- Log tail:');
|
|
111
|
-
lines.push('```');
|
|
112
|
-
for (const line of prev.log_tail.split('\n'))
|
|
113
|
-
lines.push(line);
|
|
114
|
-
lines.push('```');
|
|
88
|
+
if (prev.changed_files.length > limited.length)
|
|
89
|
+
lines.push(` - + ${prev.changed_files.length - limited.length} more`);
|
|
115
90
|
}
|
|
116
91
|
});
|
|
117
92
|
return lines;
|
|
118
93
|
}
|
|
119
|
-
function renderConstraints(technical, design) {
|
|
120
|
-
const lines = [];
|
|
121
|
-
if (technical.length === 0)
|
|
122
|
-
lines.push('- no technical constraints');
|
|
123
|
-
else
|
|
124
|
-
for (const t of technical)
|
|
125
|
-
lines.push(`- ${t}`);
|
|
126
|
-
if (design.length === 0)
|
|
127
|
-
lines.push('- no design constraints');
|
|
128
|
-
else
|
|
129
|
-
for (const d of design)
|
|
130
|
-
lines.push(`- ${d}`);
|
|
131
|
-
return lines;
|
|
132
|
-
}
|
|
133
94
|
export function packetToCompileInput(input) {
|
|
134
95
|
const { task, run, runner, agentRole, linkedContext, budget } = input;
|
|
135
96
|
const pref = budget ?? PROMPT_PREFERENCE_DEFAULTS;
|
|
@@ -142,38 +103,30 @@ export function packetToCompileInput(input) {
|
|
|
142
103
|
`Task id: ${task.task_id}`,
|
|
143
104
|
`Compiled at: ${nowIso()}`,
|
|
144
105
|
];
|
|
145
|
-
const relatedDecisions = linkedContext?.related_decisions ??
|
|
146
|
-
const relatedNotes = linkedContext?.related_notes ??
|
|
106
|
+
const relatedDecisions = linkedContext?.related_decisions ?? [];
|
|
107
|
+
const relatedNotes = linkedContext?.related_notes ?? [];
|
|
147
108
|
const sections = [
|
|
148
109
|
textSection('task', 'Task', [
|
|
149
110
|
`- ${task.title}`,
|
|
150
|
-
`- Type: ${task.type}`,
|
|
151
111
|
`- Priority: ${task.priority}`,
|
|
152
112
|
`- Status: ${task.status}`,
|
|
113
|
+
`- Created: ${task.created_at}`,
|
|
153
114
|
]),
|
|
154
|
-
textSection('objective', 'Objective', [task.
|
|
155
|
-
textSection('
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
listSection('files_to_read', 'Read these first', task.implementation.files_to_read, {
|
|
163
|
-
trim: { policy: 'trim-last', limit: 10, limit_strict: 10, overflow_label: 'read-first files' },
|
|
164
|
-
}),
|
|
165
|
-
listSection('files_to_avoid', 'Avoid changing', task.implementation.files_to_avoid, {
|
|
166
|
-
trim: { policy: 'trim-last', limit: 5, limit_strict: 3, overflow_label: 'avoid files' },
|
|
115
|
+
textSection('objective', 'Objective', [task.summary]),
|
|
116
|
+
textSection('trigger', 'Trigger', [
|
|
117
|
+
`- Condition: ${task.trigger.condition}`,
|
|
118
|
+
...(task.trigger.check_command ? [`- Verify: \`${task.trigger.check_command}\``] : []),
|
|
119
|
+
...(task.trigger.depends_on.length > 0 ? [`- Depends on: ${task.trigger.depends_on.join(', ')}`] : []),
|
|
120
|
+
]),
|
|
121
|
+
listSection('entry_points', 'Entry points', task.entry_points, {
|
|
122
|
+
trim: { policy: 'trim-last', limit: 3, limit_strict: 3, overflow_label: 'entry points' },
|
|
167
123
|
}),
|
|
168
|
-
|
|
124
|
+
textSection('definition_of_done', 'Definition of done', [task.done_condition]),
|
|
125
|
+
textSection('validation', 'Validation command', [`\`${task.validation_command}\``]),
|
|
169
126
|
textSection('linked_context', 'Linked context', renderLinkedContext(relatedDecisions, relatedNotes, 'full')),
|
|
170
|
-
// Previous runner context — only when this run is a later step in a pipeline.
|
|
171
127
|
...(linkedContext?.previous_runs && linkedContext.previous_runs.length > 0
|
|
172
128
|
? [textSection('previous_runs', 'Previous runner context', renderPreviousRuns(linkedContext.previous_runs))]
|
|
173
129
|
: []),
|
|
174
|
-
textSection('constraints', 'Constraints', renderConstraints(task.constraints.technical, task.constraints.design)),
|
|
175
|
-
listSection('validation', 'Validation', task.execution.commands.validation.map(validationLine)),
|
|
176
|
-
listSection('definition_of_done', 'Definition of done', task.definition_of_done),
|
|
177
130
|
textSection('runner_guidance', 'Runner guidance', runnerGuidance(runner)),
|
|
178
131
|
textSection('final_response_format', 'Final response format', finalResponseFormat(runner)),
|
|
179
132
|
];
|
|
@@ -183,21 +136,6 @@ export function packetToCompileInput(input) {
|
|
|
183
136
|
budget: { target: pref.budget_target, max: pref.budget_max },
|
|
184
137
|
};
|
|
185
138
|
}
|
|
186
|
-
// Legacy metadata expects `trimmed_sections: string[]` using the historical names.
|
|
187
|
-
// Map the new core metadata back for back-compat.
|
|
188
|
-
export const LEGACY_TRIM_IDS = [
|
|
189
|
-
'in_scope',
|
|
190
|
-
'out_of_scope',
|
|
191
|
-
'files_to_read',
|
|
192
|
-
'files_to_avoid',
|
|
193
|
-
'related_decisions',
|
|
194
|
-
'related_notes',
|
|
195
|
-
];
|
|
196
|
-
// Rebuild linked_context lines under a trim mode. The core compileSections()
|
|
197
|
-
// can't partially trim a text section, so we run the full pipeline twice in
|
|
198
|
-
// prompt-compiler.ts: first with full linked_context, and again with trimmed
|
|
199
|
-
// linked_context if the baseline is over budget. See prompt-compiler.ts for
|
|
200
|
-
// the wrapper that orchestrates this.
|
|
201
139
|
export function linkedContextForMode(relatedDecisions, relatedNotes, mode) {
|
|
202
140
|
return renderLinkedContext(relatedDecisions, relatedNotes, mode);
|
|
203
141
|
}
|
|
@@ -12,8 +12,8 @@ function packetInputForMode(adapterInput, mode) {
|
|
|
12
12
|
const baseline = packetToCompileInput(adapterInput);
|
|
13
13
|
if (mode === 'full')
|
|
14
14
|
return baseline;
|
|
15
|
-
const relatedDecisions = adapterInput.linkedContext?.related_decisions ??
|
|
16
|
-
const relatedNotes = adapterInput.linkedContext?.related_notes ??
|
|
15
|
+
const relatedDecisions = adapterInput.linkedContext?.related_decisions ?? [];
|
|
16
|
+
const relatedNotes = adapterInput.linkedContext?.related_notes ?? [];
|
|
17
17
|
const linkedLines = linkedContextForMode(relatedDecisions, relatedNotes, mode);
|
|
18
18
|
const sections = baseline.sections.map((section) => {
|
|
19
19
|
if (section.id !== 'linked_context')
|
|
@@ -27,8 +27,8 @@ function packetInputForMode(adapterInput, mode) {
|
|
|
27
27
|
function buildLegacyTrimmed(adapterInput, listTrimmed, mode) {
|
|
28
28
|
const out = new Set(listTrimmed);
|
|
29
29
|
if (mode !== 'full') {
|
|
30
|
-
const decisions = adapterInput.linkedContext?.related_decisions ??
|
|
31
|
-
const notes = adapterInput.linkedContext?.related_notes ??
|
|
30
|
+
const decisions = adapterInput.linkedContext?.related_decisions ?? [];
|
|
31
|
+
const notes = adapterInput.linkedContext?.related_notes ?? [];
|
|
32
32
|
if (mode === 'trim') {
|
|
33
33
|
if (decisions.length > 3)
|
|
34
34
|
out.add('related_decisions');
|