@crewx/workflow 0.3.22-rc.14 → 0.3.22-rc.140

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 (46) hide show
  1. package/SKILL.md +40 -4
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +266 -9
  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 +68 -2
  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/mermaid.d.ts.map +1 -1
  18. package/dist/src/mermaid.js +7 -4
  19. package/dist/src/mermaid.js.map +1 -1
  20. package/dist/src/parent-task-sync.d.ts +120 -0
  21. package/dist/src/parent-task-sync.d.ts.map +1 -0
  22. package/dist/src/parent-task-sync.js +307 -0
  23. package/dist/src/parent-task-sync.js.map +1 -0
  24. package/dist/src/retry-policy.d.ts +38 -0
  25. package/dist/src/retry-policy.d.ts.map +1 -0
  26. package/dist/src/retry-policy.js +140 -0
  27. package/dist/src/retry-policy.js.map +1 -0
  28. package/dist/src/run-manager.d.ts +35 -2
  29. package/dist/src/run-manager.d.ts.map +1 -1
  30. package/dist/src/run-manager.js +1182 -219
  31. package/dist/src/run-manager.js.map +1 -1
  32. package/dist/src/types.d.ts +39 -4
  33. package/dist/src/types.d.ts.map +1 -1
  34. package/dist/src/types.js +2 -0
  35. package/dist/src/types.js.map +1 -1
  36. package/dist/src/utils/output-format.d.ts +12 -0
  37. package/dist/src/utils/output-format.d.ts.map +1 -1
  38. package/dist/src/utils/output-format.js +103 -5
  39. package/dist/src/utils/output-format.js.map +1 -1
  40. package/dist/src/workflow-persistence.d.ts +48 -0
  41. package/dist/src/workflow-persistence.d.ts.map +1 -0
  42. package/dist/src/workflow-persistence.js +224 -0
  43. package/dist/src/workflow-persistence.js.map +1 -0
  44. package/dist/workflow-schema.json +34 -5
  45. package/package.json +14 -4
  46. package/workflow-schema.json +34 -5
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":";AAkBA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAS9C,OAAO,KAAK,EAAE,cAAc,EAAgB,YAAY,EAA6C,MAAM,aAAa,CAAC;AAqHzH,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;AAmhCD,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
@@ -42,8 +42,12 @@ const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
43
43
  const child_process_1 = require("child_process");
44
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++) {
@@ -120,6 +126,20 @@ function getWorkflowConfig() {
120
126
  return { paths: ['workflows'] };
121
127
  }
122
128
  }
