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.
@@ -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: task.tracking.branch,
13
- worktree: task.tracking.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 a structured task packet',
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
- '--goal <goal>',
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
- '--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>',
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 draft|ready|queued|running|review|blocked|done|all', '--json'],
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: 'Queue ready tasks with satisfied dependencies',
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: 'Queue and start all runnable ready tasks',
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 ?? task.tracking.assigned_runner ?? prefs.default_runner;
48
- const agentRole = input.agentRole ?? task.tracking.assigned_agent_role ?? prefs.default_agent_role;
49
- const worktree = (task.tracking.worktree ?? '').trim();
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: 'running' });
77
+ saveTaskPacket(input.rootPath, { ...task, status: 'active' });
78
78
  updateRunRecordEntry(input.rootPath, compiled.run_id, {
79
79
  status: 'running',
80
- branch: task.tracking.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 configured = task.execution?.commands?.validation ?? [];
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 ? 'review' : 'blocked';
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 'review';
7
+ return 'active';
8
8
  if (state === 'needs_changes')
9
- return 'ready';
9
+ return 'active';
10
10
  if (state === 'blocked')
11
11
  return 'blocked';
12
12
  if (state === 'merged')
13
13
  return 'done';
14
- return 'review';
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
- goal: suggestions.join('; '),
45
- type: sourceTask.type,
46
- status: 'draft',
49
+ status: 'active',
47
50
  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.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 { nowIso } from '../lib/format.js';
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.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' };
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.tags.some((t) => /review/i.test(t)) || task.type === 'bug') {
21
- return { role: 'reviewer', reason: 'bug/review signal present' };
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 missing = task.dependencies.filter((depId) => tasksById.get(depId)?.status !== 'done');
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 !== 'ready')
68
+ if (task.status !== 'active')
61
69
  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(', ')}` });
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
- 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
- });
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
- export const TASK_PACKET_VERSION = '1.0';
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(['draft', 'ready', 'queued', 'running', 'review', 'blocked', 'done']);
5
+ export const taskPacketStatusSchema = z.enum(['active', 'blocked', 'done']);
5
6
  export const taskPacketPrioritySchema = z.enum(['low', 'medium', 'high']);
6
- export const taskPacketTypeSchema = z.enum(['feature', 'bug', 'chore', 'research']);
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
- kind: validationKindSchema.default('custom'),
21
- command: z.string().min(1, 'validation command is required'),
22
- name: z.string().optional(),
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 taskPacketSchema = z
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
- goal: z.string().min(1, 'goal is required'),
44
- scope: z.object({
45
- in_scope: z.array(z.string()).default([]),
46
- out_of_scope: z.array(z.string()).default([]),
47
- }),
48
- context: z.object({
49
- related_decisions: z.array(z.string()).default([]),
50
- related_notes: z.array(z.string()).default([]),
51
- }),
52
- implementation: z.object({
53
- files_to_read: z.array(z.string()).default([]),
54
- files_to_avoid: z.array(z.string()).default([]),
55
- }),
56
- constraints: z.object({
57
- technical: z.array(z.string()).default([]),
58
- design: z.array(z.string()).default([]),
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 taskPacketInputSchema = taskPacketSchema.omit({ task_id: true }).partial({
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
- scope: true,
90
- context: true,
91
- implementation: true,
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
- type: input.type ?? 'chore',
107
- status: input.status ?? 'draft',
158
+ title: input.title,
159
+ summary: input.summary,
108
160
  priority: input.priority ?? 'medium',
109
- scope: {
110
- in_scope: input.scope?.in_scope ?? [],
111
- out_of_scope: input.scope?.out_of_scope ?? [],
112
- },
113
- context: {
114
- related_decisions: input.context?.related_decisions ?? [],
115
- related_notes: input.context?.related_notes ?? [],
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
- validation_results: input.result?.validation_results ?? [],
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);