@crewx/workflow 0.3.22-rc.18 → 0.3.22-rc.181

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 (49) hide show
  1. package/SKILL.md +40 -4
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +268 -86
  4. package/dist/cli.js.map +1 -1
  5. package/dist/src/card-state.d.ts +20 -0
  6. package/dist/src/card-state.d.ts.map +1 -0
  7. package/dist/src/card-state.js +121 -0
  8. package/dist/src/card-state.js.map +1 -0
  9. package/dist/src/engine.d.ts +9 -2
  10. package/dist/src/engine.d.ts.map +1 -1
  11. package/dist/src/engine.js +70 -22
  12. package/dist/src/engine.js.map +1 -1
  13. package/dist/src/failure.d.ts +41 -0
  14. package/dist/src/failure.d.ts.map +1 -0
  15. package/dist/src/failure.js +122 -0
  16. package/dist/src/failure.js.map +1 -0
  17. package/dist/src/list-view.d.ts +22 -0
  18. package/dist/src/list-view.d.ts.map +1 -0
  19. package/dist/src/list-view.js +188 -0
  20. package/dist/src/list-view.js.map +1 -0
  21. package/dist/src/mermaid.js +3 -3
  22. package/dist/src/mermaid.js.map +1 -1
  23. package/dist/src/parent-task-sync.d.ts +139 -0
  24. package/dist/src/parent-task-sync.d.ts.map +1 -0
  25. package/dist/src/parent-task-sync.js +445 -0
  26. package/dist/src/parent-task-sync.js.map +1 -0
  27. package/dist/src/retry-policy.d.ts +38 -0
  28. package/dist/src/retry-policy.d.ts.map +1 -0
  29. package/dist/src/retry-policy.js +140 -0
  30. package/dist/src/retry-policy.js.map +1 -0
  31. package/dist/src/run-manager.d.ts +36 -2
  32. package/dist/src/run-manager.d.ts.map +1 -1
  33. package/dist/src/run-manager.js +1195 -234
  34. package/dist/src/run-manager.js.map +1 -1
  35. package/dist/src/types.d.ts +37 -3
  36. package/dist/src/types.d.ts.map +1 -1
  37. package/dist/src/types.js +2 -0
  38. package/dist/src/types.js.map +1 -1
  39. package/dist/src/utils/output-format.d.ts +12 -0
  40. package/dist/src/utils/output-format.d.ts.map +1 -1
  41. package/dist/src/utils/output-format.js +103 -5
  42. package/dist/src/utils/output-format.js.map +1 -1
  43. package/dist/src/workflow-persistence.d.ts +48 -0
  44. package/dist/src/workflow-persistence.d.ts.map +1 -0
  45. package/dist/src/workflow-persistence.js +224 -0
  46. package/dist/src/workflow-persistence.js.map +1 -0
  47. package/dist/workflow-schema.json +29 -4
  48. package/package.json +23 -4
  49. package/workflow-schema.json +29 -4
package/SKILL.md CHANGED
@@ -221,6 +221,42 @@ analyze:
221
221
  next: calculate