129
+ function getNodeResultStatus(run, nodeId, nodeType, noExec) {
130
+ if (noExec)
131
+ return null;
132
+ const audit = [...(run.audit ?? [])]
133
+ .reverse()
134
+ .find((entry) => entry.node_id === nodeId && entry.type !== 'retry');
135
+ if (!audit)
136
+ return null;
137
+ if (audit.error || nodeType === 'error')
138
+ return 'failed';
139
+ if (nodeType === 'approval' || nodeType === 'parallel')
140
+ return 'paused';
141
+ return 'completed';
142
+ }
123
143
  function validateSlugInput(nameOrPath) {
124
144
  if (!nameOrPath.endsWith('.yaml') && !nameOrPath.endsWith('.yml')) {
125
145
  if (/[/\\]|\.\./.test(nameOrPath)) {
@@ -182,6 +202,11 @@ function loadAllWorkflows(engine, paths) {
182
202
  }
183
203
  return all;
184
204
  }
205
+ function resolveWorkflowSpecForRun(run, engine) {
206
+ const runFile = path.resolve(run.workflow_file);
207
+ const workflows = engine.loadWorkflows(path.dirname(runFile));
208
+ return workflows?.find((workflow) => path.resolve(workflow.filePath) === runFile && workflow.id === run.workflow_id)?.spec;
209
+ }
185
210
  function printUsage() {
186
211
  console.log(`
187
212
  @crewx/workflow@${PKG_VERSION} - Workflow management for CrewX
@@ -199,13 +224,18 @@ Commands:
199
224
  workflow run node <id> <node> Execute node (auto-invoke agent)
200
225
  workflow run auto <id> Auto-run from current node until end/pause
201
226
  workflow run reset <id> Reset execution
227
+ workflow run retry <id> Retry a failed run from the failed node (--no-run: transition only)
228
+ ⚠️ re-executes the failed node; safe only if it is idempotent
202
229
  workflow run list List all executions
230
+ workflow repair <run-id> Preview parent-task repair (use --apply to write)
203
231
  workflow usage Show detailed guide
204
232
 
205
233
  Options:
206
234
  --json JSON output
207
235
  --mermaid Output Mermaid diagram (show only)
208
236
  --no-exec Move cursor only, skip agent execution (run node)
237
+ --dry-run Preview workflow parent repair without writes (default)
238
+ --apply Apply workflow parent repair under the run lock
209
239
  --version Show version
210
240
  --help, -h Show this help
211
241
 
@@ -289,7 +319,7 @@ function cmdList(engine, jsonMode) {
289
319
  }, null, 2));
290
320
  }
291
321
  else {
292
- console.log(`\uD83D\uDCCB \uC6CC\uD06C\uD50C\uB85C \uBAA9\uB85D`);
322
+ console.log('📋 Workflows');
293
323
  workflows.forEach((wf, i) => {
294
324
  const meta = wf.spec.metadata ?? {};
295
325
  const nodeCount = wf.spec.nodes ? Object.keys(wf.spec.nodes).length : 0;
@@ -299,8 +329,8 @@ function cmdList(engine, jsonMode) {
299
329
  const firstId = workflows[0]?.id;
300
330
  if (firstId) {
301
331
  console.log(`\n${'━'.repeat(43)}`);
302
- console.log(`🔍 워크플로 상세: npx workflow show ${firstId}`);
303
- console.log(`🔀 워크플로 실행: npx workflow run start <id> --set key="value"`);
332
+ console.log(`🔍 Workflow details: npx workflow show ${firstId}`);
333
+ console.log(`🔀 Run workflow: npx workflow run start <id> --set key="value"`);
304
334
  }
305
335
  const listCtx = {
306
336
  commandId: 'workflow.list',
@@ -531,10 +561,180 @@ function cmdValidate(engine, target, jsonMode) {
531
561
  if (hasErrors)
532
562
  process.exit(1);
533
563
  }
564
+ function repairChangeForOutput(change) {
565
+ return {
566
+ task_id: change.id,
567
+ kind: change.kind,
568
+ before: {
569
+ status: change.before.status,
570
+ error: change.before.error,
571
+ completed_at: change.before.completedAt,
572
+ duration_ms: change.before.durationMs,
573
+ },
574
+ after: {
575
+ status: change.after.status,
576
+ error: change.after.error,
577
+ completed_at: change.after.completedAt,
578
+ duration_ms: change.after.durationMs,
579
+ },
580
+ reason: change.reason,
581
+ };
582
+ }
583
+ function auditSnapshotChangeForOutput(change) {
584
+ return {
585
+ instance_id: change.instanceId,
586
+ before: change.before,
587
+ after: change.after,
588
+ };
589
+ }
590
+ function reportRepair(preview, mode, jsonMode, projectionErrors = []) {
591
+ const changes = preview.parentChanges.map(repairChangeForOutput);
592
+ const auditCardChanges = preview.auditSnapshotChanges.map(auditSnapshotChangeForOutput);
593
+ const errors = [...preview.errors, ...projectionErrors];
594
+ if (jsonMode) {
595
+ console.log(JSON.stringify({
596
+ success: true,
597
+ exec_id: preview.runId,
598
+ mode,
599
+ dry_run: mode === 'dry-run',
600
+ applied: mode === 'apply',
601
+ run_status: preview.runStatus,
602
+ changes,
603
+ audit_card_changes: auditCardChanges,
604
+ ...(errors.length > 0 ? { errors } : {}),
605
+ }, null, 2));
606
+ return;
607
+ }
608
+ console.log(`🔧 Workflow repair (${mode}): ${preview.runId}`);
609
+ console.log(` Run status: ${preview.runStatus}`);
610
+ if (changes.length === 0 && auditCardChanges.length === 0) {
611
+ console.log(' No parent lifecycle changes needed.');
612
+ }
613
+ else {
614
+ for (const change of changes) {
615
+ console.log(` ${mode === 'apply' ? 'Updated' : 'Would update'} ${change.task_id} (${change.kind}): ${change.before.status} → ${change.after.status}`);
616
+ console.log(` Reason: ${change.reason}`);
617
+ }
618
+ for (const change of auditCardChanges) {
619
+ console.log(` ${mode === 'apply' ? 'Updated' : 'Would update'} audit card ${change.instance_id}: ${change.before.phase ?? '(unknown)'} → ${change.after.phase}`);
620
+ }
621
+ }
622
+ for (const error of errors) {
623
+ console.error(` Warning: ${error}`);
624
+ }
625
+ }
626
+ function validateRepairRun(run, execId) {
627
+ if (run.id !== execId) {
628
+ throw new Error(`Execution file ID mismatch: requested ${execId}, found ${run.id || '(missing)'}`);
629
+ }
630
+ if (!run.workflow_id || !run.workflow_file || !run.created_at || !run.updated_at
631
+ || typeof run.current_node !== 'string'
632
+ || !Array.isArray(run.completed_nodes)
633
+ || !run.state || typeof run.state !== 'object'
634
+ || !run.initial_state || typeof run.initial_state !== 'object') {
635
+ throw new Error(`Run "${execId}" is incomplete and cannot be repaired`);
636
+ }
637
+ }
638
+ function cmdRepair(rest, jsonMode, apply, dryRun) {
639
+ const execId = rest[0];
640
+ if (!execId) {
641
+ const msg = 'Usage: workflow repair <run-id> [--dry-run|--apply]';
642
+ if (jsonMode)
643
+ console.log(JSON.stringify({ success: false, error: msg }));
644
+ else
645
+ console.error(`❌ ${msg}`);
646
+ process.exit(1);
647
+ }
648
+ if (apply && dryRun) {
649
+ const msg = 'Choose either --dry-run or --apply, not both';
650
+ if (jsonMode)
651
+ console.log(JSON.stringify({ success: false, error: msg }));
652
+ else
653
+ console.error(`❌ ${msg}`);
654
+ process.exit(1);
655
+ }
656
+ const projectRoot = getProjectRoot();
657
+ const workspaceId = (0, sdk_1.hashWorkspaceId)(projectRoot);
658
+ const taskRepo = new repository_1.TaskRepository();
659
+ const mgr = new run_manager_1.RunManager(undefined, projectRoot);
660
+ const engine = new engine_1.WorkflowEngine();
661
+ let uiInstanceStore;
662
+ let readOnlyUiInstanceStore;
663
+ const resolveWorkflowSpec = (run) => resolveWorkflowSpecForRun(run, engine);
664
+ const getUiInstanceStore = () => {
665
+ uiInstanceStore ?? (uiInstanceStore = new sdk_1.UiInstanceStore(taskRepo.resolveDbPath()));
666
+ return uiInstanceStore;
667
+ };
668
+ const getReadOnlyUiInstanceStore = () => {
669
+ readOnlyUiInstanceStore ?? (readOnlyUiInstanceStore = (0, sdk_1.openUiInstanceReadStore)(taskRepo.resolveDbPath()));
670
+ return readOnlyUiInstanceStore;
671
+ };
672
+ const persistenceProjection = (0, workflow_persistence_1.createWorkflowPersistenceProjection)({
673
+ taskRepo,
674
+ workspaceId,
675
+ resolveWorkflowSpec,
676
+ getUiInstanceStore,
677
+ });
678
+ const persistencePreviewOptions = {
679
+ taskRepo,
680
+ workspaceId,
681
+ resolveWorkflowSpec,
682
+ getUiInstanceStore: getReadOnlyUiInstanceStore,
683
+ };
684
+ try {
685
+ const observed = mgr.readRun(execId);
686
+ if (!observed)
687
+ throw new Error(`Execution not found: ${execId}`);
688
+ validateRepairRun(observed, execId);
689
+ if (!(0, parent_task_sync_1.deriveWorkflowParentLifecycle)(observed)) {
690
+ throw new Error(`Run "${execId}" has an unsupported or insufficient persisted status: ${observed.status ?? 'pending'}`);
691
+ }
692
+ if (!apply) {
693
+ reportRepair((0, workflow_persistence_1.previewWorkflowPersistenceProjection)(persistencePreviewOptions, observed), 'dry-run', jsonMode);
694
+ return;
695
+ }
696
+ const result = mgr.withRunLock(execId, (latest) => {
697
+ validateRepairRun(latest, execId);
698
+ if (!(0, parent_task_sync_1.deriveWorkflowParentLifecycle)(latest)) {
699
+ throw new Error(`Run "${execId}" has an unsupported or insufficient persisted status: ${latest.status ?? 'pending'}`);
700
+ }
701
+ const preview = (0, workflow_persistence_1.previewWorkflowPersistenceProjection)(persistencePreviewOptions, latest);
702
+ const projection = persistenceProjection(latest);
703
+ return { preview, projection };
704
+ });
705
+ reportRepair(result.preview, 'apply', jsonMode, result.projection.errors);
706
+ }
707
+ catch (e) {
708
+ const msg = e instanceof Error ? e.message : String(e);
709
+ if (jsonMode)
710
+ console.log(JSON.stringify({ success: false, error: msg }));
711
+ else
712
+ console.error(`❌ ${msg}`);
713
+ process.exit(1);
714
+ }
715
+ finally {
716
+ uiInstanceStore?.close();
717
+ readOnlyUiInstanceStore?.close();
718
+ }
719
+ }
534
720
  async function cmdRun(rest, rawArgs, jsonMode, engine) {
535
721
  const runSub = rest[0];
536
722
  const runArgs = rest.slice(1);
537
- const mgr = new run_manager_1.RunManager();
723
+ const projectRoot = getProjectRoot();
724
+ const taskRepo = new repository_1.TaskRepository();
725
+ let uiInstanceStore;
726
+ const persistenceProjection = (0, workflow_persistence_1.createWorkflowPersistenceProjection)({
727
+ taskRepo,
728
+ workspaceId: (0, sdk_1.hashWorkspaceId)(projectRoot),
729
+ resolveWorkflowSpec: (run) => resolveWorkflowSpecForRun(run, engine),
730
+ getUiInstanceStore: () => {
731
+ uiInstanceStore ?? (uiInstanceStore = new sdk_1.UiInstanceStore(taskRepo.resolveDbPath()));
732
+ return uiInstanceStore;
733
+ },
734
+ });
735
+ const mgr = new run_manager_1.RunManager(undefined, projectRoot, {
736
+ onRunPersisted: persistenceProjection,
737
+ });
538
738
  const { paths } = getWorkflowConfig();
539
739
  switch (runSub) {
540
740
  case 'start': {
@@ -707,6 +907,7 @@ async function cmdRun(rest, rawArgs, jsonMode, engine) {
707
907
  const run = noExec
708
908
  ? mgr.moveNode(execId, nodeId)
709
909
  : await mgr.executeNode(execId, nodeId);
910
+ const nodeStatus = getNodeResultStatus(run, nodeId, mgr.getNodeType(run.workflow_file, run.workflow_id, nodeId), noExec);
710
911
  if (jsonMode) {
711
912
  console.log(JSON.stringify({
712
913
  success: true,
@@ -726,8 +927,8 @@ async function cmdRun(rest, rawArgs, jsonMode, engine) {
726
927
  console.log(` Completed: ${run.completed_nodes.join(' \u2192 ')}`);
727
928
  }
728
929
  console.log(` Current: ${run.current_node}`);
729
- if (run.status) {
730
- console.log(` Status: ${run.status}`);
930
+ if (nodeStatus) {
931
+ console.log(` Status: ${nodeStatus}`);
731
932
  }
732
933
  }
733
934
  let nextNode = null;
@@ -809,6 +1010,58 @@ async function cmdRun(rest, rawArgs, jsonMode, engine) {
809
1010
  }
810
1011
  break;
811
1012
  }
1013
+ case 'retry': {
1014
+ const positional = runArgs.filter(a => !a.startsWith('--'));
1015
+ const execId = positional[0];
1016
+ if (!execId) {
1017
+ const msg = 'Usage: workflow run retry <exec-id> [--no-run]';
1018
+ if (jsonMode) {
1019
+ console.log(JSON.stringify({ success: false, error: msg }));
1020
+ }
1021
+ else {
1022
+ console.error(`❌ ${msg}`);
1023
+ }
1024
+ process.exit(1);
1025
+ }
1026
+ const noRun = rawArgs.includes('--no-run');
1027
+ try {
1028
+ const run = mgr.retry(execId, { autoRun: !noRun });
1029
+ if (noRun) {
1030
+ if (jsonMode) {
1031
+ console.log(JSON.stringify({
1032
+ success: true,
1033
+ exec_id: run.id,
1034
+ current_node: run.current_node,
1035
+ retry_count: run.retry_count,
1036
+ status: run.status,
1037
+ }, null, 2));
1038
+ }
1039
+ else {
1040
+ console.log(`✅ Retry transition: ${execId}`);
1041
+ console.log(` Resume node: ${run.current_node}`);
1042
+ console.log(` Retry count: ${run.retry_count}`);
1043
+ console.log(` (state transition only; run "workflow run auto ${execId}" to execute — re-runs the node, safe only if idempotent)`);
1044
+ }
1045
+ }
1046
+ else {
1047
+ const result = await mgr.runAuto(run.id);
1048
+ reportAutoResult(result, jsonMode);
1049
+ if (result.outcome === 'failed')
1050
+ process.exit(1);
1051
+ }
1052
+ }
1053
+ catch (e) {
1054
+ const msg = e.message;
1055
+ if (jsonMode) {
1056
+ console.log(JSON.stringify({ success: false, error: msg }));
1057
+ }
1058
+ else {
1059
+ console.error(`❌ ${msg}`);
1060
+ }
1061
+ process.exit(1);
1062
+ }
1063
+ break;
1064
+ }
812
1065
  case 'list': {
813
1066
  const runs = mgr.listRuns();
814
1067
  if (jsonMode) {
@@ -871,7 +1124,7 @@ async function cmdRun(rest, rawArgs, jsonMode, engine) {
871
1124
  break;
872
1125
  }
873
1126
  default: {
874
- const msg = `Unknown run subcommand: ${runSub || '(none)'}\nUsage: workflow run <start|state|node|auto|reset|list> [args...]`;
1127
+ const msg = `Unknown run subcommand: ${runSub || '(none)'}\nUsage: workflow run <start|state|node|auto|reset|retry|list> [args...]`;
875
1128
  if (jsonMode) {
876
1129
  console.log(JSON.stringify({ success: false, error: msg }));
877
1130
  }
@@ -884,7 +1137,7 @@ async function cmdRun(rest, rawArgs, jsonMode, engine) {
884
1137
  }
885
1138
  function reportAutoResult(result, jsonMode) {
886
1139
  const { run, outcome, reason, executed } = result;
887
- const status = run.status ?? (outcome === 'paused' ? 'paused' : 'running');
1140
+ const status = run.status;
888
1141
  if (jsonMode) {
889
1142
  console.log(JSON.stringify({
890
1143
  success: outcome !== 'failed',
@@ -1024,6 +1277,10 @@ async function main(inputArgs) {
1024
1277
  await cmdRun(rest, args, jsonMode, engine);
1025
1278
  break;
1026
1279
  }
1280
+ case 'repair': {
1281
+ cmdRepair(rest, jsonMode, !!options.apply, !!options['dry-run']);
1282
+ break;
1283
+ }
1027
1284
  case 'usage':
1028
1285
  showDetailedUsage();
1029
1286
  break;