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
|
@@ -9,8 +9,8 @@ export function compileTaskPrompt(input) {
|
|
|
9
9
|
runner_type: input.runner,
|
|
10
10
|
agent_role: input.agentRole,
|
|
11
11
|
status: 'preparing',
|
|
12
|
-
branch:
|
|
13
|
-
worktree:
|
|
12
|
+
branch: '',
|
|
13
|
+
worktree: '',
|
|
14
14
|
...(input.parentRunId ? { parent_run_id: input.parentRunId } : {}),
|
|
15
15
|
...(input.replayReason ? { replay_reason: input.replayReason } : {}),
|
|
16
16
|
...(input.pipelineId ? { pipeline_id: input.pipelineId } : {}),
|
|
@@ -173,31 +173,20 @@ export function getCapabilitiesManifest(version) {
|
|
|
173
173
|
subcommands: [
|
|
174
174
|
{
|
|
175
175
|
name: 'create',
|
|
176
|
-
description: 'Create
|
|
176
|
+
description: 'Create an agent-ready queue task',
|
|
177
177
|
json_output: true,
|
|
178
178
|
arguments: [],
|
|
179
179
|
options: [
|
|
180
180
|
'--title <title>',
|
|
181
181
|
'--summary <summary>',
|
|
182
|
-
'--
|
|
183
|
-
'--type feature|bug|chore|research',
|
|
184
|
-
'--status draft|ready|queued|running|review|blocked|done',
|
|
182
|
+
'--status active|blocked|done',
|
|
185
183
|
'--priority low|medium|high',
|
|
186
|
-
'--
|
|
187
|
-
'--
|
|
188
|
-
'--
|
|
189
|
-
'--
|
|
190
|
-
'--
|
|
191
|
-
'--
|
|
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>',
|
|
184
|
+
'--trigger <condition>',
|
|
185
|
+
'--trigger-check <command>',
|
|
186
|
+
'--depends-on <task-ids>',
|
|
187
|
+
'--entry-points <paths>',
|
|
188
|
+
'--done-condition <text>',
|
|
189
|
+
'--validate-cmd <command>',
|
|
201
190
|
'--json',
|
|
202
191
|
],
|
|
203
192
|
},
|
|
@@ -213,7 +202,7 @@ export function getCapabilitiesManifest(version) {
|
|
|
213
202
|
description: 'List task packets',
|
|
214
203
|
json_output: true,
|
|
215
204
|
arguments: [],
|
|
216
|
-
options: ['--status
|
|
205
|
+
options: ['--status active|blocked|done|all', '--json'],
|
|
217
206
|
},
|
|
218
207
|
{
|
|
219
208
|
name: 'assign',
|
|
@@ -222,6 +211,13 @@ export function getCapabilitiesManifest(version) {
|
|
|
222
211
|
arguments: ['<task-id>'],
|
|
223
212
|
options: ['--agent-role planner|builder-ui|builder-app|reviewer|tester', '--runner codex|claude', '--json'],
|
|
224
213
|
},
|
|
214
|
+
{
|
|
215
|
+
name: 'set-status',
|
|
216
|
+
description: 'Transition task packet status with validation',
|
|
217
|
+
json_output: true,
|
|
218
|
+
arguments: ['<task-id>'],
|
|
219
|
+
options: ['--to active|blocked|done', '--reason <text>', '--by human|agent', '--session <id>', '--json'],
|
|
220
|
+
},
|
|
225
221
|
{
|
|
226
222
|
name: 'create-followup',
|
|
227
223
|
description: 'Create follow-up task from run report',
|
|
@@ -256,14 +252,14 @@ export function getCapabilitiesManifest(version) {
|
|
|
256
252
|
subcommands: [
|
|
257
253
|
{
|
|
258
254
|
name: 'queue',
|
|
259
|
-
description: '
|
|
255
|
+
description: 'Evaluate active tasks and mark dependency-blocked tasks',
|
|
260
256
|
json_output: true,
|
|
261
257
|
arguments: [],
|
|
262
258
|
options: ['--json'],
|
|
263
259
|
},
|
|
264
260
|
{
|
|
265
261
|
name: 'start-ready',
|
|
266
|
-
description: '
|
|
262
|
+
description: 'Evaluate active tasks and run the ones with satisfied triggers',
|
|
267
263
|
json_output: true,
|
|
268
264
|
arguments: [],
|
|
269
265
|
options: ['--dry-run', '--json'],
|
|
@@ -44,9 +44,9 @@ export async function runTaskExecution(input) {
|
|
|
44
44
|
const prefs = loadRunnerPreferences(input.rootPath);
|
|
45
45
|
const dryRun = Boolean(input.dryRun);
|
|
46
46
|
const task = getTaskPacket(input.rootPath, input.taskId);
|
|
47
|
-
const runner = input.runner ??
|
|
48
|
-
const agentRole = input.agentRole ??
|
|
49
|
-
const worktree =
|
|
47
|
+
const runner = input.runner ?? prefs.default_runner;
|
|
48
|
+
const agentRole = input.agentRole ?? prefs.default_agent_role;
|
|
49
|
+
const worktree = '';
|
|
50
50
|
let cwd;
|
|
51
51
|
if (worktree.length > 0) {
|
|
52
52
|
if (!fs.existsSync(worktree)) {
|
|
@@ -74,10 +74,10 @@ export async function runTaskExecution(input) {
|
|
|
74
74
|
}
|
|
75
75
|
const logPath = path.resolve(path.join(input.rootPath, '.sidecar', 'runs', 'logs', `${compiled.run_id}.log`));
|
|
76
76
|
const adapter = getRunnerAdapter(runner);
|
|
77
|
-
saveTaskPacket(input.rootPath, { ...task, status: '
|
|
77
|
+
saveTaskPacket(input.rootPath, { ...task, status: 'active' });
|
|
78
78
|
updateRunRecordEntry(input.rootPath, compiled.run_id, {
|
|
79
79
|
status: 'running',
|
|
80
|
-
branch:
|
|
80
|
+
branch: '',
|
|
81
81
|
worktree: cwd,
|
|
82
82
|
});
|
|
83
83
|
const prepared = adapter.prepare({
|
|
@@ -107,13 +107,8 @@ export async function runTaskExecution(input) {
|
|
|
107
107
|
changedFiles = await captureFilesChangedSince(cwd, preRunSnapshot);
|
|
108
108
|
}
|
|
109
109
|
if (!dryRun && collected.executed && ok) {
|
|
110
|
-
const
|
|
111
|
-
const steps = [];
|
|
112
|
-
for (const entry of configured) {
|
|
113
|
-
const normalized = normalizeValidationStep(entry);
|
|
114
|
-
if (normalized)
|
|
115
|
-
steps.push(normalized);
|
|
116
|
-
}
|
|
110
|
+
const normalized = normalizeValidationStep(task.validation_command);
|
|
111
|
+
const steps = normalized ? [normalized] : [];
|
|
117
112
|
if (steps.length > 0) {
|
|
118
113
|
validationAttempted = true;
|
|
119
114
|
const results = await runValidationCommands(cwd, steps, logPath);
|
|
@@ -130,7 +125,7 @@ export async function runTaskExecution(input) {
|
|
|
130
125
|
}
|
|
131
126
|
}
|
|
132
127
|
const finishedStatus = ok ? 'completed' : 'failed';
|
|
133
|
-
const nextTaskStatus = ok ? '
|
|
128
|
+
const nextTaskStatus = ok ? 'done' : 'blocked';
|
|
134
129
|
// Auto-approve on all-green is opt-in via preferences. We only auto-approve when at least
|
|
135
130
|
// one validation step actually ran and every step passed — a runner-only success with no
|
|
136
131
|
// validation configured still requires a human click.
|
|
@@ -4,14 +4,14 @@ import { getRunRecord, listRunRecords, updateRunRecordEntry } from '../runs/run-
|
|
|
4
4
|
import { createTaskPacketRecord, getTaskPacket, saveTaskPacket } from '../tasks/task-service.js';
|
|
5
5
|
function taskStatusForReview(state) {
|
|
6
6
|
if (state === 'approved')
|
|
7
|
-
return '
|
|
7
|
+
return 'active';
|
|
8
8
|
if (state === 'needs_changes')
|
|
9
|
-
return '
|
|
9
|
+
return 'active';
|
|
10
10
|
if (state === 'blocked')
|
|
11
11
|
return 'blocked';
|
|
12
12
|
if (state === 'merged')
|
|
13
13
|
return 'done';
|
|
14
|
-
return '
|
|
14
|
+
return 'active';
|
|
15
15
|
}
|
|
16
16
|
export function reviewRun(rootPath, runId, state, options) {
|
|
17
17
|
const run = getRunRecord(rootPath, runId);
|
|
@@ -38,22 +38,21 @@ export function createFollowupTaskFromRun(rootPath, runId) {
|
|
|
38
38
|
const run = getRunRecord(rootPath, runId);
|
|
39
39
|
const sourceTask = getTaskPacket(rootPath, run.task_id);
|
|
40
40
|
const suggestions = run.follow_ups.length > 0 ? run.follow_ups : ['Investigate run issues and apply required changes'];
|
|
41
|
+
const entryPoints = sourceTask.entry_points.length > 0
|
|
42
|
+
? sourceTask.entry_points.slice(0, 3)
|
|
43
|
+
: run.changed_files.slice(0, 3).length > 0
|
|
44
|
+
? run.changed_files.slice(0, 3)
|
|
45
|
+
: ['src/cli.ts'];
|
|
41
46
|
const created = createTaskPacketRecord(rootPath, {
|
|
42
47
|
title: `Follow-up: ${sourceTask.title}`,
|
|
43
48
|
summary: run.review_note || run.summary || 'Follow-up work from reviewed run',
|
|
44
|
-
|
|
45
|
-
type: sourceTask.type,
|
|
46
|
-
status: 'draft',
|
|
49
|
+
status: 'active',
|
|
47
50
|
priority: sourceTask.priority,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
technical_constraints: sourceTask.constraints.technical,
|
|
54
|
-
design_constraints: sourceTask.constraints.design,
|
|
55
|
-
validation_commands: sourceTask.execution.commands.validation.map((v) => ({ ...v })),
|
|
56
|
-
definition_of_done: [...sourceTask.definition_of_done, ...suggestions],
|
|
51
|
+
trigger_condition: `After ${sourceTask.task_id} is done and this follow-up is explicitly scheduled`,
|
|
52
|
+
trigger_depends_on: [sourceTask.task_id],
|
|
53
|
+
entry_points: entryPoints,
|
|
54
|
+
done_condition: suggestions.join('; '),
|
|
55
|
+
validation_command: sourceTask.validation_command,
|
|
57
56
|
});
|
|
58
57
|
return {
|
|
59
58
|
source_run_id: run.run_id,
|
|
@@ -1,24 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { getTaskPacket, listTaskPackets, saveTaskPacket } from '../tasks/task-service.js';
|
|
3
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();
|
|
4
|
+
const joined = [task.title, task.summary, ...task.entry_points].join(' ').toLowerCase();
|
|
12
5
|
return /(ui|frontend|css|html|react|view|component)/.test(joined);
|
|
13
6
|
}
|
|
14
7
|
function pickRole(task) {
|
|
15
|
-
if (task.
|
|
16
|
-
return { role: '
|
|
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' };
|
|
8
|
+
if (/test|qa|verification|validate/i.test(task.title) || /test|qa|verification|validate/i.test(task.summary)) {
|
|
9
|
+
return { role: 'tester', reason: 'testing signal in title/summary' };
|
|
19
10
|
}
|
|
20
|
-
if (task.
|
|
21
|
-
return { role: 'reviewer', reason: '
|
|
11
|
+
if (/review|audit|risk|regression/i.test(task.title) || /review|audit|risk|regression/i.test(task.summary)) {
|
|
12
|
+
return { role: 'reviewer', reason: 'review signal in title/summary' };
|
|
22
13
|
}
|
|
23
14
|
if (hasUiSignal(task))
|
|
24
15
|
return { role: 'builder-ui', reason: 'ui/frontend signal detected' };
|
|
@@ -30,26 +21,43 @@ function defaultRunnerForRole(role) {
|
|
|
30
21
|
return 'codex';
|
|
31
22
|
}
|
|
32
23
|
export function dependenciesMet(task, tasksById) {
|
|
33
|
-
const
|
|
24
|
+
const deps = task.trigger.depends_on ?? [];
|
|
25
|
+
const missing = deps.filter((depId) => tasksById.get(depId)?.status !== 'done');
|
|
34
26
|
return { ok: missing.length === 0, missing };
|
|
35
27
|
}
|
|
28
|
+
function triggerCommandSatisfied(rootPath, command) {
|
|
29
|
+
const proc = spawnSync(command, {
|
|
30
|
+
cwd: rootPath,
|
|
31
|
+
shell: true,
|
|
32
|
+
stdio: 'pipe',
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
});
|
|
35
|
+
const ok = proc.status === 0;
|
|
36
|
+
const stderr = (proc.stderr || '').trim();
|
|
37
|
+
const stdout = (proc.stdout || '').trim();
|
|
38
|
+
if (ok)
|
|
39
|
+
return { ok: true, reason: `trigger check passed: ${command}` };
|
|
40
|
+
const details = stderr || stdout || `exit ${String(proc.status ?? '1')}`;
|
|
41
|
+
return { ok: false, reason: `trigger check failed: ${command} (${details})` };
|
|
42
|
+
}
|
|
43
|
+
export function evaluateTrigger(rootPath, task, tasksById) {
|
|
44
|
+
const dep = dependenciesMet(task, tasksById);
|
|
45
|
+
if (!dep.ok)
|
|
46
|
+
return { ok: false, reason: `dependencies not done: ${dep.missing.join(', ')}` };
|
|
47
|
+
const checkCommand = task.trigger.check_command?.trim();
|
|
48
|
+
if (checkCommand)
|
|
49
|
+
return triggerCommandSatisfied(rootPath, checkCommand);
|
|
50
|
+
if (task.trigger.depends_on.length > 0) {
|
|
51
|
+
return { ok: true, reason: 'dependency trigger satisfied' };
|
|
52
|
+
}
|
|
53
|
+
return { ok: false, reason: 'trigger requires user confirmation (no check command)' };
|
|
54
|
+
}
|
|
36
55
|
export function assignTask(rootPath, taskId, override) {
|
|
37
56
|
const task = getTaskPacket(rootPath, taskId);
|
|
38
57
|
const auto = pickRole(task);
|
|
39
58
|
const role = override?.role ?? auto.role;
|
|
40
59
|
const runner = override?.runner ?? defaultRunnerForRole(role);
|
|
41
60
|
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
61
|
return { task_id: task.task_id, agent_role: role, runner, reason };
|
|
54
62
|
}
|
|
55
63
|
export function queueReadyTasks(rootPath) {
|
|
@@ -57,38 +65,17 @@ export function queueReadyTasks(rootPath) {
|
|
|
57
65
|
const byId = new Map(tasks.map((t) => [t.task_id, t]));
|
|
58
66
|
const decisions = [];
|
|
59
67
|
for (const task of tasks) {
|
|
60
|
-
if (task.status !== '
|
|
68
|
+
if (task.status !== 'active')
|
|
61
69
|
continue;
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
const trigger = evaluateTrigger(rootPath, task, byId);
|
|
71
|
+
if (!trigger.ok) {
|
|
72
|
+
if (task.trigger.depends_on.length > 0) {
|
|
73
|
+
saveTaskPacket(rootPath, { ...task, status: 'blocked' });
|
|
74
|
+
}
|
|
75
|
+
decisions.push({ task_id: task.task_id, queued: false, reason: trigger.reason });
|
|
66
76
|
continue;
|
|
67
77
|
}
|
|
68
|
-
|
|
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
|
-
});
|
|
78
|
+
decisions.push({ task_id: task.task_id, queued: true, reason: trigger.reason });
|
|
92
79
|
}
|
|
93
80
|
return decisions;
|
|
94
81
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SidecarError } from '../lib/errors.js';
|
|
2
|
+
import { getTaskPacket, saveTaskPacket } from '../tasks/task-service.js';
|
|
3
|
+
const TASK_STATUS_TRANSITIONS = {
|
|
4
|
+
active: ['blocked', 'done'],
|
|
5
|
+
blocked: ['active', 'done'],
|
|
6
|
+
done: ['active'],
|
|
7
|
+
};
|
|
8
|
+
export function allowedTaskStatusTransitions(fromStatus) {
|
|
9
|
+
return TASK_STATUS_TRANSITIONS[fromStatus];
|
|
10
|
+
}
|
|
11
|
+
export function transitionTaskStatus(rootPath, taskId, toStatus) {
|
|
12
|
+
const task = getTaskPacket(rootPath, taskId);
|
|
13
|
+
const fromStatus = task.status;
|
|
14
|
+
if (fromStatus === toStatus) {
|
|
15
|
+
throw new SidecarError(`Task ${task.task_id} is already '${toStatus}'`);
|
|
16
|
+
}
|
|
17
|
+
const allowed = allowedTaskStatusTransitions(fromStatus);
|
|
18
|
+
if (!allowed.includes(toStatus)) {
|
|
19
|
+
throw new SidecarError(`Invalid status transition for ${task.task_id}: ${fromStatus} -> ${toStatus}. Allowed: ${allowed.join(', ')}`);
|
|
20
|
+
}
|
|
21
|
+
const updated = { ...task, status: toStatus };
|
|
22
|
+
const filePath = saveTaskPacket(rootPath, updated);
|
|
23
|
+
return {
|
|
24
|
+
task_id: task.task_id,
|
|
25
|
+
from_status: fromStatus,
|
|
26
|
+
to_status: toStatus,
|
|
27
|
+
path: filePath,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -1,148 +1,179 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
2
|
+
import { nowIso } from '../lib/format.js';
|
|
3
|
+
export const TASK_PACKET_VERSION = '2.0';
|
|
3
4
|
const taskIdSchema = z.string().regex(/^T-\d{3,}$/, 'Task id must look like T-001');
|
|
4
|
-
export const taskPacketStatusSchema = z.enum(['
|
|
5
|
+
export const taskPacketStatusSchema = z.enum(['active', 'blocked', 'done']);
|
|
5
6
|
export const taskPacketPrioritySchema = z.enum(['low', 'medium', 'high']);
|
|
6
|
-
export const
|
|
7
|
-
export const taskAgentRoleSchema = z.enum(['planner', 'builder-ui', 'builder-app', 'reviewer', 'tester']);
|
|
8
|
-
export const taskRunnerSchema = z.enum(['codex', 'claude']);
|
|
9
|
-
export const validationKindSchema = z.enum(['typecheck', 'lint', 'test', 'build', 'custom']);
|
|
10
|
-
// Accept string entries ("npm test") or object entries ({kind,command,...}). String entries
|
|
11
|
-
// are promoted to { kind: 'custom', command }. This preserves the v1 packet shape while
|
|
12
|
-
// giving new packets first-class typed validation steps.
|
|
13
|
-
export const validationStepSchema = z.preprocess((raw) => {
|
|
14
|
-
if (typeof raw === 'string') {
|
|
15
|
-
return { kind: 'custom', command: raw };
|
|
16
|
-
}
|
|
17
|
-
return raw;
|
|
18
|
-
}, z
|
|
7
|
+
export const taskTriggerSchema = z
|
|
19
8
|
.object({
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
timeout_ms: z.number().int().positive().optional(),
|
|
9
|
+
condition: z.string().min(1, 'trigger condition is required'),
|
|
10
|
+
check_command: z.string().min(1).optional(),
|
|
11
|
+
depends_on: z.array(taskIdSchema).default([]),
|
|
24
12
|
})
|
|
25
|
-
.strict()
|
|
26
|
-
export const
|
|
13
|
+
.strict();
|
|
14
|
+
export const taskResultSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
summary: z.string().default(''),
|
|
17
|
+
changed_files: z.array(z.string()).default([]),
|
|
18
|
+
validation_output: z.string().default(''),
|
|
19
|
+
validated_at: z.string().datetime({ offset: true }).nullable().default(null),
|
|
20
|
+
})
|
|
21
|
+
.strict();
|
|
22
|
+
function normalizeLegacyStatus(value) {
|
|
23
|
+
if (value === 'open')
|
|
24
|
+
return 'active';
|
|
25
|
+
if (value === 'in_progress')
|
|
26
|
+
return 'active';
|
|
27
|
+
if (value === 'draft' || value === 'ready' || value === 'queued' || value === 'running' || value === 'review')
|
|
28
|
+
return 'active';
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function normalizeLegacyPacket(raw) {
|
|
32
|
+
if (!raw || typeof raw !== 'object')
|
|
33
|
+
return raw;
|
|
34
|
+
const packet = raw;
|
|
35
|
+
const entryFromLegacy = (() => {
|
|
36
|
+
const implementation = packet.implementation;
|
|
37
|
+
const files = implementation?.files_to_read;
|
|
38
|
+
if (Array.isArray(files)) {
|
|
39
|
+
return files
|
|
40
|
+
.map((v) => (typeof v === 'string' ? v.trim() : ''))
|
|
41
|
+
.filter((v) => v.length > 0)
|
|
42
|
+
.slice(0, 3);
|
|
43
|
+
}
|
|
44
|
+
return [];
|
|
45
|
+
})();
|
|
46
|
+
const doneFromLegacy = (() => {
|
|
47
|
+
const dod = packet.definition_of_done;
|
|
48
|
+
if (Array.isArray(dod) && dod.length > 0) {
|
|
49
|
+
const first = dod.find((v) => typeof v === 'string' && v.trim().length > 0);
|
|
50
|
+
if (typeof first === 'string')
|
|
51
|
+
return first.trim();
|
|
52
|
+
}
|
|
53
|
+
const goal = typeof packet.goal === 'string' ? packet.goal.trim() : '';
|
|
54
|
+
return goal;
|
|
55
|
+
})();
|
|
56
|
+
const validationFromLegacy = (() => {
|
|
57
|
+
const execution = packet.execution;
|
|
58
|
+
const commands = execution?.commands;
|
|
59
|
+
const validation = commands?.validation;
|
|
60
|
+
if (Array.isArray(validation) && validation.length > 0) {
|
|
61
|
+
const first = validation[0];
|
|
62
|
+
if (typeof first === 'string')
|
|
63
|
+
return first.trim();
|
|
64
|
+
if (first && typeof first === 'object') {
|
|
65
|
+
const command = first.command;
|
|
66
|
+
if (typeof command === 'string')
|
|
67
|
+
return command.trim();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return '';
|
|
71
|
+
})();
|
|
72
|
+
const dependenciesFromLegacy = (() => {
|
|
73
|
+
const deps = packet.dependencies;
|
|
74
|
+
if (Array.isArray(deps)) {
|
|
75
|
+
return deps.filter((v) => typeof v === 'string' && /^T-\d{3,}$/.test(v));
|
|
76
|
+
}
|
|
77
|
+
return [];
|
|
78
|
+
})();
|
|
79
|
+
const baseSummary = typeof packet.summary === 'string' ? packet.summary.trim() : '';
|
|
80
|
+
const baseGoal = typeof packet.goal === 'string' ? packet.goal.trim() : '';
|
|
81
|
+
const baseTitle = typeof packet.title === 'string' ? packet.title.trim() : '';
|
|
82
|
+
const resultLegacy = (() => {
|
|
83
|
+
const result = packet.result;
|
|
84
|
+
if (!result || typeof result !== 'object')
|
|
85
|
+
return undefined;
|
|
86
|
+
const validationResults = Array.isArray(result.validation_results)
|
|
87
|
+
? result.validation_results.filter((v) => typeof v === 'string')
|
|
88
|
+
: [];
|
|
89
|
+
return {
|
|
90
|
+
summary: typeof result.summary === 'string' ? result.summary : '',
|
|
91
|
+
changed_files: Array.isArray(result.changed_files)
|
|
92
|
+
? result.changed_files.filter((v) => typeof v === 'string')
|
|
93
|
+
: [],
|
|
94
|
+
validation_output: validationResults.join('\n'),
|
|
95
|
+
validated_at: null,
|
|
96
|
+
};
|
|
97
|
+
})();
|
|
98
|
+
return {
|
|
99
|
+
version: typeof packet.version === 'string' ? packet.version : TASK_PACKET_VERSION,
|
|
100
|
+
task_id: packet.task_id,
|
|
101
|
+
title: baseTitle,
|
|
102
|
+
summary: baseSummary || baseGoal || baseTitle,
|
|
103
|
+
priority: packet.priority,
|
|
104
|
+
status: normalizeLegacyStatus(packet.status),
|
|
105
|
+
created_at: packet.created_at,
|
|
106
|
+
updated_at: packet.updated_at,
|
|
107
|
+
trigger: packet.trigger ?? {
|
|
108
|
+
condition: dependenciesFromLegacy.length > 0
|
|
109
|
+
? `After dependencies are done: ${dependenciesFromLegacy.join(', ')}`
|
|
110
|
+
: 'Set explicit trigger before execution.',
|
|
111
|
+
depends_on: dependenciesFromLegacy,
|
|
112
|
+
},
|
|
113
|
+
entry_points: packet.entry_points ?? entryFromLegacy,
|
|
114
|
+
done_condition: packet.done_condition ?? doneFromLegacy,
|
|
115
|
+
validation_command: packet.validation_command ?? validationFromLegacy,
|
|
116
|
+
...(resultLegacy ? { result: resultLegacy } : {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const taskPacketShapeSchema = z
|
|
27
120
|
.object({
|
|
28
121
|
version: z.string().default(TASK_PACKET_VERSION),
|
|
29
122
|
task_id: taskIdSchema,
|
|
30
123
|
title: z.string().min(1, 'title is required'),
|
|
31
|
-
type: taskPacketTypeSchema.default('chore'),
|
|
32
|
-
status: z
|
|
33
|
-
.preprocess((value) => {
|
|
34
|
-
if (value === 'open')
|
|
35
|
-
return 'draft';
|
|
36
|
-
if (value === 'in_progress')
|
|
37
|
-
return 'running';
|
|
38
|
-
return value;
|
|
39
|
-
}, taskPacketStatusSchema)
|
|
40
|
-
.default('draft'),
|
|
41
|
-
priority: taskPacketPrioritySchema.default('medium'),
|
|
42
124
|
summary: z.string().min(1, 'summary is required'),
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}),
|
|
60
|
-
execution: z.object({
|
|
61
|
-
commands: z.object({
|
|
62
|
-
validation: z.array(validationStepSchema).default([]),
|
|
63
|
-
}),
|
|
64
|
-
}),
|
|
65
|
-
dependencies: z.array(taskIdSchema).default([]),
|
|
66
|
-
tags: z.array(z.string()).default([]),
|
|
67
|
-
target_areas: z.array(z.string()).default([]),
|
|
68
|
-
definition_of_done: z.array(z.string()).default([]),
|
|
69
|
-
tracking: z.object({
|
|
70
|
-
branch: z.string().default(''),
|
|
71
|
-
worktree: z.string().default(''),
|
|
72
|
-
assigned_agent_role: taskAgentRoleSchema.nullable().default(null),
|
|
73
|
-
assigned_runner: taskRunnerSchema.nullable().default(null),
|
|
74
|
-
assignment_reason: z.string().default(''),
|
|
75
|
-
assigned_at: z.string().datetime({ offset: true }).nullable().default(null),
|
|
76
|
-
}),
|
|
77
|
-
result: z.object({
|
|
78
|
-
summary: z.string().default(''),
|
|
79
|
-
changed_files: z.array(z.string()).default([]),
|
|
80
|
-
validation_results: z.array(z.string()).default([]),
|
|
125
|
+
priority: taskPacketPrioritySchema.default('medium'),
|
|
126
|
+
status: z.preprocess(normalizeLegacyStatus, taskPacketStatusSchema).default('active'),
|
|
127
|
+
created_at: z.string().datetime({ offset: true }).default(() => nowIso()),
|
|
128
|
+
updated_at: z.string().datetime({ offset: true }).default(() => nowIso()),
|
|
129
|
+
trigger: taskTriggerSchema,
|
|
130
|
+
entry_points: z
|
|
131
|
+
.array(z.string().min(1))
|
|
132
|
+
.min(1, 'at least one entry point is required')
|
|
133
|
+
.max(3, 'entry points must be 1-3 files'),
|
|
134
|
+
done_condition: z.string().min(1, 'done condition is required'),
|
|
135
|
+
validation_command: z.string().min(1, 'validation command is required'),
|
|
136
|
+
result: taskResultSchema.default({
|
|
137
|
+
summary: '',
|
|
138
|
+
changed_files: [],
|
|
139
|
+
validation_output: '',
|
|
140
|
+
validated_at: null,
|
|
81
141
|
}),
|
|
82
142
|
})
|
|
83
143
|
.strict();
|
|
84
|
-
export const
|
|
144
|
+
export const taskPacketSchema = z.preprocess(normalizeLegacyPacket, taskPacketShapeSchema);
|
|
145
|
+
export const taskPacketInputSchema = taskPacketShapeSchema.omit({ task_id: true }).partial({
|
|
85
146
|
version: true,
|
|
86
|
-
type: true,
|
|
87
|
-
status: true,
|
|
88
147
|
priority: true,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
constraints: true,
|
|
93
|
-
execution: true,
|
|
94
|
-
dependencies: true,
|
|
95
|
-
tags: true,
|
|
96
|
-
target_areas: true,
|
|
97
|
-
definition_of_done: true,
|
|
98
|
-
tracking: true,
|
|
148
|
+
status: true,
|
|
149
|
+
created_at: true,
|
|
150
|
+
updated_at: true,
|
|
99
151
|
result: true,
|
|
100
152
|
});
|
|
101
153
|
export function createTaskPacket(taskId, input) {
|
|
154
|
+
const now = nowIso();
|
|
102
155
|
const normalized = {
|
|
103
|
-
...input,
|
|
104
156
|
version: input.version ?? TASK_PACKET_VERSION,
|
|
105
157
|
task_id: taskId,
|
|
106
|
-
|
|
107
|
-
|
|
158
|
+
title: input.title,
|
|
159
|
+
summary: input.summary,
|
|
108
160
|
priority: input.priority ?? 'medium',
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
},
|
|
117
|
-
implementation: {
|
|
118
|
-
files_to_read: input.implementation?.files_to_read ?? [],
|
|
119
|
-
files_to_avoid: input.implementation?.files_to_avoid ?? [],
|
|
120
|
-
},
|
|
121
|
-
constraints: {
|
|
122
|
-
technical: input.constraints?.technical ?? [],
|
|
123
|
-
design: input.constraints?.design ?? [],
|
|
124
|
-
},
|
|
125
|
-
execution: {
|
|
126
|
-
commands: {
|
|
127
|
-
validation: input.execution?.commands?.validation ?? [],
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
dependencies: input.dependencies ?? [],
|
|
131
|
-
tags: input.tags ?? [],
|
|
132
|
-
target_areas: input.target_areas ?? [],
|
|
133
|
-
definition_of_done: input.definition_of_done ?? [],
|
|
134
|
-
tracking: {
|
|
135
|
-
branch: input.tracking?.branch ?? '',
|
|
136
|
-
worktree: input.tracking?.worktree ?? '',
|
|
137
|
-
assigned_agent_role: input.tracking?.assigned_agent_role ?? null,
|
|
138
|
-
assigned_runner: input.tracking?.assigned_runner ?? null,
|
|
139
|
-
assignment_reason: input.tracking?.assignment_reason ?? '',
|
|
140
|
-
assigned_at: input.tracking?.assigned_at ?? null,
|
|
161
|
+
status: input.status ?? 'active',
|
|
162
|
+
created_at: input.created_at ?? now,
|
|
163
|
+
updated_at: input.updated_at ?? now,
|
|
164
|
+
trigger: {
|
|
165
|
+
condition: input.trigger?.condition ?? '',
|
|
166
|
+
...(input.trigger?.check_command ? { check_command: input.trigger.check_command } : {}),
|
|
167
|
+
depends_on: input.trigger?.depends_on ?? [],
|
|
141
168
|
},
|
|
169
|
+
entry_points: input.entry_points ?? [],
|
|
170
|
+
done_condition: input.done_condition ?? '',
|
|
171
|
+
validation_command: input.validation_command ?? '',
|
|
142
172
|
result: {
|
|
143
173
|
summary: input.result?.summary ?? '',
|
|
144
174
|
changed_files: input.result?.changed_files ?? [],
|
|
145
|
-
|
|
175
|
+
validation_output: input.result?.validation_output ?? '',
|
|
176
|
+
validated_at: input.result?.validated_at ?? null,
|
|
146
177
|
},
|
|
147
178
|
};
|
|
148
179
|
return taskPacketSchema.parse(normalized);
|