222
222
  ```
223
223
 
224
+ #### agent_task — execution context & failure signaling
225
+
226
+ Every `agent_task` prompt is automatically prefixed with a `<crewx.workflow>` preamble
227
+ telling the agent it runs headless inside a workflow node (workflow ID, node ID, run ID),
228
+ that clarifying questions are impossible, and how to signal failure. Disable with
229
+ `CREWX_WORKFLOW_PREAMBLE=off`.
230
+
231
+ Two semantic-failure channels stop the run at the node (in addition to non-zero exit codes):
232
+
233
+ 1. **`workflow_signal` (agent-initiated)** — the agent outputs exactly one JSON object:
234
+
235
+ ```json
236
+ {"workflow_signal": "fail", "reason": "build is red — tsc reports 14 errors"}
237
+ ```
238
+
239
+ The engine detects it (pure JSON or embedded in prose), marks the node failed, and
240
+ records the reason in `run.error`. This is the agent's `exit 1`.
241
+
242
+ 2. **`fail_when` (node-declared)** — a safe expression evaluated against state *after*
243
+ the node's output is stored. Truthy → node fails, run halts. The judged output is
244
+ still preserved in state for inspection. No separate `check_*` branch node needed.
245
+
246
+ ```yaml
247
+ verify:
248
+ type: agent_task
249
+ agent: core_dev
250
+ output: verify_result
251
+ output_format: json
252
+ fail_when: 'state.verify_result.status !== "pass"'
253
+ next: smoke_test
254
+ ```
255
+
256
+ `fail_when` works on `agent_task`, `skill_task`, and `shell_task`. It uses the same
257
+ safe expression subset as `branch` conditions (`includes()`, `===`, `&&`, `Number()`...).
258
+ Use `branch` when failure should route somewhere; use `fail_when` when failure should stop the run.
259
+
224
260
  #### agent_task — JSON output mode
225
261
 
226
262
  When `output_format: json`, the engine requires the agent to return a single JSON object.
@@ -298,7 +334,7 @@ extract_attachments:
298
334
  | `output_schema` | object | No | Standard JSON Schema 2020-12 for parsed output. |
299
335
  | `output_retry` | integer | No | Retry attempts if JSON parse or schema validation fails. |
300
336
 
301
- `skill_task` uses `CREWX_CLI` (default: `npx crewx`) and executes the equivalent of:
337
+ `skill_task` uses `CREWX_CLI` (default: `crewx`) and executes the equivalent of:
302
338
 
303
339
  ```bash
304
340
  crewx skill <skill> ...args
@@ -561,7 +597,7 @@ Examples:
561
597
  | Input | Run behavior |
562
598
  |-------|--------------|
563
599
  | `/wf rc-publish` | Starts `rc-publish` in auto mode |
564
- | `/wf rc-publish 미리승인한다` | Starts `rc-publish` with `state.input = "미리승인한다"` |
600
+ | `/wf rc-publish pre-approved` | Starts `rc-publish` with `state.input = "pre-approved"` |
565
601
  | `/wf rc-publish --manual dry run` | Creates the run in manual mode with `state.input = "dry run"` |
566
602
  | `/wf rc-publish --auto prepare RC notes` | Explicit auto mode; same as default |
567
603
 
@@ -589,7 +625,7 @@ Branches and templates may also read `state.input`:
589
625
  condition: state.input.includes("dry run")
