zigrix 0.1.0-alpha.0

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.
Files changed (58) hide show
  1. package/LICENSE +184 -0
  2. package/README.md +128 -0
  3. package/dist/agents/registry.d.ts +28 -0
  4. package/dist/agents/registry.js +98 -0
  5. package/dist/config/defaults.d.ts +66 -0
  6. package/dist/config/defaults.js +58 -0
  7. package/dist/config/load.d.ts +13 -0
  8. package/dist/config/load.js +96 -0
  9. package/dist/config/mutate.d.ts +10 -0
  10. package/dist/config/mutate.js +61 -0
  11. package/dist/config/schema.d.ts +132 -0
  12. package/dist/config/schema.js +123 -0
  13. package/dist/configure.d.ts +24 -0
  14. package/dist/configure.js +164 -0
  15. package/dist/doctor.d.ts +4 -0
  16. package/dist/doctor.js +99 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +791 -0
  19. package/dist/onboard.d.ts +99 -0
  20. package/dist/onboard.js +490 -0
  21. package/dist/orchestration/dispatch.d.ts +9 -0
  22. package/dist/orchestration/dispatch.js +146 -0
  23. package/dist/orchestration/evidence.d.ts +19 -0
  24. package/dist/orchestration/evidence.js +97 -0
  25. package/dist/orchestration/finalize.d.ts +7 -0
  26. package/dist/orchestration/finalize.js +136 -0
  27. package/dist/orchestration/pipeline.d.ts +11 -0
  28. package/dist/orchestration/pipeline.js +26 -0
  29. package/dist/orchestration/report.d.ts +5 -0
  30. package/dist/orchestration/report.js +92 -0
  31. package/dist/orchestration/worker.d.ts +34 -0
  32. package/dist/orchestration/worker.js +132 -0
  33. package/dist/rules/templates.d.ts +24 -0
  34. package/dist/rules/templates.js +73 -0
  35. package/dist/runner/run.d.ts +10 -0
  36. package/dist/runner/run.js +91 -0
  37. package/dist/runner/schema.d.ts +33 -0
  38. package/dist/runner/schema.js +10 -0
  39. package/dist/runner/store.d.ts +5 -0
  40. package/dist/runner/store.js +19 -0
  41. package/dist/state/events.d.ts +3 -0
  42. package/dist/state/events.js +53 -0
  43. package/dist/state/paths.d.ts +15 -0
  44. package/dist/state/paths.js +23 -0
  45. package/dist/state/tasks.d.ts +61 -0
  46. package/dist/state/tasks.js +247 -0
  47. package/dist/state/verify.d.ts +2 -0
  48. package/dist/state/verify.js +65 -0
  49. package/examples/hello-workflow.json +13 -0
  50. package/package.json +44 -0
  51. package/skills/zigrix-doctor/SKILL.md +20 -0
  52. package/skills/zigrix-evidence/SKILL.md +21 -0
  53. package/skills/zigrix-init/SKILL.md +23 -0
  54. package/skills/zigrix-report/SKILL.md +20 -0
  55. package/skills/zigrix-shared/SKILL.md +31 -0
  56. package/skills/zigrix-task-create/SKILL.md +34 -0
  57. package/skills/zigrix-task-status/SKILL.md +27 -0
  58. package/skills/zigrix-worker/SKILL.md +22 -0
