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.
@@ -1,10 +1,21 @@
1
+ import { JSON_CONTRACT_VERSION } from '../lib/output.js';
1
2
  export function getCapabilitiesManifest(version) {
2
3
  return {
3
- schema_version: 1,
4
- cli: {
5
- name: 'sidecar',
6
- version,
7
- },
4
+ cli_name: 'sidecar',
5
+ cli_version: version,
6
+ json_contract_version: JSON_CONTRACT_VERSION,
7
+ features: [
8
+ 'json_envelope_v1',
9
+ 'capabilities_manifest',
10
+ 'event_ingest',
11
+ 'export_json',
12
+ 'export_jsonl_events',
13
+ 'run_records_v1',
14
+ 'prompt_compiler_v1',
15
+ 'task_orchestration_v1',
16
+ 'run_review_workflow_v1',
17
+ 'optional_local_ui',
18
+ ],
8
19
  commands: [
9
20
  {
10
21
  name: 'init',
@@ -20,6 +31,29 @@ export function getCapabilitiesManifest(version) {
20
31
  arguments: [],
21
32
  options: ['--json'],
22
33
  },
34
+ {
35
+ name: 'preferences',
36
+ description: 'Preferences operations',
37
+ json_output: true,
38
+ arguments: [],
39
+ options: [],
40
+ subcommands: [
41
+ {
42
+ name: 'show',
43
+ description: 'Show project preferences',
44
+ json_output: true,
45
+ arguments: [],
46
+ options: ['--json'],
47
+ },
48
+ ],
49
+ },
50
+ {
51
+ name: 'ui',
52
+ description: 'Launch optional local Sidecar UI (lazy-installed on first run)',
53
+ json_output: false,
54
+ arguments: [],
55
+ options: ['--no-open', '--port <port>', '--install-only', '--project <path>', '--reinstall'],
56
+ },
23
57
  {
24
58
  name: 'capabilities',
25
59
  description: 'Show machine-readable CLI manifest',
@@ -34,6 +68,40 @@ export function getCapabilitiesManifest(version) {
34
68
  arguments: [],
35
69
  options: ['--limit <n>', '--format text|markdown|json', '--json'],
36
70
  },
71
+ {
72
+ name: 'event',
73
+ description: 'Generic event ingest operations',
74
+ json_output: true,
75
+ arguments: [],
76
+ options: [],
77
+ subcommands: [
78
+ {
79
+ name: 'add',
80
+ description: 'Add a validated generic event payload',
81
+ json_output: true,
82
+ arguments: [],
83
+ options: [
84
+ '--type <type>',
85
+ '--title <title>',
86
+ '--summary <summary>',
87
+ '--details-json <json>',
88
+ '--created-by <by>',
89
+ '--source <source>',
90
+ '--session-id <id>',
91
+ '--json-input <json>',
92
+ '--stdin',
93
+ '--json',
94
+ ],
95
+ },
96
+ ],
97
+ },
98
+ {
99
+ name: 'export',
100
+ description: 'Export project memory in JSON or JSONL',
101
+ json_output: true,
102
+ arguments: [],
103
+ options: ['--format json|jsonl', '--limit <n>', '--type <event-type>', '--since <iso-date>', '--until <iso-date>', '--output <path>', '--json'],
104
+ },
37
105
  {
38
106
  name: 'summary',
39
107
  description: 'Summary operations',
@@ -104,25 +172,136 @@ export function getCapabilitiesManifest(version) {
104
172
  options: [],
105
173
  subcommands: [
106
174
  {
107
- name: 'add',
108
- description: 'Create an open task',
175
+ name: 'create',
176
+ description: 'Create a structured task packet',
177
+ json_output: true,
178
+ arguments: [],
179
+ options: [
180
+ '--title <title>',
181
+ '--summary <summary>',
182
+ '--goal <goal>',
183
+ '--type feature|bug|chore|research',
184
+ '--status draft|ready|queued|running|review|blocked|done',
185
+ '--priority low|medium|high',
186
+ '--dependencies <task-ids>',
187
+ '--tags <tags>',
188
+ '--target-areas <areas>',
189
+ '--scope-in <items>',
190
+ '--scope-out <items>',
191
+ '--related-decisions <items>',
192
+ '--related-notes <items>',
193
+ '--files-read <paths>',
194
+ '--files-avoid <paths>',
195
+ '--constraint-tech <items>',
196
+ '--constraint-design <items>',
197
+ '--validate-cmds <commands>',
198
+ '--dod <items>',
199
+ '--branch <name>',
200
+ '--worktree <path>',
201
+ '--json',
202
+ ],
203
+ },
204
+ {
205
+ name: 'show',
206
+ description: 'Show a task packet',
207
+ json_output: true,
208
+ arguments: ['<task-id>'],
209
+ options: ['--json'],
210
+ },
211
+ {
212
+ name: 'list',
213
+ description: 'List task packets',
214
+ json_output: true,
215
+ arguments: [],
216
+ options: ['--status draft|ready|queued|running|review|blocked|done|all', '--json'],
217
+ },
218
+ {
219
+ name: 'assign',
220
+ description: 'Assign task to an agent role and runner',
109
221
  json_output: true,
110
- arguments: ['<title>'],
111
- options: ['--description <text>', '--priority low|medium|high', '--by human|agent', '--json'],
222
+ arguments: ['<task-id>'],
223
+ options: ['--agent-role planner|builder-ui|builder-app|reviewer|tester', '--runner codex|claude', '--json'],
112
224
  },
113
225
  {
114
- name: 'done',
115
- description: 'Mark a task as done',
226
+ name: 'create-followup',
227
+ description: 'Create follow-up task from run report',
228
+ json_output: true,
229
+ arguments: ['<run-id>'],
230
+ options: ['--json'],
231
+ },
232
+ ],
233
+ },
234
+ {
235
+ name: 'prompt',
236
+ description: 'Prompt compilation operations',
237
+ json_output: true,
238
+ arguments: [],
239
+ options: [],
240
+ subcommands: [
241
+ {
242
+ name: 'compile',
243
+ description: 'Compile an execution brief prompt from a task packet',
116
244
  json_output: true,
117
245
  arguments: ['<task-id>'],
118
- options: ['--by human|agent', '--json'],
246
+ options: ['--runner codex|claude', '--agent-role <role>', '--preview', '--json'],
247
+ },
248
+ ],
249
+ },
250
+ {
251
+ name: 'run',
252
+ description: 'Execute a task run or inspect run records',
253
+ json_output: true,
254
+ arguments: ['[task-id]'],
255
+ options: ['--runner codex|claude', '--agent-role planner|builder-ui|builder-app|reviewer|tester', '--dry-run', '--json'],
256
+ subcommands: [
257
+ {
258
+ name: 'queue',
259
+ description: 'Queue ready tasks with satisfied dependencies',
260
+ json_output: true,
261
+ arguments: [],
262
+ options: ['--json'],
263
+ },
264
+ {
265
+ name: 'start-ready',
266
+ description: 'Queue and start all runnable ready tasks',
267
+ json_output: true,
268
+ arguments: [],
269
+ options: ['--dry-run', '--json'],
270
+ },
271
+ {
272
+ name: 'approve',
273
+ description: 'Review run as approved, needs changes, or merged',
274
+ json_output: true,
275
+ arguments: ['<run-id>'],
276
+ options: ['--state approved|needs_changes|merged', '--note <text>', '--by <name>', '--json'],
277
+ },
278
+ {
279
+ name: 'block',
280
+ description: 'Mark run blocked',
281
+ json_output: true,
282
+ arguments: ['<run-id>'],
283
+ options: ['--note <text>', '--by <name>', '--json'],
119
284
  },
120
285
  {
121
286
  name: 'list',
122
- description: 'List tasks',
287
+ description: 'List run records',
123
288
  json_output: true,
124
289
  arguments: [],
125
- options: ['--status open|done|all', '--format table|json', '--json'],
290
+ options: ['--task <task-id>', '--status queued|preparing|running|review|blocked|completed|failed|all', '--json'],
291
+ },
292
+ {
293
+ name: 'show',
294
+ description: 'Show a run record',
295
+ json_output: true,
296
+ arguments: ['<run-id>'],
297
+ options: ['--json'],
298
+ },
299
+ {
300
+ name: 'summary',
301
+ description: 'Show project-level run review summary',
302
+ json_output: true,
303
+ arguments: [],
304
+ options: ['--json'],
126
305
  },
127
306
  ],
128
307
  },
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ import { createEvent } from './event-service.js';
3
+ const eventTypeSchema = z.enum([
4
+ 'note',
5
+ 'decision',
6
+ 'worklog',
7
+ 'task_created',
8
+ 'task_completed',
9
+ 'summary_generated',
10
+ ]);
11
+ const createdBySchema = z.enum(['human', 'agent', 'system']).default('system');
12
+ const sourceSchema = z.enum(['cli', 'imported', 'generated']).default('cli');
13
+ export const eventIngestSchema = z
14
+ .object({
15
+ type: eventTypeSchema,
16
+ title: z.string().trim().min(1).optional(),
17
+ summary: z.string().trim().min(1).optional(),
18
+ details_json: z.record(z.string(), z.unknown()).optional(),
19
+ created_by: createdBySchema.optional(),
20
+ source: sourceSchema.optional(),
21
+ session_id: z.number().int().positive().nullable().optional(),
22
+ })
23
+ .superRefine((payload, ctx) => {
24
+ if (payload.type === 'decision') {
25
+ if (!payload.title)
26
+ ctx.addIssue({ code: 'custom', path: ['title'], message: 'title is required for decision events' });
27
+ if (!payload.summary)
28
+ ctx.addIssue({ code: 'custom', path: ['summary'], message: 'summary is required for decision events' });
29
+ return;
30
+ }
31
+ if (payload.type === 'summary_generated')
32
+ return;
33
+ if (!payload.summary) {
34
+ ctx.addIssue({ code: 'custom', path: ['summary'], message: `summary is required for ${payload.type} events` });
35
+ }
36
+ });
37
+ export function ingestEvent(db, input) {
38
+ const payload = eventIngestSchema.parse(input.payload);
39
+ const title = payload.title ??
40
+ (payload.type === 'note'
41
+ ? 'Note'
42
+ : payload.type === 'worklog'
43
+ ? 'Worklog entry'
44
+ : payload.type === 'task_created'
45
+ ? 'Task created'
46
+ : payload.type === 'task_completed'
47
+ ? 'Task completed'
48
+ : payload.type === 'summary_generated'
49
+ ? 'Summary refreshed'
50
+ : 'Event');
51
+ const summary = payload.summary ?? '';
52
+ const eventId = createEvent(db, {
53
+ projectId: input.project_id,
54
+ type: payload.type,
55
+ title,
56
+ summary,
57
+ details: payload.details_json ?? {},
58
+ createdBy: payload.created_by ?? 'system',
59
+ source: payload.source ?? 'cli',
60
+ sessionId: payload.session_id ?? null,
61
+ });
62
+ return {
63
+ id: eventId,
64
+ type: payload.type,
65
+ title,
66
+ summary,
67
+ details_json: payload.details_json ?? {},
68
+ created_by: payload.created_by ?? 'system',
69
+ source: payload.source ?? 'cli',
70
+ session_id: payload.session_id ?? null,
71
+ };
72
+ }
@@ -0,0 +1,79 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { JSON_CONTRACT_VERSION } from '../lib/output.js';
4
+ export function buildExportJson(db, input) {
5
+ const project = db.prepare('SELECT id, name, root_path, created_at, updated_at FROM projects WHERE id = ?').get(input.projectId);
6
+ const preferencesPath = path.join(input.rootPath, '.sidecar', 'preferences.json');
7
+ const preferences = fs.existsSync(preferencesPath)
8
+ ? JSON.parse(fs.readFileSync(preferencesPath, 'utf8'))
9
+ : null;
10
+ const sessions = db
11
+ .prepare('SELECT id, project_id, started_at, ended_at, actor_type, actor_name, summary FROM sessions WHERE project_id = ? ORDER BY started_at DESC')
12
+ .all(input.projectId);
13
+ const tasks = db
14
+ .prepare(`SELECT id, project_id, title, description, status, priority, created_at, updated_at, closed_at, origin_event_id FROM tasks WHERE project_id = ? ORDER BY CASE WHEN status = 'open' THEN 0 ELSE 1 END, updated_at DESC`)
15
+ .all(input.projectId);
16
+ const artifacts = db
17
+ .prepare('SELECT id, project_id, path, kind, note, created_at FROM artifacts WHERE project_id = ? ORDER BY created_at DESC')
18
+ .all(input.projectId);
19
+ const eventWhere = ['project_id = ?'];
20
+ const eventArgs = [input.projectId];
21
+ if (input.type) {
22
+ eventWhere.push('type = ?');
23
+ eventArgs.push(input.type);
24
+ }
25
+ if (input.since) {
26
+ eventWhere.push('created_at >= ?');
27
+ eventArgs.push(input.since);
28
+ }
29
+ if (input.until) {
30
+ eventWhere.push('created_at <= ?');
31
+ eventArgs.push(input.until);
32
+ }
33
+ const limitClause = input.limit && input.limit > 0 ? ` LIMIT ${Math.floor(input.limit)}` : '';
34
+ const events = db
35
+ .prepare(`SELECT id, project_id, type, title, summary, details_json, created_at, created_by, source, session_id
36
+ FROM events
37
+ WHERE ${eventWhere.join(' AND ')}
38
+ ORDER BY created_at DESC${limitClause}`)
39
+ .all(...eventArgs);
40
+ return {
41
+ version: JSON_CONTRACT_VERSION,
42
+ project,
43
+ preferences: preferences,
44
+ sessions: sessions,
45
+ tasks: tasks,
46
+ artifacts: artifacts,
47
+ events,
48
+ };
49
+ }
50
+ export function buildExportJsonlEvents(db, input) {
51
+ const where = ['project_id = ?'];
52
+ const args = [input.projectId];
53
+ if (input.type) {
54
+ where.push('type = ?');
55
+ args.push(input.type);
56
+ }
57
+ if (input.since) {
58
+ where.push('created_at >= ?');
59
+ args.push(input.since);
60
+ }
61
+ if (input.until) {
62
+ where.push('created_at <= ?');
63
+ args.push(input.until);
64
+ }
65
+ const limitClause = input.limit && input.limit > 0 ? ` LIMIT ${Math.floor(input.limit)}` : '';
66
+ const rows = db
67
+ .prepare(`SELECT id, project_id, type, title, summary, details_json, created_at, created_by, source, session_id
68
+ FROM events
69
+ WHERE ${where.join(' AND ')}
70
+ ORDER BY created_at DESC${limitClause}`)
71
+ .all(...args);
72
+ return rows.map((row) => JSON.stringify({ version: JSON_CONTRACT_VERSION, record_type: 'event', project_id: row.project_id, data: row }));
73
+ }
74
+ export function writeOutputFile(outputPath, content) {
75
+ const abs = path.resolve(outputPath);
76
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
77
+ fs.writeFileSync(abs, content);
78
+ return abs;
79
+ }
@@ -0,0 +1,59 @@
1
+ import { nowIso } from '../lib/format.js';
2
+ import { compileTaskPrompt } from '../prompts/prompt-service.js';
3
+ import { getTaskPacket } from '../tasks/task-service.js';
4
+ import { getRunnerAdapter } from '../runners/factory.js';
5
+ import { loadRunnerPreferences } from '../runners/config.js';
6
+ import { updateRunRecordEntry } from '../runs/run-service.js';
7
+ import { saveTaskPacket } from '../tasks/task-service.js';
8
+ export function runTaskExecution(input) {
9
+ const prefs = loadRunnerPreferences(input.rootPath);
10
+ const dryRun = Boolean(input.dryRun);
11
+ const task = getTaskPacket(input.rootPath, input.taskId);
12
+ const runner = input.runner ?? task.tracking.assigned_runner ?? prefs.default_runner;
13
+ const agentRole = input.agentRole ?? task.tracking.assigned_agent_role ?? prefs.default_agent_role;
14
+ const compiled = compileTaskPrompt({
15
+ rootPath: input.rootPath,
16
+ taskId: task.task_id,
17
+ runner,
18
+ agentRole,
19
+ });
20
+ const adapter = getRunnerAdapter(runner);
21
+ saveTaskPacket(input.rootPath, { ...task, status: 'running' });
22
+ updateRunRecordEntry(input.rootPath, compiled.run_id, {
23
+ status: 'running',
24
+ branch: task.tracking.branch,
25
+ worktree: task.tracking.worktree || input.rootPath,
26
+ });
27
+ const prepared = adapter.prepare({
28
+ runId: compiled.run_id,
29
+ taskId: task.task_id,
30
+ agentRole,
31
+ promptPath: compiled.prompt_path,
32
+ projectRoot: input.rootPath,
33
+ });
34
+ const executed = adapter.execute({ prepared, dryRun });
35
+ const collected = adapter.collectResult(executed);
36
+ const finishedStatus = collected.ok ? 'completed' : 'failed';
37
+ const nextTaskStatus = collected.ok ? 'review' : 'blocked';
38
+ saveTaskPacket(input.rootPath, { ...getTaskPacket(input.rootPath, task.task_id), status: nextTaskStatus });
39
+ updateRunRecordEntry(input.rootPath, compiled.run_id, {
40
+ status: finishedStatus,
41
+ completed_at: nowIso(),
42
+ summary: collected.summary,
43
+ commands_run: collected.commandsRun,
44
+ validation_results: collected.validationResults,
45
+ blockers: collected.blockers,
46
+ follow_ups: collected.followUps,
47
+ });
48
+ return {
49
+ task_id: task.task_id,
50
+ run_id: compiled.run_id,
51
+ runner_type: runner,
52
+ agent_role: agentRole,
53
+ prompt_path: compiled.prompt_path,
54
+ status: finishedStatus,
55
+ dry_run: dryRun,
56
+ shell_command: prepared.shellLine,
57
+ summary: collected.summary,
58
+ };
59
+ }
@@ -0,0 +1,76 @@
1
+ import { SidecarError } from '../lib/errors.js';
2
+ import { nowIso } from '../lib/format.js';
3
+ import { getRunRecord, listRunRecords, updateRunRecordEntry } from '../runs/run-service.js';
4
+ import { createTaskPacketRecord, getTaskPacket, saveTaskPacket } from '../tasks/task-service.js';
5
+ function taskStatusForReview(state) {
6
+ if (state === 'approved')
7
+ return 'review';
8
+ if (state === 'needs_changes')
9
+ return 'ready';
10
+ if (state === 'blocked')
11
+ return 'blocked';
12
+ if (state === 'merged')
13
+ return 'done';
14
+ return 'review';
15
+ }
16
+ export function reviewRun(rootPath, runId, state, options) {
17
+ const run = getRunRecord(rootPath, runId);
18
+ if (run.status !== 'completed' && run.status !== 'failed' && run.status !== 'blocked') {
19
+ throw new SidecarError('Run must be completed, failed, or blocked before review actions');
20
+ }
21
+ updateRunRecordEntry(rootPath, run.run_id, {
22
+ review_state: state,
23
+ reviewed_at: nowIso(),
24
+ reviewed_by: options?.by ?? 'human',
25
+ review_note: options?.note ?? '',
26
+ });
27
+ const task = getTaskPacket(rootPath, run.task_id);
28
+ const nextTaskStatus = taskStatusForReview(state);
29
+ saveTaskPacket(rootPath, { ...task, status: nextTaskStatus });
30
+ return {
31
+ run_id: run.run_id,
32
+ task_id: run.task_id,
33
+ review_state: state,
34
+ task_status: nextTaskStatus,
35
+ };
36
+ }
37
+ export function createFollowupTaskFromRun(rootPath, runId) {
38
+ const run = getRunRecord(rootPath, runId);
39
+ const sourceTask = getTaskPacket(rootPath, run.task_id);
40
+ const suggestions = run.follow_ups.length > 0 ? run.follow_ups : ['Investigate run issues and apply required changes'];
41
+ const created = createTaskPacketRecord(rootPath, {
42
+ title: `Follow-up: ${sourceTask.title}`,
43
+ summary: run.review_note || run.summary || 'Follow-up work from reviewed run',
44
+ goal: suggestions.join('; '),
45
+ type: sourceTask.type,
46
+ status: 'draft',
47
+ priority: sourceTask.priority,
48
+ dependencies: [sourceTask.task_id],
49
+ tags: Array.from(new Set([...sourceTask.tags, 'follow-up'])),
50
+ target_areas: sourceTask.target_areas,
51
+ files_to_read: sourceTask.implementation.files_to_read,
52
+ files_to_avoid: sourceTask.implementation.files_to_avoid,
53
+ technical_constraints: sourceTask.constraints.technical,
54
+ design_constraints: sourceTask.constraints.design,
55
+ validation_commands: sourceTask.execution.commands.validation,
56
+ definition_of_done: [...sourceTask.definition_of_done, ...suggestions],
57
+ });
58
+ return {
59
+ source_run_id: run.run_id,
60
+ task_id: created.task.task_id,
61
+ title: created.task.title,
62
+ };
63
+ }
64
+ export function buildReviewSummary(rootPath) {
65
+ const runs = listRunRecords(rootPath);
66
+ return {
67
+ completed_runs: runs.filter((r) => r.status === 'completed').length,
68
+ blocked_runs: runs.filter((r) => r.status === 'blocked' || r.review_state === 'blocked').length,
69
+ suggested_follow_ups: runs.reduce((acc, r) => acc + r.follow_ups.length, 0),
70
+ recently_merged: runs
71
+ .filter((r) => r.review_state === 'merged' && r.reviewed_at)
72
+ .sort((a, b) => String(b.reviewed_at).localeCompare(String(a.reviewed_at)))
73
+ .slice(0, 10)
74
+ .map((r) => ({ run_id: r.run_id, task_id: r.task_id, reviewed_at: r.reviewed_at || '' })),
75
+ };
76
+ }
@@ -0,0 +1,94 @@
1
+ import { nowIso } from '../lib/format.js';
2
+ import { getTaskPacket, listTaskPackets, saveTaskPacket } from '../tasks/task-service.js';
3
+ function hasUiSignal(task) {
4
+ const joined = [
5
+ ...task.tags,
6
+ ...task.target_areas,
7
+ ...task.implementation.files_to_read,
8
+ ...task.implementation.files_to_avoid,
9
+ ]
10
+ .join(' ')
11
+ .toLowerCase();
12
+ return /(ui|frontend|css|html|react|view|component)/.test(joined);
13
+ }
14
+ function pickRole(task) {
15
+ if (task.type === 'research')
16
+ return { role: 'planner', reason: 'task type is research' };
17
+ if (task.tags.some((t) => t.toLowerCase() === 'test') || task.target_areas.some((a) => /test/i.test(a))) {
18
+ return { role: 'tester', reason: 'tags/target_areas indicate testing' };
19
+ }
20
+ if (task.tags.some((t) => /review/i.test(t)) || task.type === 'bug') {
21
+ return { role: 'reviewer', reason: 'bug/review signal present' };
22
+ }
23
+ if (hasUiSignal(task))
24
+ return { role: 'builder-ui', reason: 'ui/frontend signal detected' };
25
+ return { role: 'builder-app', reason: 'default app implementation path' };
26
+ }
27
+ function defaultRunnerForRole(role) {
28
+ if (role === 'reviewer' || role === 'planner')
29
+ return 'claude';
30
+ return 'codex';
31
+ }
32
+ export function dependenciesMet(task, tasksById) {
33
+ const missing = task.dependencies.filter((depId) => tasksById.get(depId)?.status !== 'done');
34
+ return { ok: missing.length === 0, missing };
35
+ }
36
+ export function assignTask(rootPath, taskId, override) {
37
+ const task = getTaskPacket(rootPath, taskId);
38
+ const auto = pickRole(task);
39
+ const role = override?.role ?? auto.role;
40
+ const runner = override?.runner ?? defaultRunnerForRole(role);
41
+ const reason = override?.role || override?.runner ? 'manual override' : auto.reason;
42
+ const updated = {
43
+ ...task,
44
+ tracking: {
45
+ ...task.tracking,
46
+ assigned_agent_role: role,
47
+ assigned_runner: runner,
48
+ assignment_reason: reason,
49
+ assigned_at: nowIso(),
50
+ },
51
+ };
52
+ saveTaskPacket(rootPath, updated);
53
+ return { task_id: task.task_id, agent_role: role, runner, reason };
54
+ }
55
+ export function queueReadyTasks(rootPath) {
56
+ const tasks = listTaskPackets(rootPath);
57
+ const byId = new Map(tasks.map((t) => [t.task_id, t]));
58
+ const decisions = [];
59
+ for (const task of tasks) {
60
+ if (task.status !== 'ready')
61
+ continue;
62
+ const dep = dependenciesMet(task, byId);
63
+ if (!dep.ok) {
64
+ saveTaskPacket(rootPath, { ...task, status: 'blocked' });
65
+ decisions.push({ task_id: task.task_id, queued: false, reason: `blocked by dependencies: ${dep.missing.join(', ')}` });
66
+ continue;
67
+ }
68
+ const assignment = task.tracking.assigned_agent_role && task.tracking.assigned_runner
69
+ ? {
70
+ role: task.tracking.assigned_agent_role,
71
+ runner: task.tracking.assigned_runner,
72
+ }
73
+ : (() => {
74
+ const decided = assignTask(rootPath, task.task_id);
75
+ return { role: decided.agent_role, runner: decided.runner };
76
+ })();
77
+ const latest = getTaskPacket(rootPath, task.task_id);
78
+ saveTaskPacket(rootPath, {
79
+ ...latest,
80
+ status: 'queued',
81
+ tracking: {
82
+ ...latest.tracking,
83
+ assigned_agent_role: assignment.role,
84
+ assigned_runner: assignment.runner,
85
+ },
86
+ });
87
+ decisions.push({
88
+ task_id: task.task_id,
89
+ queued: true,
90
+ reason: `queued for ${assignment.role} via ${assignment.runner}`,
91
+ });
92
+ }
93
+ return decisions;
94
+ }