590
626
  ```
591
627
 
592
- Important security rule: `state.input` is **not authority**. It may influence prompts, branches, and templates, but it must never bypass an `approval` node. For example, `/wf rc-publish 미리승인한다` stores the text in `state.input`; it does not approve publishing. Approval still requires the signed ChatCard action button.
628
+ Important security rule: `state.input` is **not authority**. It may influence prompts, branches, and templates, but it must never bypass an `approval` node. For example, `/wf rc-publish pre-approved` stores the text in `state.input`; it does not approve publishing. Approval still requires the signed ChatCard action button.
593
629
 
594
630
  ### Stored ChatCard
595
631
 
@@ -645,7 +681,7 @@ Rules:
645
681
  | Variable | Description | Default |
646
682
  |----------|-------------|---------|
647
683
  | `CREWX_WORKSPACE` | Workspace directory (where `workflows/` lives) | `.` |
648
- | `CREWX_CLI` | CrewX CLI path (for `agent_task` and `skill_task`) | `npx crewx` |
684
+ | `CREWX_CLI` | CrewX CLI path (for `agent_task` and `skill_task`) | `crewx` |
649
685
  | `CREWX_WORKFLOW_TIMEOUT` | `agent_task` execution timeout (ms) | `1800000` (30 min) |
650
686
  | `CREWX_WORKFLOW_SHELL` | First safety gate for `shell_task`; must be `1` | unset |
651
687
  | `CREWX_CONFIG` | Custom config file path | `crewx.yaml` |
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":";AAgBA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,KAAK,EAAE,cAAc,EAAgB,YAAY,EAA+B,MAAM,aAAa,CAAC;AA6F3G,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;CACrC;AAED,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,cAAc,EACtB,SAAS,CAAC,EAAE,cAAc,EAAE,GAC3B,gBAAgB,CAkDlB;AAmxBD,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAClC,MAAM,GAAG,IAAI,CAoBf;AAMD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,MAAM,EAAE,CAsB1E;AA4CD,wBAAsB,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsD9D"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":";AAmBA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAS9C,OAAO,KAAK,EAAE,cAAc,EAAgB,YAAY,EAA6C,MAAM,aAAa,CAAC;AA4GzH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;CACrC;AAED,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,cAAc,EACtB,SAAS,CAAC,EAAE,cAAc,EAAE,GAC3B,gBAAgB,CAkDlB;AAg9BD,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAClC,MAAM,GAAG,IAAI,CAoBf;AAMD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,MAAM,EAAE,CAsB1E;AA4CD,wBAAsB,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA2D9D"}
package/dist/cli.js CHANGED
@@ -39,11 +39,15 @@ exports.findNextNodeForCAL = findNextNodeForCAL;
39
39
  exports.getFlowChain = getFlowChain;
40
40
  exports.main = main;
41
41
  const fs = __importStar(require("fs"));
42
+ const list_view_1 = require("./src/list-view");
42
43
  const path = __importStar(require("path"));
43
44
  const child_process_1 = require("child_process");
44
- const yaml = __importStar(require("js-yaml"));
45
+ const sdk_1 = require("@crewx/sdk");
46
+ const repository_1 = require("@crewx/sdk/repository");
45
47
  const engine_1 = require("./src/engine");
46
48
  const run_manager_1 = require("./src/run-manager");
49
+ const workflow_persistence_1 = require("./src/workflow-persistence");
50
+ const parent_task_sync_1 = require("./src/parent-task-sync");
47
51
  const knowledge_core_1 = require("@crewx/knowledge-core");
48
52
  let PKG_VERSION = 'unknown';
49
53
  try {
@@ -63,7 +67,9 @@ async function loadTracer() {
63
67
  return null;
64
68
  }
65
69
  }
66
- const BOOLEAN_FLAGS = new Set(['help', 'json', 'mermaid', 'version', 'no-exec', 'auto']);
70
+ const BOOLEAN_FLAGS = new Set([
71
+ 'help', 'json', 'mermaid', 'version', 'no-exec', 'auto', 'no-run', 'apply', 'dry-run',
72
+ ]);
67
73
  function parseArgs(args) {
68
74
  const result = { positional: [], options: {} };
69
75
  for (let i = 0; i < args.length; i++) {
@@ -108,17 +114,21 @@ function getProjectRoot() {
108
114
  }
109
115
  }
110
116
  function getWorkflowConfig() {
111
- try {
112
- const configPath = process.env.CREWX_CONFIG ?? path.join(getProjectRoot(), 'crewx.yaml');
113
- const raw = fs.readFileSync(configPath, 'utf8');
114
- const config = yaml.load(raw);
115
- const settings = config?.settings;
116
- const wf = settings?.workflow ?? {};
117
- return { paths: wf.paths ?? ['workflows'] };
118
- }
119
- catch {
120
- return { paths: ['workflows'] };
121
- }
117
+ return (0, list_view_1.readWorkflowConfig)(process.env.CREWX_CONFIG ?? path.join(getProjectRoot(), 'crewx.yaml'));
118
+ }
119
+ function getNodeResultStatus(run, nodeId, nodeType, noExec) {
120
+ if (noExec)
121
+ return null;
122
+ const audit = [...(run.audit ?? [])]
123
+ .reverse()
124
+ .find((entry) => entry.node_id === nodeId && entry.type !== 'retry');
125
+ if (!audit)
126
+ return null;
127
+ if (audit.error || nodeType === 'error')
128
+ return 'failed';
129
+ if (nodeType === 'approval' || nodeType === 'parallel')
130
+ return 'paused';
131
+ return 'completed';
122
132
  }
123
133
  function validateSlugInput(nameOrPath) {
124
134
  if (!nameOrPath.endsWith('.yaml') && !nameOrPath.endsWith('.yml')) {
@@ -166,21 +176,12 @@ function resolveWorkflow(nameOrPath, paths, engine, preloaded) {
166
176
  ` Hint: use "workflow list" to see all workflow IDs`);
167
177
  }
168
178
  function loadAllWorkflows(engine, paths) {
169
- const all = [];
170
- const seenIds = new Set();
171
- for (const dir of paths) {
172
- const absDir = path.resolve(process.cwd(), dir);
173
- const workflows = engine.loadWorkflows(absDir);
174
- if (!workflows)
175
- continue;
176
- for (const wf of workflows) {
177
- if (!seenIds.has(wf.id)) {
178
- seenIds.add(wf.id);
179
- all.push(wf);
180
- }
181
- }
182
- }
183
- return all;
179
+ return (0, list_view_1.loadWorkflowPaths)(paths, process.cwd());
180
+ }
181
+ function resolveWorkflowSpecForRun(run, engine) {
182
+ const runFile = path.resolve(run.workflow_file);
183
+ const workflows = engine.loadWorkflows(path.dirname(runFile));
184
+ return workflows?.find((workflow) => path.resolve(workflow.filePath) === runFile && workflow.id === run.workflow_id)?.spec;
184
185
  }
185
186
  function printUsage() {
186
187
  console.log(`