package/dist/index.js ADDED
@@ -0,0 +1,791 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import { Command } from 'commander';
4
+ import { addAgent, excludeAgent, includeAgent, listAgents, removeAgent, setAgentEnabled, setAgentRole, } from './agents/registry.js';
5
+ import { runConfigure } from './configure.js';
6
+ import { diffValues, getValueAtPath, parseConfigInput, resetValueAtPath, setValueAtPath } from './config/mutate.js';
7
+ import { defaultConfig } from './config/defaults.js';
8
+ import { getConfigValue, loadConfig, writeConfigFile, writeDefaultConfig } from './config/load.js';
9
+ import { zigrixConfigJsonSchema } from './config/schema.js';
10
+ import { gatherDoctor, renderDoctorText } from './doctor.js';
11
+ import { runOnboard } from './onboard.js';
12
+ import { dispatchTask } from './orchestration/dispatch.js';
13
+ import { collectEvidence, mergeEvidence } from './orchestration/evidence.js';
14
+ import { finalizeTask } from './orchestration/finalize.js';
15
+ import { runPipeline } from './orchestration/pipeline.js';
16
+ import { renderReport } from './orchestration/report.js';
17
+ import { completeWorker, prepareWorker, registerWorker } from './orchestration/worker.js';
18
+ import { listRules, renderTemplate, validateRules } from './rules/templates.js';
19
+ import { runWorkflow, summarizeRun } from './runner/run.js';
20
+ import { loadRunRecord } from './runner/store.js';
21
+ import { ensureBaseState, resolvePaths } from './state/paths.js';
22
+ import { applyStalePolicy, createTask, findStaleTasks, listTaskEvents, listTasks, loadTask, rebuildIndex, recordTaskProgress, updateTaskStatus, } from './state/tasks.js';
23
+ import { verifyState } from './state/verify.js';
24
+ const STATUS_MAP = {
25
+ start: 'IN_PROGRESS',
26
+ report: 'REPORTED',
27
+ };
28
+ function printValue(value, json = false) {
29
+ if (json || typeof value !== 'string') {
30
+ console.log(JSON.stringify(value, null, 2));
31
+ return;
32
+ }
33
+ console.log(value);
34
+ }
35
+ function requireConfigPath(configPath, baseDir) {
36
+ if (!configPath) {
37
+ throw new Error(`zigrix config not found under ${baseDir}; run 'zigrix onboard' first`);
38
+ }
39
+ return configPath;
40
+ }
41
+ function persistAndPrintMutation(params) {
42
+ const targetPath = requireConfigPath(params.configPath, params.baseDir);
43
+ writeConfigFile(targetPath, params.nextConfig);
44
+ printValue({ ok: true, action: params.action, agentId: params.agentId, configPath: targetPath }, params.json);
45
+ }
46
+ function persistConfigMutation(params) {
47
+ const targetPath = requireConfigPath(params.configPath, params.baseDir);
48
+ writeConfigFile(targetPath, params.nextConfig);
49
+ printValue({ ok: true, action: params.action, path: params.path ?? null, configPath: targetPath }, params.json);
50
+ }
51
+ function requireYes(yes, action = 'perform this action') {
52
+ if (!yes) {
53
+ throw new Error(`refusing to ${action} without --yes`);
54
+ }
55
+ }
56
+ function loadRuntime(options) {
57
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
58
+ return { ...loaded, paths: resolvePaths(loaded.config) };
59
+ }
60
+ const program = new Command();
61
+ program
62
+ .name('zigrix')
63
+ .description('Zigrix — multi-project parallel task orchestration CLI')
64
+ .version('0.1.0-alpha.0');
65
+ const config = program.command('config').description('Inspect Zigrix config');
66
+ const agent = program.command('agent').description('Manage Zigrix agent registry and orchestration membership');
67
+ const rule = program.command('rule').description('Inspect and validate rule assets');
68
+ const template = program.command('template').description('Inspect and modify prompt templates');
69
+ const reset = program.command('reset').description('Restore default config sections or clean runtime state');
70
+ const state = program.command('state').description('Inspect and verify runtime state');
71
+ const task = program.command('task').description('Task operations');
72
+ const worker = program.command('worker').description('Worker lifecycle operations');
73
+ const evidence = program.command('evidence').description('Evidence collection and merge operations');
74
+ const report = program.command('report').description('User-facing reporting helpers');
75
+ const pipeline = program.command('pipeline').description('High-level orchestration helpers');
76
+ // ─── onboard ────────────────────────────────────────────────────────────────
77
+ program
78
+ .command('onboard')
79
+ .description('Set up Zigrix for first use (creates ~/.zigrix, seeds rules, registers agents)')
80
+ .option('--yes', 'non-interactive confirmation')
81
+ .option('--json', 'JSON output')
82
+ .option('--project-dir <path>', 'path to project directory containing orchestration/rules/')
83
+ .action(async (options) => {
84
+ const result = await runOnboard({
85
+ yes: Boolean(options.yes),
86
+ projectDir: options.projectDir,
87
+ silent: Boolean(options.json),
88
+ });
89
+ printValue(result, options.json ?? true);
90
+ });
91
+ // ─── configure ──────────────────────────────────────────────────────────────
92
+ program
93
+ .command('configure')
94
+ .description('Reconfigure agents, rules, PATH, skills, or workspace settings')
95
+ .option('--section <section>', 'reconfigure specific section (agents|rules|workspace|path|skills), repeatable', (value, prev = []) => [...prev, value], [])
96
+ .option('--projects-base-dir <path>', 'set projects base directory')
97
+ .option('--project-dir <path>', 'path to project directory containing orchestration/rules/')
98
+ .option('--yes', 'non-interactive confirmation')
99
+ .option('--json', 'JSON output')
100
+ .action(async (options) => {
101
+ const result = await runConfigure({
102
+ sections: options.section.length > 0 ? options.section : undefined,
103
+ yes: Boolean(options.yes),
104
+ projectDir: options.projectDir,
105
+ projectsBaseDir: options.projectsBaseDir,
106
+ silent: Boolean(options.json),
107
+ });
108
+ printValue(result, options.json ?? true);
109
+ });
110
+ // ─── init (deprecated) ─────────────────────────────────────────────────────
111
+ program
112
+ .command('init')
113
+ .description('[DEPRECATED] Use "zigrix onboard" instead')
114
+ .option('--yes', 'non-interactive confirmation')
115
+ .option('--json', 'JSON output')
116
+ .action((options) => {
117
+ console.error('⚠️ "zigrix init" is deprecated. Use "zigrix onboard" instead.');
118
+ const configPath = writeDefaultConfig(undefined, Boolean(options.yes));
119
+ const loaded = loadRuntime({ config: configPath });
120
+ ensureBaseState(loaded.paths);
121
+ rebuildIndex(loaded.paths);
122
+ printValue({ ok: true, path: configPath, deprecated: true, useInstead: 'zigrix onboard' }, options.json);
123
+ });
124
+ // ─── doctor ─────────────────────────────────────────────────────────────────
125
+ program
126
+ .command('doctor')
127
+ .description('Inspect environment, config, and runtime readiness')
128
+ .option('--config <path>')
129
+ .option('--base-dir <path>')
130
+ .option('--json')
131
+ .action((options) => {
132
+ const loaded = loadRuntime({ baseDir: options.baseDir, config: options.config });
133
+ const payload = gatherDoctor(loaded, loaded.paths);
134
+ if (options.json) {
135
+ printValue(payload, true);
136
+ return;
137
+ }
138
+ console.log(renderDoctorText(payload));
139
+ });
140
+ // ─── config ─────────────────────────────────────────────────────────────────
141
+ config
142
+ .command('validate')
143
+ .option('--config <path>', 'explicit config path')
144
+ .option('--base-dir <path>', 'Zigrix base directory override')
145
+ .option('--json', 'JSON output')
146
+ .action((options) => {
147
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
148
+ printValue({ ok: true, configPath: loaded.configPath, baseDir: loaded.baseDir }, options.json);
149
+ });
150
+ config
151
+ .command('get [path]')
152
+ .option('--config <path>', 'explicit config path')
153
+ .option('--base-dir <path>', 'Zigrix base directory override')
154
+ .option('--json', 'JSON output')
155
+ .action((dottedPath, options) => {
156
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
157
+ printValue(getConfigValue(loaded.config, dottedPath) ?? null, true);
158
+ });
159
+ config
160
+ .command('schema [path]')
161
+ .option('--json', 'JSON output')
162
+ .action((dottedPath, options) => {
163
+ const value = dottedPath
164
+ ? dottedPath.split('.').reduce((acc, key) => {
165
+ if (acc && typeof acc === 'object' && key in acc) {
166
+ return acc[key];
167
+ }
168
+ return null;
169
+ }, zigrixConfigJsonSchema)
170
+ : zigrixConfigJsonSchema;
171
+ printValue(value, options.json ?? true);
172
+ });
173
+ config
174
+ .command('set <path>')
175
+ .requiredOption('--value <jsonOrString>')
176
+ .option('--config <path>', 'explicit config path')
177
+ .option('--base-dir <path>', 'Zigrix base directory override')
178
+ .option('--json', 'JSON output')
179
+ .action((dottedPath, options) => {
180
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
181
+ const nextConfig = setValueAtPath(loaded.config, dottedPath, parseConfigInput(options.value));
182
+ persistConfigMutation({
183
+ configPath: loaded.configPath,
184
+ baseDir: loaded.baseDir,
185
+ nextConfig,
186
+ json: options.json,
187
+ action: 'config.set',
188
+ path: dottedPath,
189
+ });
190
+ });
191
+ config
192
+ .command('diff <path>')
193
+ .option('--config <path>', 'explicit config path')
194
+ .option('--base-dir <path>', 'Zigrix base directory override')
195
+ .option('--json', 'JSON output')
196
+ .action((dottedPath, options) => {
197
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
198
+ printValue(diffValues(getValueAtPath(loaded.config, dottedPath), getValueAtPath(defaultConfig, dottedPath)), true);
199
+ });
200
+ config
201
+ .command('reset')
202
+ .option('--path <path>', 'dotted config path to restore from defaults', 'all')
203
+ .option('--config <path>', 'explicit config path')
204
+ .option('--base-dir <path>', 'Zigrix base directory override')
205
+ .option('--yes', 'confirm destructive reset')
206
+ .option('--json', 'JSON output')
207
+ .action((options) => {
208
+ requireYes(options.yes, 'reset config');
209
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
210
+ const nextConfig = resetValueAtPath(loaded.config, options.path);
211
+ persistConfigMutation({
212
+ configPath: loaded.configPath,
213
+ baseDir: loaded.baseDir,
214
+ nextConfig,
215
+ json: options.json,
216
+ action: 'config.reset',
217
+ path: options.path,
218
+ });
219
+ });
220
+ // ─── agent ──────────────────────────────────────────────────────────────────
221
+ agent
222
+ .command('list')
223
+ .option('--config <path>')
224
+ .option('--base-dir <path>')
225
+ .option('--json')
226
+ .action((options) => printValue(listAgents(loadConfig({ baseDir: options.baseDir, configPath: options.config }).config), true));
227
+ agent
228
+ .command('add')
229
+ .requiredOption('--id <agentId>')
230
+ .requiredOption('--role <role>')
231
+ .requiredOption('--runtime <runtime>')
232
+ .option('--label <label>')
233
+ .option('--include')
234
+ .option('--disabled')
235
+ .option('--config <path>')
236
+ .option('--base-dir <path>')
237
+ .option('--json')
238
+ .action((options) => {
239
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
240
+ const result = addAgent(loaded.config, { id: options.id, role: options.role, runtime: options.runtime, label: options.label, enabled: !options.disabled, include: Boolean(options.include) });
241
+ persistAndPrintMutation({ configPath: loaded.configPath, baseDir: loaded.baseDir, nextConfig: result.config, json: options.json, action: 'agent.add', agentId: result.agentId });
242
+ });
243
+ agent
244
+ .command('remove <agentId>')
245
+ .option('--config <path>')
246
+ .option('--base-dir <path>')
247
+ .option('--json')
248
+ .action((agentId, options) => {
249
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
250
+ const result = removeAgent(loaded.config, agentId);
251
+ persistAndPrintMutation({ configPath: loaded.configPath, baseDir: loaded.baseDir, nextConfig: result.config, json: options.json, action: 'agent.remove', agentId: result.agentId });
252
+ });
253
+ agent
254
+ .command('include <agentId>')
255
+ .option('--config <path>')
256
+ .option('--base-dir <path>')
257
+ .option('--json')
258
+ .action((agentId, options) => {
259
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
260
+ const result = includeAgent(loaded.config, agentId);
261
+ persistAndPrintMutation({ configPath: loaded.configPath, baseDir: loaded.baseDir, nextConfig: result.config, json: options.json, action: 'agent.include', agentId: result.agentId });
262
+ });
263
+ agent
264
+ .command('exclude <agentId>')
265
+ .option('--config <path>')
266
+ .option('--base-dir <path>')
267
+ .option('--json')
268
+ .action((agentId, options) => {
269
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
270
+ const result = excludeAgent(loaded.config, agentId);
271
+ persistAndPrintMutation({ configPath: loaded.configPath, baseDir: loaded.baseDir, nextConfig: result.config, json: options.json, action: 'agent.exclude', agentId: result.agentId });
272
+ });
273
+ agent
274
+ .command('enable <agentId>')
275
+ .option('--config <path>')
276
+ .option('--base-dir <path>')
277
+ .option('--json')
278
+ .action((agentId, options) => {
279
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
280
+ const result = setAgentEnabled(loaded.config, agentId, true);
281
+ persistAndPrintMutation({ configPath: loaded.configPath, baseDir: loaded.baseDir, nextConfig: result.config, json: options.json, action: 'agent.enable', agentId: result.agentId });
282
+ });
283
+ agent
284
+ .command('disable <agentId>')
285
+ .option('--config <path>')
286
+ .option('--base-dir <path>')
287
+ .option('--json')
288
+ .action((agentId, options) => {
289
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
290
+ const result = setAgentEnabled(loaded.config, agentId, false);
291
+ persistAndPrintMutation({ configPath: loaded.configPath, baseDir: loaded.baseDir, nextConfig: result.config, json: options.json, action: 'agent.disable', agentId: result.agentId });
292
+ });
293
+ agent
294
+ .command('set-role <agentId>')
295
+ .requiredOption('--role <role>')
296
+ .option('--config <path>')
297
+ .option('--base-dir <path>')
298
+ .option('--json')
299
+ .action((agentId, options) => {
300
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
301
+ const result = setAgentRole(loaded.config, agentId, options.role);
302
+ persistAndPrintMutation({ configPath: loaded.configPath, baseDir: loaded.baseDir, nextConfig: result.config, json: options.json, action: 'agent.set-role', agentId: result.agentId });
303
+ });
304
+ // ─── rule ───────────────────────────────────────────────────────────────────
305
+ rule
306
+ .command('list')
307
+ .option('--config <path>')
308
+ .option('--base-dir <path>')
309
+ .option('--json')
310
+ .action((options) => printValue(listRules(loadConfig({ baseDir: options.baseDir, configPath: options.config }).config), true));
311
+ rule
312
+ .command('get <path>')
313
+ .option('--config <path>')
314
+ .option('--base-dir <path>')
315
+ .option('--json')
316
+ .action((dottedPath, options) => printValue(getConfigValue(loadConfig({ baseDir: options.baseDir, configPath: options.config }).config, dottedPath) ?? null, true));
317
+ rule
318
+ .command('validate')
319
+ .option('--config <path>')
320
+ .option('--base-dir <path>')
321
+ .option('--json')
322
+ .action((options) => printValue(validateRules(loadConfig({ baseDir: options.baseDir, configPath: options.config }).config), true));
323
+ rule
324
+ .command('render <templateKind>')
325
+ .requiredOption('--context <json>')
326
+ .option('--config <path>')
327
+ .option('--base-dir <path>')
328
+ .option('--json')
329
+ .action((templateKind, options) => {
330
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
331
+ const tpl = getConfigValue(loaded.config, `templates.${templateKind}`);
332
+ if (!tpl?.body)
333
+ throw new Error(`template not found: ${templateKind}`);
334
+ const rendered = renderTemplate(templateKind, tpl.body, JSON.parse(options.context));
335
+ printValue({ ok: true, templateKind, rendered }, true);
336
+ });
337
+ rule
338
+ .command('set <path>')
339
+ .requiredOption('--value <jsonOrString>')
340
+ .option('--config <path>')
341
+ .option('--base-dir <path>')
342
+ .option('--json')
343
+ .action((dottedPath, options) => {
344
+ if (!dottedPath.startsWith('rules.'))
345
+ throw new Error('rule path must start with rules.');
346
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
347
+ const nextConfig = setValueAtPath(loaded.config, dottedPath, parseConfigInput(options.value));
348
+ persistConfigMutation({ configPath: loaded.configPath, baseDir: loaded.baseDir, nextConfig, json: options.json, action: 'rule.set', path: dottedPath });
349
+ });
350
+ rule
351
+ .command('diff <path>')
352
+ .option('--config <path>')
353
+ .option('--base-dir <path>')
354
+ .option('--json')
355
+ .action((dottedPath, options) => {
356
+ if (!dottedPath.startsWith('rules.'))
357
+ throw new Error('rule path must start with rules.');
358
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
359
+ printValue(diffValues(getValueAtPath(loaded.config, dottedPath), getValueAtPath(defaultConfig, dottedPath)), true);
360
+ });
361
+ rule
362
+ .command('reset')
363
+ .requiredOption('--path <path>')
364
+ .option('--config <path>')
365
+ .option('--base-dir <path>')
366
+ .option('--yes')
367
+ .option('--json')
368
+ .action((options) => {
369
+ if (!options.path.startsWith('rules.'))
370
+ throw new Error('rule path must start with rules.');
371
+ requireYes(options.yes, 'reset rule config');
372
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
373
+ const nextConfig = resetValueAtPath(loaded.config, options.path);
374
+ persistConfigMutation({ configPath: loaded.configPath, baseDir: loaded.baseDir, nextConfig, json: options.json, action: 'rule.reset', path: options.path });
375
+ });
376
+ // ─── template ───────────────────────────────────────────────────────────────
377
+ template
378
+ .command('list')
379
+ .option('--config <path>')
380
+ .option('--base-dir <path>')
381
+ .option('--json')
382
+ .action((options) => {
383
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
384
+ printValue(Object.keys(loaded.config.templates).map((name) => ({ name, path: `templates.${name}` })), true);
385
+ });
386
+ template
387
+ .command('get <name>')
388
+ .option('--config <path>')
389
+ .option('--base-dir <path>')
390
+ .option('--json')
391
+ .action((name, options) => {
392
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
393
+ printValue(getValueAtPath(loaded.config, `templates.${name}`) ?? null, true);
394
+ });
395
+ template
396
+ .command('set <name>')
397
+ .requiredOption('--body <body>')
398
+ .option('--format <format>', 'markdown|text')
399
+ .option('--version <version>')
400
+ .option('--placeholders <jsonArray>')
401
+ .option('--config <path>')
402
+ .option('--base-dir <path>')
403
+ .option('--json')
404
+ .action((name, options) => {
405
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
406
+ const current = getValueAtPath(loaded.config, `templates.${name}`);
407
+ if (!current)
408
+ throw new Error(`template not found: ${name}`);
409
+ const nextTemplate = {
410
+ ...current,
411
+ body: options.body,
412
+ ...(options.format ? { format: options.format } : {}),
413
+ ...(options.version ? { version: Number(options.version) } : {}),
414
+ ...(options.placeholders ? { placeholders: parseConfigInput(options.placeholders) } : {}),
415
+ };
416
+ const nextConfig = setValueAtPath(loaded.config, `templates.${name}`, nextTemplate);
417
+ persistConfigMutation({ configPath: loaded.configPath, baseDir: loaded.baseDir, nextConfig, json: options.json, action: 'template.set', path: `templates.${name}` });
418
+ });
419
+ template
420
+ .command('diff <name>')
421
+ .option('--config <path>')
422
+ .option('--base-dir <path>')
423
+ .option('--json')
424
+ .action((name, options) => {
425
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
426
+ printValue(diffValues(getValueAtPath(loaded.config, `templates.${name}`), getValueAtPath(defaultConfig, `templates.${name}`)), true);
427
+ });
428
+ template
429
+ .command('reset <name>')
430
+ .option('--config <path>')
431
+ .option('--base-dir <path>')
432
+ .option('--yes')
433
+ .option('--json')
434
+ .action((name, options) => {
435
+ requireYes(options.yes, 'reset template config');
436
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
437
+ const nextConfig = resetValueAtPath(loaded.config, `templates.${name}`);
438
+ persistConfigMutation({ configPath: loaded.configPath, baseDir: loaded.baseDir, nextConfig, json: options.json, action: 'template.reset', path: `templates.${name}` });
439
+ });
440
+ template
441
+ .command('render <name>')
442
+ .requiredOption('--context <json>')
443
+ .option('--config <path>')
444
+ .option('--base-dir <path>')
445
+ .option('--json')
446
+ .action((name, options) => {
447
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
448
+ const item = getValueAtPath(loaded.config, `templates.${name}`);
449
+ if (!item?.body)
450
+ throw new Error(`template not found: ${name}`);
451
+ printValue({ ok: true, name, rendered: renderTemplate(name, item.body, JSON.parse(options.context)) }, true);
452
+ });
453
+ // ─── reset ──────────────────────────────────────────────────────────────────
454
+ reset
455
+ .command('config')
456
+ .option('--path <path>', 'dotted config path to restore from defaults', 'all')
457
+ .option('--config <path>')
458
+ .option('--base-dir <path>')
459
+ .option('--yes')
460
+ .option('--json')
461
+ .action((options) => {
462
+ requireYes(options.yes, 'reset config');
463
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
464
+ const nextConfig = resetValueAtPath(loaded.config, options.path);
465
+ persistConfigMutation({ configPath: loaded.configPath, baseDir: loaded.baseDir, nextConfig, json: options.json, action: 'reset.config', path: options.path });
466
+ });
467
+ reset
468
+ .command('state')
469
+ .option('--base-dir <path>')
470
+ .option('--config <path>')
471
+ .option('--yes')
472
+ .option('--json')
473
+ .action((options) => {
474
+ requireYes(options.yes, 'reset runtime state');
475
+ const loaded = loadRuntime({ baseDir: options.baseDir, config: options.config });
476
+ // Remove task data but preserve config and rules
477
+ for (const dir of [loaded.paths.tasksDir, loaded.paths.evidenceDir, loaded.paths.promptsDir, loaded.paths.runsDir]) {
478
+ fs.rmSync(dir, { recursive: true, force: true });
479
+ }
480
+ fs.rmSync(loaded.paths.eventsFile, { force: true });
481
+ fs.rmSync(loaded.paths.indexFile, { force: true });
482
+ ensureBaseState(loaded.paths);
483
+ const index = rebuildIndex(loaded.paths);
484
+ printValue({ ok: true, action: 'reset.state', baseDir: loaded.paths.baseDir, index }, true);
485
+ });
486
+ // ─── state ──────────────────────────────────────────────────────────────────
487
+ state
488
+ .command('check')
489
+ .option('--base-dir <path>')
490
+ .option('--config <path>')
491
+ .option('--json')
492
+ .action((options) => {
493
+ const loaded = loadRuntime({ baseDir: options.baseDir, config: options.config });
494
+ printValue(verifyState(loaded.paths), true);
495
+ });
496
+ program
497
+ .command('index-rebuild')
498
+ .option('--config <path>')
499
+ .option('--base-dir <path>')
500
+ .option('--json')
501
+ .action((options) => {
502
+ const loaded = loadRuntime({ baseDir: options.baseDir, config: options.config });
503
+ printValue(rebuildIndex(loaded.paths), true);
504
+ });
505
+ // ─── task ───────────────────────────────────────────────────────────────────
506
+ task
507
+ .command('dispatch')
508
+ .description('Create a task with full orchestration metadata and boot prompt (replaces dev_dispatch.py)')
509
+ .requiredOption('--title <title>')
510
+ .requiredOption('--description <description>')
511
+ .requiredOption('--scale <scale>', 'simple|normal|risky|large')
512
+ .option('--project-dir <path>', 'target project directory')
513
+ .option('--requested-by <name>', 'who requested this task')
514
+ .option('--constraints <constraints>', 'task constraints')
515
+ .option('--config <path>')
516
+ .option('--base-dir <path>')
517
+ .option('--json')
518
+ .action((options) => {
519
+ const loaded = loadRuntime({ baseDir: options.baseDir, config: options.config });
520
+ const result = dispatchTask(loaded.paths, {
521
+ title: options.title,
522
+ description: options.description,
523
+ scale: options.scale,
524
+ projectDir: options.projectDir,
525
+ requestedBy: options.requestedBy,
526
+ constraints: options.constraints,
527
+ });
528
+ printValue(result, true);
529
+ });
530
+ task
531
+ .command('create')
532
+ .requiredOption('--title <title>')
533
+ .requiredOption('--description <description>')
534
+ .option('--scale <scale>', 'simple|normal|risky|large', 'normal')
535
+ .option('--required-agent <agent>', 'repeatable', (value, prev = []) => [...prev, value], [])
536
+ .option('--project-dir <path>', 'target project directory for this task')
537
+ .option('--requested-by <name>', 'who requested this task')
538
+ .option('--prefix <prefix>', 'task ID prefix (DEV|TEST)', 'DEV')
539
+ .option('--config <path>')
540
+ .option('--base-dir <path>')
541
+ .option('--json')
542
+ .action((options) => {
543
+ const loaded = loadRuntime({ baseDir: options.baseDir, config: options.config });
544
+ const created = createTask(loaded.paths, { title: options.title, description: options.description, scale: options.scale, requiredAgents: options.requiredAgent, projectDir: options.projectDir, requestedBy: options.requestedBy, prefix: options.prefix });
545
+ printValue(created, true);
546
+ });
547
+ task
548
+ .command('list')
549
+ .option('--config <path>')
550
+ .option('--base-dir <path>')
551
+ .option('--json')
552
+ .action((options) => printValue(listTasks(loadRuntime({ baseDir: options.baseDir, config: options.config }).paths), true));
553
+ task
554
+ .command('status <taskId>')
555
+ .option('--config <path>')
556
+ .option('--base-dir <path>')
557
+ .option('--json')
558
+ .action((taskId, options) => {
559
+ const payload = loadTask(loadRuntime({ baseDir: options.baseDir, config: options.config }).paths, taskId);
560
+ if (!payload)
561
+ throw new Error(`task not found: ${taskId}`);
562
+ printValue(payload, true);
563
+ });
564
+ task
565
+ .command('events [taskId]')
566
+ .option('--config <path>')
567
+ .option('--base-dir <path>')
568
+ .option('--json')
569
+ .action((taskId, options) => printValue(listTaskEvents(loadRuntime({ baseDir: options.baseDir, config: options.config }).paths, taskId), true));
570
+ task
571
+ .command('progress')
572
+ .requiredOption('--task-id <taskId>')
573
+ .requiredOption('--actor <actor>')
574
+ .requiredOption('--message <message>')
575
+ .option('--unit-id <unitId>')
576
+ .option('--work-package <workPackage>')
577
+ .option('--config <path>')
578
+ .option('--base-dir <path>')
579
+ .option('--json')
580
+ .action((options) => {
581
+ const payload = recordTaskProgress(loadRuntime({ baseDir: options.baseDir, config: options.config }).paths, { taskId: options.taskId, actor: options.actor, message: options.message, unitId: options.unitId, workPackage: options.workPackage });
582
+ if (!payload)
583
+ throw new Error(`task not found: ${options.taskId}`);
584
+ printValue(payload, true);
585
+ });
586
+ task
587
+ .command('stale')
588
+ .option('--hours <hours>', 'stale threshold hours', '24')
589
+ .option('--apply')
590
+ .option('--reason <reason>', 'block reason', 'stale_timeout')
591
+ .option('--config <path>')
592
+ .option('--base-dir <path>')
593
+ .option('--json')
594
+ .action((options) => {
595
+ const paths = loadRuntime({ baseDir: options.baseDir, config: options.config }).paths;
596
+ const hours = Number(options.hours);
597
+ const payload = options.apply ? applyStalePolicy(paths, hours, options.reason) : { ok: true, hours, count: findStaleTasks(paths, hours).length, tasks: findStaleTasks(paths, hours) };
598
+ printValue(payload, true);
599
+ });
600
+ for (const [name, status] of Object.entries(STATUS_MAP)) {
601
+ task
602
+ .command(`${name} <taskId>`)
603
+ .option('--config <path>')
604
+ .option('--base-dir <path>')
605
+ .option('--json')
606
+ .action((taskId, options) => {
607
+ const payload = updateTaskStatus(loadRuntime({ baseDir: options.baseDir, config: options.config }).paths, taskId, status);
608
+ if (!payload)
609
+ throw new Error(`task not found: ${taskId}`);
610
+ printValue(payload, true);
611
+ });
612
+ }
613
+ task
614
+ .command('finalize <taskId>')
615
+ .description('Finalize a task: merge evidence, check units, optionally auto-report (replaces dev_finalize.py)')
616
+ .option('--auto-report', 'auto-transition to REPORTED if complete')
617
+ .option('--sec-issues', 'flag security issues (blocks auto-report)')
618
+ .option('--qa-issues', 'flag QA issues (blocks auto-report)')
619
+ .option('--config <path>')
620
+ .option('--base-dir <path>')
621
+ .option('--json')
622
+ .action((taskId, options) => {
623
+ const loaded = loadRuntime({ baseDir: options.baseDir, config: options.config });
624
+ const result = finalizeTask(loaded.paths, {
625
+ taskId,
626
+ autoReport: Boolean(options.autoReport),
627
+ secIssues: Boolean(options.secIssues),
628
+ qaIssues: Boolean(options.qaIssues),
629
+ });
630
+ if (!result)
631
+ throw new Error(`task not found: ${taskId}`);
632
+ printValue(result, true);
633
+ });
634
+ // ─── worker ─────────────────────────────────────────────────────────────────
635
+ worker
636
+ .command('prepare')
637
+ .requiredOption('--task-id <taskId>')
638
+ .requiredOption('--agent-id <agentId>')
639
+ .requiredOption('--description <description>')
640
+ .option('--constraints <constraints>')
641
+ .option('--unit-id <unitId>')
642
+ .option('--work-package <workPackage>')
643
+ .option('--dod <dod>')
644
+ .option('--project-dir <path>', 'working directory for this worker')
645
+ .option('--config <path>')
646
+ .option('--base-dir <path>')
647
+ .option('--json')
648
+ .action((options) => {
649
+ const payload = prepareWorker(loadRuntime({ baseDir: options.baseDir, config: options.config }).paths, { taskId: options.taskId, agentId: options.agentId, description: options.description, constraints: options.constraints, unitId: options.unitId, workPackage: options.workPackage, dod: options.dod, projectDir: options.projectDir });
650
+ if (!payload)
651
+ throw new Error(`task not found: ${options.taskId}`);
652
+ printValue(payload, true);
653
+ });
654
+ worker
655
+ .command('register')
656
+ .requiredOption('--task-id <taskId>')
657
+ .requiredOption('--agent-id <agentId>')
658
+ .requiredOption('--session-key <sessionKey>')
659
+ .option('--run-id <runId>')
660
+ .option('--session-id <sessionId>')
661
+ .option('--unit-id <unitId>')
662
+ .option('--work-package <workPackage>')
663
+ .option('--reason <reason>')
664
+ .option('--config <path>')
665
+ .option('--base-dir <path>')
666
+ .option('--json')
667
+ .action((options) => {
668
+ const payload = registerWorker(loadRuntime({ baseDir: options.baseDir, config: options.config }).paths, { taskId: options.taskId, agentId: options.agentId, sessionKey: options.sessionKey, runId: options.runId, sessionId: options.sessionId, unitId: options.unitId, workPackage: options.workPackage, reason: options.reason });
669
+ if (!payload)
670
+ throw new Error(`task not found: ${options.taskId}`);
671
+ printValue(payload, true);
672
+ });
673
+ worker
674
+ .command('complete')
675
+ .requiredOption('--task-id <taskId>')
676
+ .requiredOption('--agent-id <agentId>')
677
+ .requiredOption('--session-key <sessionKey>')
678
+ .requiredOption('--run-id <runId>')
679
+ .option('--session-id <sessionId>')
680
+ .option('--result <result>', 'done|blocked|skipped', 'done')
681
+ .option('--unit-id <unitId>')
682
+ .option('--work-package <workPackage>')
683
+ .option('--config <path>')
684
+ .option('--base-dir <path>')
685
+ .option('--json')
686
+ .action((options) => {
687
+ const payload = completeWorker(loadRuntime({ baseDir: options.baseDir, config: options.config }).paths, { taskId: options.taskId, agentId: options.agentId, sessionKey: options.sessionKey, runId: options.runId, sessionId: options.sessionId, result: options.result, unitId: options.unitId, workPackage: options.workPackage });
688
+ if (!payload)
689
+ throw new Error(`task not found: ${options.taskId}`);
690
+ printValue(payload, true);
691
+ });
692
+ // ─── evidence ───────────────────────────────────────────────────────────────
693
+ evidence
694
+ .command('collect')
695
+ .requiredOption('--task-id <taskId>')
696
+ .requiredOption('--agent-id <agentId>')
697
+ .option('--run-id <runId>')
698
+ .option('--unit-id <unitId>')
699
+ .option('--session-key <sessionKey>')
700
+ .option('--session-id <sessionId>')
701
+ .option('--transcript <transcript>')
702
+ .option('--summary <summary>')
703
+ .option('--tool-result <toolResult>', 'repeatable', (value, prev = []) => [...prev, value], [])
704
+ .option('--notes <notes>')
705
+ .option('--limit <limit>', 'transcript line limit', '40')
706
+ .option('--config <path>')
707
+ .option('--base-dir <path>')
708
+ .option('--json')
709
+ .action((options) => {
710
+ const payload = collectEvidence(loadRuntime({ baseDir: options.baseDir, config: options.config }).paths, { taskId: options.taskId, agentId: options.agentId, runId: options.runId, unitId: options.unitId, sessionKey: options.sessionKey, sessionId: options.sessionId, transcript: options.transcript, summary: options.summary, toolResults: options.toolResult, notes: options.notes, limit: Number(options.limit) });
711
+ if (!payload)
712
+ throw new Error(`task not found: ${options.taskId}`);
713
+ printValue(payload, true);
714
+ });
715
+ evidence
716
+ .command('merge')
717
+ .requiredOption('--task-id <taskId>')
718
+ .option('--required-agent <agent>', 'repeatable', (value, prev = []) => [...prev, value], [])
719
+ .option('--require-qa')
720
+ .option('--config <path>')
721
+ .option('--base-dir <path>')
722
+ .option('--json')
723
+ .action((options) => {
724
+ const payload = mergeEvidence(loadRuntime({ baseDir: options.baseDir, config: options.config }).paths, { taskId: options.taskId, requiredAgents: options.requiredAgent, requireQa: Boolean(options.requireQa) });
725
+ if (!payload)
726
+ throw new Error(`task not found: ${options.taskId}`);
727
+ printValue(payload, true);
728
+ });
729
+ // ─── report ─────────────────────────────────────────────────────────────────
730
+ report
731
+ .command('render')
732
+ .requiredOption('--task-id <taskId>')
733
+ .option('--record-events')
734
+ .option('--config <path>')
735
+ .option('--base-dir <path>')
736
+ .option('--json')
737
+ .action((options) => {
738
+ const payload = renderReport(loadRuntime({ baseDir: options.baseDir, config: options.config }).paths, { taskId: options.taskId, recordEvents: Boolean(options.recordEvents) });
739
+ if (!payload)
740
+ throw new Error(`task not found: ${options.taskId}`);
741
+ printValue(payload, true);
742
+ });
743
+ // ─── pipeline ───────────────────────────────────────────────────────────────
744
+ pipeline
745
+ .command('run')
746
+ .requiredOption('--title <title>')
747
+ .requiredOption('--description <description>')
748
+ .option('--scale <scale>', 'simple|normal|risky|large', 'normal')
749
+ .option('--required-agent <agent>', 'repeatable', (value, prev = []) => [...prev, value], [])
750
+ .option('--evidence-summary <agentEqSummary>', 'repeatable', (value, prev = []) => [...prev, value], [])
751
+ .option('--require-qa')
752
+ .option('--auto-report')
753
+ .option('--record-feedback')
754
+ .option('--config <path>')
755
+ .option('--base-dir <path>')
756
+ .option('--json')
757
+ .action((options) => {
758
+ const payload = runPipeline(loadRuntime({ baseDir: options.baseDir, config: options.config }).paths, { title: options.title, description: options.description, scale: options.scale, requiredAgents: options.requiredAgent, evidenceSummaries: options.evidenceSummary, requireQa: Boolean(options.requireQa), autoReport: Boolean(options.autoReport), recordFeedback: Boolean(options.recordFeedback) });
759
+ printValue(payload, true);
760
+ });
761
+ // ─── run / inspect ──────────────────────────────────────────────────────────
762
+ program
763
+ .command('run <workflowPath>')
764
+ .description('Run a minimal sequential workflow file')
765
+ .option('--config <path>')
766
+ .option('--base-dir <path>')
767
+ .option('--json')
768
+ .action(async (workflowPath, options) => {
769
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
770
+ const result = await runWorkflow({ config: loaded.config, workflowPath });
771
+ if (options.json) {
772
+ printValue({ ...result.record, savedPath: result.savedPath }, true);
773
+ return;
774
+ }
775
+ console.log(summarizeRun(result.record));
776
+ console.log(`saved: ${result.savedPath}`);
777
+ });
778
+ program
779
+ .command('inspect <runIdOrPath>')
780
+ .description('Inspect a saved run record')
781
+ .option('--config <path>')
782
+ .option('--base-dir <path>')
783
+ .option('--json')
784
+ .action((runIdOrPath, options) => {
785
+ const loaded = loadConfig({ baseDir: options.baseDir, configPath: options.config });
786
+ printValue(loadRunRecord(loaded.config, runIdOrPath), true);
787
+ });
788
+ program.parseAsync(process.argv).catch((error) => {
789
+ console.error(error instanceof Error ? error.message : String(error));
790
+ process.exitCode = 1;
791
+ });