@formigio/fazemos-cli 0.10.54 → 0.10.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5640,6 +5640,94 @@ pipelines
5640
5640
  process.exit(1);
5641
5641
  }
5642
5642
  });
5643
+ // ── OPERATOR-RECOVERY-BUNDLE — `pl reconcile <instanceId>` ──
5644
+ // Idempotent operator-safe healing command (spec §2).
5645
+ // Re-runs the DAG resolver, detects stranded steps (queued/in_progress with
5646
+ // no live execution), gates through canLaunch, re-launches eligible ones,
5647
+ // and returns a structured JSON report (launched, skipped, stalled).
5648
+ //
5649
+ // POST /api/pipeline-instances/:id/reconcile
5650
+ // Auth: owner or admin only
5651
+ //
5652
+ // Distinct from pl rematerialize (P-TOOL-1): rematerialize rebases onto the
5653
+ // TEMPLATE (schema/content-side); reconcile re-launches missing EXECUTIONS on
5654
+ // the instance as-is (runtime-side only). Different jobs kept separate.
5655
+ pipelines
5656
+ .command('reconcile')
5657
+ .description('Idempotent operator recovery: re-runs the DAG resolver, detects stranded steps ' +
5658
+ '(queued/in_progress with no live execution), and re-launches eligible ones. ' +
5659
+ 'Safe to run multiple times — the second call fires nothing if the first succeeded. ' +
5660
+ 'Owner/admin only. Distinct from rematerialize: this heals EXECUTIONS, not the template schema.')
5661
+ .argument('<instanceId>', 'Pipeline instance ID')
5662
+ .option('--json', 'Print the raw API response as JSON (machine-readable)')
5663
+ .action(async (instanceId, opts) => {
5664
+ try {
5665
+ const path = `/api/pipeline-instances/${instanceId}/reconcile`;
5666
+ const data = (await api('POST', path, {}));
5667
+ if (opts.json) {
5668
+ console.log(JSON.stringify(data, null, 2));
5669
+ return;
5670
+ }
5671
+ // Human-readable reconcile report
5672
+ const reconciledAt = data?.reconciled_at ? new Date(data.reconciled_at).toLocaleString() : 'unknown';
5673
+ console.log(chalk.bold(`Reconcile — instance ${instanceId}`));
5674
+ console.log(chalk.gray(` Reconciled at : ${reconciledAt}`));
5675
+ console.log(chalk.gray(` DAG resolver : ${data?.dag_resolver_ran ? 'ran' : 'skipped'}`));
5676
+ console.log(chalk.gray(` Steps verified: ${data?.steps_verified ?? 0}`));
5677
+ console.log();
5678
+ // Launched
5679
+ const launched = Array.isArray(data?.launched) ? data.launched : [];
5680
+ if (launched.length > 0) {
5681
+ console.log(chalk.green(` Launched (${launched.length}):`));
5682
+ for (const s of launched) {
5683
+ console.log(chalk.green(` ✓ ${s.step_name} (${s.step_id}) → execution ${s.execution_id}` +
5684
+ (s.assigned_to ? ` [agent: ${s.assigned_to}]` : '')));
5685
+ }
5686
+ }
5687
+ else {
5688
+ console.log(chalk.gray(' Launched: (none)'));
5689
+ }
5690
+ // Skipped (canLaunch=false due to pause/budget/fan-out)
5691
+ const skipped = Array.isArray(data?.skipped) ? data.skipped : [];
5692
+ if (skipped.length > 0) {
5693
+ console.log(chalk.yellow(`\n Skipped — canLaunch blocked (${skipped.length}):`));
5694
+ for (const s of skipped) {
5695
+ console.log(chalk.yellow(` ⚠ ${s.step_name} (${s.step_id}) [${s.reason}]: ${s.detail}`));
5696
+ }
5697
+ }
5698
+ // Stalled (governance signal — queued, no execution, canLaunch=false for other reason)
5699
+ const stalled = Array.isArray(data?.stalled) ? data.stalled : [];
5700
+ if (stalled.length > 0) {
5701
+ console.log(chalk.red(`\n Stalled — governance signal (${stalled.length}):`));
5702
+ for (const s of stalled) {
5703
+ console.log(chalk.red(` ✗ ${s.step_name} (${s.step_id}) [status: ${s.status}]: ${s.detail}`));
5704
+ }
5705
+ }
5706
+ console.log();
5707
+ if (launched.length === 0 && skipped.length === 0 && stalled.length === 0) {
5708
+ console.log(chalk.green('No stranded steps found — instance is healthy.'));
5709
+ }
5710
+ else {
5711
+ const summary = [];
5712
+ if (launched.length > 0)
5713
+ summary.push(chalk.green(`${launched.length} launched`));
5714
+ if (skipped.length > 0)
5715
+ summary.push(chalk.yellow(`${skipped.length} skipped`));
5716
+ if (stalled.length > 0)
5717
+ summary.push(chalk.red(`${stalled.length} stalled`));
5718
+ console.log(`Summary: ${summary.join(', ')}`);
5719
+ }
5720
+ }
5721
+ catch (err) {
5722
+ // 403 owner/admin gate
5723
+ if (err instanceof ApiError && err.status === 403) {
5724
+ console.error(chalk.red(`Permission denied: ${err.message}`));
5725
+ process.exit(1);
5726
+ }
5727
+ console.error(chalk.red(err.message));
5728
+ process.exit(1);
5729
+ }
5730
+ });
5643
5731
  pipelines
5644
5732
  .command('set-params')
5645
5733
  .description('Set instance parameters (atomic update)')