@@ -199,13 +200,18 @@ Commands:
199
200
  workflow run node <id> <node> Execute node (auto-invoke agent)
200
201
  workflow run auto <id> Auto-run from current node until end/pause
201
202
  workflow run reset <id> Reset execution
203
+ workflow run retry <id> Retry a failed run from the failed node (--no-run: transition only)
204
+ ⚠️ re-executes the failed node; safe only if it is idempotent
202
205
  workflow run list List all executions
206
+ workflow repair <run-id> Preview parent-task repair (use --apply to write)
203
207
  workflow usage Show detailed guide
204
208
 
205
209
  Options:
206
210
  --json JSON output
207
211
  --mermaid Output Mermaid diagram (show only)
208
212
  --no-exec Move cursor only, skip agent execution (run node)
213
+ --dry-run Preview workflow parent repair without writes (default)
214
+ --apply Apply workflow parent repair under the run lock
209
215
  --version Show version
210
216
  --help, -h Show this help
211
217
 
@@ -261,58 +267,7 @@ function formatFlow(nodes) {
261
267
  }
262
268
  function cmdList(engine, jsonMode) {
263
269
  const { paths } = getWorkflowConfig();
264
- const workflows = loadAllWorkflows(engine, paths);
265
- if (workflows.length === 0) {
266
- const msg = `No workflow YAML files found in: ${paths.join(', ')}`;
267
- if (jsonMode) {
268
- console.log(JSON.stringify({ success: true, message: msg, workflows: [] }));
269
- }
270
- else {
271
- console.log(`\u2139\uFE0F ${msg}`);
272
- }
273
- return;
274
- }
275
- if (jsonMode) {
276
- console.log(JSON.stringify({
277
- success: true,
278
- workflows: workflows.map(wf => {
279
- const meta = wf.spec.metadata ?? {};
280
- const nodeCount = wf.spec.nodes ? Object.keys(wf.spec.nodes).length : 0;
281
- return {
282
- id: wf.id,
283
- name: meta.name ?? wf.id,
284
- description: meta.description ?? '',
285
- node_count: nodeCount,
286
- file: wf.filePath,
287
- };
288
- }),
289
- }, null, 2));
290
- }
291
- else {
292
- console.log(`\uD83D\uDCCB \uC6CC\uD06C\uD50C\uB85C \uBAA9\uB85D`);
293
- workflows.forEach((wf, i) => {
294
- const meta = wf.spec.metadata ?? {};
295
- const nodeCount = wf.spec.nodes ? Object.keys(wf.spec.nodes).length : 0;
296
- const descLine = meta.description ? `\n \uD83D\uDCDD ${meta.description.trim().split('\n')[0]}` : '';
297
- console.log(` ${i + 1}. ${wf.id} \u2014 ${meta.name ?? wf.id} (${nodeCount} nodes)\n \uD83D\uDCC1 ${wf.filePath}${descLine}`);
298
- });
299
- const firstId = workflows[0]?.id;
300
- if (firstId) {
301
- console.log(`\n${'━'.repeat(43)}`);
302
- console.log(`🔍 워크플로 상세: npx workflow show ${firstId}`);
303
- console.log(`🔀 워크플로 실행: npx workflow run start <id> --set key="value"`);
304
- }
305
- const listCtx = {
306
- commandId: 'workflow.list',
307
- params: {},
308
- resultIds: workflows.slice(0, 3).map(w => w.id),
309
- resultCount: workflows.length,
310
- };
311
- const listActions = (0, knowledge_core_1.generateNextActions)(listCtx);
312
- const listBlock = (0, knowledge_core_1.formatActions)(listActions);
313
- if (listBlock)
314
- console.log(listBlock);
315
- }
270
+ console.log((0, list_view_1.formatWorkflowList)({ paths, workflows: loadAllWorkflows(engine, paths), jsonMode }));
316
271
  }
