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
|
@@ -1,10 +1,21 @@
|
|
|
1
|
+
import { JSON_CONTRACT_VERSION } from '../lib/output.js';
|
|
1
2
|
export function getCapabilitiesManifest(version) {
|
|
2
3
|
return {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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: '
|
|
108
|
-
description: 'Create
|
|
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: ['<
|
|
111
|
-
options: ['--
|
|
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: '
|
|
115
|
-
description: '
|
|
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: ['--
|
|
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
|
|
287
|
+
description: 'List run records',
|
|
123
288
|
json_output: true,
|
|
124
289
|
arguments: [],
|
|
125
|
-
options: ['--
|
|
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
|
+
}
|