317
272
  function cmdShow(engine, name, jsonMode, mermaidMode) {
318
273
  const { paths } = getWorkflowConfig();
@@ -514,7 +469,7 @@ function cmdValidate(engine, target, jsonMode) {
514
469
  workflows: flatResults,
515
470
  }, null, 2));
516
471
  }
517
- if (!hasErrors) {
472
+ if (!jsonMode && !hasErrors) {
518
473
  const calName = resolvedSlug ?? firstValidName;
519
474
  if (calName) {
520
475
  const valCtx = {
@@ -531,10 +486,180 @@ function cmdValidate(engine, target, jsonMode) {
531
486
  if (hasErrors)
532
487
  process.exit(1);
533
488
  }
489
+ function repairChangeForOutput(change) {
490
+ return {
491
+ task_id: change.id,
492
+ kind: change.kind,
493
+ before: {
494
+ status: change.before.status,
495
+ error: change.before.error,
496
+ completed_at: change.before.completedAt,
497
+ duration_ms: change.before.durationMs,
498
+ },
499
+ after: {
500
+ status: change.after.status,
501
+ error: change.after.error,
502
+ completed_at: change.after.completedAt,
503
+ duration_ms: change.after.durationMs,
504
+ },
505
+ reason: change.reason,
506
+ };
507
+ }
508
+ function auditSnapshotChangeForOutput(change) {
509
+ return {
510
+ instance_id: change.instanceId,
511
+ before: change.before,
512
+ after: change.after,
513
+ };
514
+ }
515
+ function reportRepair(preview, mode, jsonMode, projectionErrors = []) {
516
+ const changes = preview.parentChanges.map(repairChangeForOutput);
517
+ const auditCardChanges = preview.auditSnapshotChanges.map(auditSnapshotChangeForOutput);
518
+ const errors = [...preview.errors, ...projectionErrors];
519
+ if (jsonMode) {
520
+ console.log(JSON.stringify({
521
+ success: true,
522
+ exec_id: preview.runId,
523
+ mode,
524
+ dry_run: mode === 'dry-run',
525
+ applied: mode === 'apply',
526
+ run_status: preview.runStatus,
527
+ changes,
528
+ audit_card_changes: auditCardChanges,
529
+ ...(errors.length > 0 ? { errors } : {}),
530
+ }, null, 2));
531
+ return;
532
+ }
533
+ console.log(`🔧 Workflow repair (${mode}): ${preview.runId}`);
534
+ console.log(` Run status: ${preview.runStatus}`);
535
+ if (changes.length === 0 && auditCardChanges.length === 0) {
536
+ console.log(' No parent lifecycle changes needed.');
537
+ }
538
+ else {
539
+ for (const change of changes) {
540
+ console.log(` ${mode === 'apply' ? 'Updated' : 'Would update'} ${change.task_id} (${change.kind}): ${change.before.status} → ${change.after.status}`);
541
+ console.log(` Reason: ${change.reason}`);
542
+ }
543
+ for (const change of auditCardChanges) {
544
+ console.log(` ${mode === 'apply' ? 'Updated' : 'Would update'} audit card ${change.instance_id}: ${change.before.phase ?? '(unknown)'} → ${change.after.phase}`);
545
+ }
546
+ }
547
+ for (const error of errors) {
548
+ console.error(` Warning: ${error}`);
549
+ }
550
+ }
551
+ function validateRepairRun(run, execId) {
552
+ if (run.id !== execId) {
553
+ throw new Error(`Execution file ID mismatch: requested ${execId}, found ${run.id || '(missing)'}`);
554
+ }
555
+ if (!run.workflow_id || !run.workflow_file || !run.created_at || !run.updated_at
556
+ || typeof run.current_node !== 'string'
557
+ || !Array.isArray(run.completed_nodes)
558
+ || !run.state || typeof run.state !== 'object'
559
+ || !run.initial_state || typeof run.initial_state !== 'object') {
560
+ throw new Error(`Run "${execId}" is incomplete and cannot be repaired`);
561
+ }
562
+ }
563
+ function cmdRepair(rest, jsonMode, apply, dryRun) {
564
+ const execId = rest[0];
565
+ if (!execId) {
566
+ const msg = 'Usage: workflow repair <run-id> [--dry-run|--apply]';
567
+ if (jsonMode)
568
+ console.log(JSON.stringify({ success: false, error: msg }));
569
+ else
570
+ console.error(`❌ ${msg}`);
571
+ process.exit(1);
572
+ }
573
+ if (apply && dryRun) {
574
+ const msg = 'Choose either --dry-run or --apply, not both';
575
+ if (jsonMode)
576
+ console.log(JSON.stringify({ success: false, error: msg }));
577
+ else
578
+ console.error(`❌ ${msg}`);
579
+ process.exit(1);
580
+ }
581
+ const projectRoot = getProjectRoot();
582
+ const workspaceId = (0, sdk_1.hashWorkspaceId)(projectRoot);
583
+ const taskRepo = new repository_1.TaskRepository();
584
+ const mgr = new run_manager_1.RunManager(undefined, projectRoot);
585
+ const engine = new engine_1.WorkflowEngine();
586
+ let uiInstanceStore;
587
+ let readOnlyUiInstanceStore;
588
+ const resolveWorkflowSpec = (run) => resolveWorkflowSpecForRun(run, engine);
589
+ const getUiInstanceStore = () => {
590
+ uiInstanceStore ?? (uiInstanceStore = new sdk_1.UiInstanceStore(taskRepo.resolveDbPath()));
591
+ return uiInstanceStore;
592
+ };
593
+ const getReadOnlyUiInstanceStore = () => {
594
+ readOnlyUiInstanceStore ?? (readOnlyUiInstanceStore = (0, sdk_1.openUiInstanceReadStore)(taskRepo.resolveDbPath()));
595
+ return readOnlyUiInstanceStore;
596
+ };
597
+ const persistenceProjection = (0, workflow_persistence_1.createWorkflowPersistenceProjection)({
598
+ taskRepo,
599
+ workspaceId,
600
+ resolveWorkflowSpec,
601
+ getUiInstanceStore,
602
+ });
603
+ const persistencePreviewOptions = {
604
+ taskRepo,
605
+ workspaceId,
606
+ resolveWorkflowSpec,
607
+ getUiInstanceStore: getReadOnlyUiInstanceStore,
608
+ };
609
+ try {
610
+ const observed = mgr.readRun(execId);
611
+ if (!observed)
612
+ throw new Error(`Execution not found: ${execId}`);
613
+ validateRepairRun(observed, execId);
614
+ if (!(0, parent_task_sync_1.deriveWorkflowParentLifecycle)(observed)) {
615
+ throw new Error(`Run "${execId}" has an unsupported or insufficient persisted status: ${observed.status ?? 'pending'}`);
616
+ }
617
+ if (!apply) {
618
+ reportRepair((0, workflow_persistence_1.previewWorkflowPersistenceProjection)(persistencePreviewOptions, observed), 'dry-run', jsonMode);
619
+ return;
620
+ }
621
+ const result = mgr.withRunLock(execId, (latest) => {
622
+ validateRepairRun(latest, execId);
623
+ if (!(0, parent_task_sync_1.deriveWorkflowParentLifecycle)(latest)) {
624
+ throw new Error(`Run "${execId}" has an unsupported or insufficient persisted status: ${latest.status ?? 'pending'}`);
625
+ }
626
+ const preview = (0, workflow_persistence_1.previewWorkflowPersistenceProjection)(persistencePreviewOptions, latest);
627
+ const projection = persistenceProjection(latest);
628
+ return { preview, projection };
629
+ });
630
+ reportRepair(result.preview, 'apply', jsonMode, result.projection.errors);
631
+ }
632
+ catch (e) {
633
+ const msg = e instanceof Error ? e.message : String(e);
634
+ if (jsonMode)
635
+ console.log(JSON.stringify({ success: false, error: msg }));
636
+ else
637
+ console.error(`❌ ${msg}`);
638
+ process.exit(1);
639
+ }
640
+ finally {
641
+ uiInstanceStore?.close();
642
+ readOnlyUiInstanceStore?.close();
643
+ }
644
+ }
534
645
  async function cmdRun(rest, rawArgs, jsonMode, engine) {
535
646
  const runSub = rest[0];
536
647
  const runArgs = rest.slice(1);
537
- const mgr = new run_manager_1.RunManager();
648
+ const projectRoot = getProjectRoot();
649
+ const taskRepo = new repository_1.TaskRepository();
650
+ let uiInstanceStore;
651
+ const persistenceProjection = (0, workflow_persistence_1.createWorkflowPersistenceProjection)({
652
+ taskRepo,
653
+ workspaceId: (0, sdk_1.hashWorkspaceId)(projectRoot),
654
+ resolveWorkflowSpec: (run) => resolveWorkflowSpecForRun(run, engine),
655
+ getUiInstanceStore: () => {
656
+ uiInstanceStore ?? (uiInstanceStore = new sdk_1.UiInstanceStore(taskRepo.resolveDbPath()));
657
+ return uiInstanceStore;
658
+ },
659
+ });
660
+ const mgr = new run_manager_1.RunManager(undefined, projectRoot, {
661
+ onRunPersisted: persistenceProjection,
662
+ });
538
663
  const { paths } = getWorkflowConfig();
539
664
  switch (runSub) {
540
665
  case 'start': {
@@ -707,6 +832,7 @@ async function cmdRun(rest, rawArgs, jsonMode, engine) {
707
832
  const run = noExec
708
833
  ? mgr.moveNode(execId, nodeId)
709
834
  : await mgr.executeNode(execId, nodeId);
835
+ const nodeStatus = getNodeResultStatus(run, nodeId, mgr.getNodeType(run.workflow_file, run.workflow_id, nodeId), noExec);
710
836
  if (jsonMode) {
711
837
  console.log(JSON.stringify({
712
838
  success: true,
@@ -726,8 +852,8 @@ async function cmdRun(rest, rawArgs, jsonMode, engine) {
726
852
  console.log(` Completed: ${run.completed_nodes.join(' \u2192 ')}`);
727
853
  }
728
854
  console.log(` Current: ${run.current_node}`);
729
- if (run.status) {
730
- console.log(` Status: ${run.status}`);
855
+ if (nodeStatus) {
856
+ console.log(` Status: ${nodeStatus}`);
731
857
  }
732
858
  }
733
859
  let nextNode = null;
@@ -809,6 +935,58 @@ async function cmdRun(rest, rawArgs, jsonMode, engine) {
809
935
  }
810
936
  break;
811
937
  }
938
+ case 'retry': {
939
+ const positional = runArgs.filter(a => !a.startsWith('--'));
940
+ const execId = positional[0];
941
+ if (!execId) {
942
+ const msg = 'Usage: workflow run retry <exec-id> [--no-run]';
943
+ if (jsonMode) {
944
+ console.log(JSON.stringify({ success: false, error: msg }));
945
+ }
946
+ else {
947
+ console.error(`❌ ${msg}`);
948
+ }
949
+ process.exit(1);
950
+ }
951
+ const noRun = rawArgs.includes('--no-run');
952
+ try {
953
+ const run = mgr.retry(execId, { autoRun: !noRun });
954
+ if (noRun) {
955
+ if (jsonMode) {
956
+ console.log(JSON.stringify({
957
+ success: true,
958
+ exec_id: run.id,
959
+ current_node: run.current_node,
960
+ retry_count: run.retry_count,
961
+ status: run.status,
962
+ }, null, 2));
963
+ }
964
+ else {
965
+ console.log(`✅ Retry transition: ${execId}`);
966
+ console.log(` Resume node: ${run.current_node}`);
967
+ console.log(` Retry count: ${run.retry_count}`);
968
+ console.log(` (state transition only; run "workflow run auto ${execId}" to execute — re-runs the node, safe only if idempotent)`);
969
+ }
970
+ }
971
+ else {
972
+ const result = await mgr.runAuto(run.id);
973
+ reportAutoResult(result, jsonMode);
974
+ if (result.outcome === 'failed')
975
+ process.exit(1);
976
+ }
977
+ }
978
+ catch (e) {
979
+ const msg = e.message;
980
+ if (jsonMode) {
981
+ console.log(JSON.stringify({ success: false, error: msg }));
982
+ }
983
+ else {
984
+ console.error(`❌ ${msg}`);
985
+ }
986
+ process.exit(1);
987
+ }
988
+ break;
989
+ }
812
990
  case 'list': {
813
991
  const runs = mgr.listRuns();
814
992
  if (jsonMode) {
@@ -871,7 +1049,7 @@ async function cmdRun(rest, rawArgs, jsonMode, engine) {
871
1049
  break;
872
1050
  }
873
1051
  default: {
874
- const msg = `Unknown run subcommand: ${runSub || '(none)'}\nUsage: workflow run <start|state|node|auto|reset|list> [args...]`;
1052
+ const msg = `Unknown run subcommand: ${runSub || '(none)'}\nUsage: workflow run <start|state|node|auto|reset|retry|list> [args...]`;
875
1053
  if (jsonMode) {
876
1054
  console.log(JSON.stringify({ success: false, error: msg }));
877
1055
  }
@@ -884,7 +1062,7 @@ async function cmdRun(rest, rawArgs, jsonMode, engine) {
884
1062
  }
885
1063
  function reportAutoResult(result, jsonMode) {
886
1064
  const { run, outcome, reason, executed } = result;
887
- const status = run.status ?? (outcome === 'paused' ? 'paused' : 'running');
1065
+ const status = run.status;
888
1066
  if (jsonMode) {
889
1067
  console.log(JSON.stringify({
890
1068
  success: outcome !== 'failed',
@@ -1024,6 +1202,10 @@ async function main(inputArgs) {
1024
1202
  await cmdRun(rest, args, jsonMode, engine);
1025
1203
  break;
1026
1204
  }
1205
+ case 'repair': {
1206
+ cmdRepair(rest, jsonMode, !!options.apply, !!options['dry-run']);
1207
+ break;
1208
+ }
1027
1209
  case 'usage':
1028
1210
  showDetailedUsage();
1029
1211
  break;