kiro-spec-engine 1.47.0 → 1.47.1

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/CHANGELOG.md CHANGED
@@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
32
32
  - **Spec growth/duplicate guardrails**: Added `--spec-session-max-created`, `--spec-session-max-created-per-goal`, and `--spec-session-max-duplicate-goals` (with hard-fail option) plus summary telemetry (`goal_input_guard`, `spec_session_growth_guard`) to reduce runaway autonomous portfolio expansion.
33
33
  - **Autonomous KPI trend command**: Added `kse auto kpi trend` to aggregate weekly success/completion, failure, sub-spec, and spec-growth telemetry from persisted autonomous summary sessions.
34
34
  - **Autonomous KPI trend period/csv/anomaly enhancement**: Extended `kse auto kpi trend` with `--period week|day`, `--csv` export mode, and JSON anomaly diagnostics (`anomaly_detection`, `anomalies`) for latest-period regression checks.
35
+ - **Program governance stabilization loop**: Added `close-loop-program` governance controls (`--program-govern-until-stable`, `--program-govern-max-rounds`, `--program-govern-max-minutes`, anomaly knobs, `--program-govern-use-action`, `--no-program-govern-auto-action`) so gate/anomaly failures can trigger bounded replay/recover rounds with remediation action execution until stable, with `program_governance`, `program_kpi_trend`, and `program_kpi_anomalies` telemetry.
35
36
  - **Close-loop multi-goal batch command**: Added `kse auto close-loop-batch <goals-file>` with file-format autodetect (`json|lines`), `--continue-on-error`, and per-goal summary output so autonomous master/sub execution can scale across multiple goals in one run.
36
37
  - **Close-loop batch global scheduler**: Added `--batch-parallel` (`1-20`) to execute multiple goals concurrently in `close-loop-batch`, enabling master/sub portfolios to progress in parallel without manual orchestration handoffs.
37
38
  - **Close-loop batch resume from summary**: Added `--resume-from-summary <path>` to recover pending goals from a prior batch run and continue autonomous delivery without rebuilding the entire goal queue manually.
package/README.md CHANGED
@@ -531,6 +531,7 @@ kse create-spec <name> # Legacy: create empty Spec folder only
531
531
  # Autonomous close-loop program (one command, no confirmation loop)
532
532
  kse auto close-loop "<goal>" # Auto split goal into master/sub specs and execute to closure
533
533
  kse auto close-loop "<goal>" --dry-run --json # Preview decomposition plan only
534
+ kse auto close-loop-program "<goal>" --program-govern-until-stable --program-govern-use-action 1 --json # Program-level recovery + governance with remediation action execution
534
535
 
535
536
  # Spec workflow (recommended)
536
537
  kse spec bootstrap --name <spec> --non-interactive # Generate requirements/design/tasks draft
package/README.zh.md CHANGED
@@ -448,6 +448,7 @@ kse create-spec <name> # 兼容旧版:仅创建空 Spec 目录
448
448
  # 自动闭环主从编排(单命令)
449
449
  kse auto close-loop "<目标>" # 自动拆分 Master/Sub Spec 并推进到完成态
450
450
  kse auto close-loop "<目标>" --dry-run --json # 仅预览拆分与依赖计划
451
+ kse auto close-loop-program "<目标>" --program-govern-until-stable --program-govern-use-action 1 --json # 程序级自动恢复 + 治理循环(含 remediation action 执行)直到稳定
451
452
 
452
453
  # Spec 工作流(推荐)
453
454
  kse spec bootstrap --name <spec> --non-interactive # 生成 requirements/design/tasks 初稿
@@ -300,6 +300,15 @@ Close-loop recovery command:
300
300
  - `--program-audit-out <path>` exports full recovery/program audit JSON.
301
301
  - Output includes `recovered_from_summary`, `recovery_plan` (including `selection_explain`), `recovery_cycle` (with elapsed/budget metadata), and `recovery_memory` (including scope + selection explanation) for full auditability of applied strategy changes.
302
302
 
303
+ Program governance loop (for `close-loop-program`):
304
+ - `--program-govern-until-stable` enables bounded post-run governance rounds.
305
+ - `--program-govern-max-rounds <n>` and `--program-govern-max-minutes <n>` bound governance loop cost.
306
+ - `--program-govern-anomaly-weeks <n>` + `--program-govern-anomaly-period <week|day>` make governance anomaly-aware using autonomous KPI trend.
307
+ - `--no-program-govern-anomaly` limits governance trigger to gate/budget failures only.
308
+ - `--program-govern-use-action <n>` pins remediation action index for governance rounds.
309
+ - `--no-program-govern-auto-action` disables automatic remediation action selection/execution inside governance.
310
+ - Output includes `program_governance`, `program_kpi_trend`, and `program_kpi_anomalies` for traceable autonomous stabilization history.
311
+
303
312
  KPI trend command:
304
313
  - `kse auto kpi trend --weeks <n> --mode <all|batch|program|recover> --period <week|day> --json` aggregates periodic success/completion, failure, sub-spec, and spec-growth telemetry from persisted autonomous session summaries.
305
314
  - Add `--csv` to print/export trend buckets as CSV (`--out` writes CSV when `--csv` is enabled).
@@ -337,6 +337,9 @@ kse auto close-loop-program \
337
337
  --program-gate-fallback-profile prod \
338
338
  --program-min-success-rate 95 \
339
339
  --program-max-risk-level medium \
340
+ --program-govern-until-stable \
341
+ --program-govern-max-rounds 3 \
342
+ --program-govern-use-action 1 \
340
343
  --program-kpi-out .kiro/reports/close-loop-program-kpi.json \
341
344
  --program-audit-out .kiro/reports/close-loop-program-audit.json \
342
345
  --json
@@ -521,6 +524,14 @@ Close-loop program (`kse auto close-loop-program "<goal>"`) options:
521
524
  - `--program-max-agent-budget <n>`: convergence gate max allowed agent budget/effective parallel budget (`1-500`)
522
525
  - `--program-max-total-sub-specs <n>`: convergence gate max total sub-specs across program goals (`1-500000`)
523
526
  - `--no-program-gate-auto-remediate`: disable automatic remediation hints/prune attempts after gate failure
527
+ - `--program-govern-until-stable`: enable post-run governance loop that keeps replaying/recovering until gate/anomaly stability
528
+ - `--program-govern-max-rounds <n>`: max governance rounds (`1-20`, default `3`)
529
+ - `--program-govern-max-minutes <n>`: elapsed-time budget for governance loop (`1-10080`, default `60`)
530
+ - `--program-govern-anomaly-weeks <n>`: KPI lookback weeks for anomaly-triggered governance (`1-260`, default `8`)
531
+ - `--program-govern-anomaly-period <week|day>`: KPI bucket period for anomaly governance checks (default `week`)
532
+ - `--no-program-govern-anomaly`: disable anomaly-triggered governance and only govern by gate/budget failures
533
+ - `--program-govern-use-action <n>`: pin remediation action index (`1-20`) used in governance rounds
534
+ - `--no-program-govern-auto-action`: disable automatic remediation action selection/execution in governance rounds
524
535
  - `--program-min-quality-score <n>`: minimum semantic decomposition quality score before automatic refinement (`0-100`, default `70`)
525
536
  - `--program-quality-gate`: fail run when final decomposition quality remains below `--program-min-quality-score`
526
537
  - `--recovery-memory-scope <scope>`: scope key for recovery memory isolation (default auto: project + git branch)
@@ -530,6 +541,10 @@ Close-loop program (`kse auto close-loop-program "<goal>"`) options:
530
541
  - Program summary includes `program_kpi`, `program_gate`, `program_gate_fallbacks`, `program_gate_effective`, and optional `program_kpi_file` / `program_audit_file` for portfolio-level observability pipelines.
531
542
  - `program_gate` now supports unified budget checks (success/risk + elapsed time + agent budget + total sub-spec ceiling).
532
543
  - On gate/budget failure, summary can include `program_gate_auto_remediation` with auto patch/prune actions.
544
+ - With `--program-govern-until-stable`, summary additionally includes:
545
+ - `program_governance` (round history, stop reason, exhausted/converged state)
546
+ - `program_governance` includes action-selection metadata (`auto_action_enabled`, `action_selection_enabled`, `pinned_action_index`, per-round `selected_action*`).
547
+ - `program_kpi_trend` and `program_kpi_anomalies` (anomaly-aware governance context)
533
548
  - Program summary includes `program_diagnostics` with `failure_clusters` and `remediation_actions` (prioritized follow-up commands for convergence).
534
549
  - Program summary includes `program_coordination` (master/sub topology, unresolved goal indexes, scheduler snapshot) and `auto_recovery` metadata.
535
550
 
@@ -416,6 +416,14 @@ function registerAutoCommands(program) {
416
416
  .option('--program-max-agent-budget <n>', 'Program convergence gate: maximum allowed agent budget/effective parallel budget', parseInt)
417
417
  .option('--program-max-total-sub-specs <n>', 'Program convergence gate: maximum total sub-specs generated across program goals', parseInt)
418
418
  .option('--no-program-gate-auto-remediate', 'Disable automatic remediation patch/prune suggestions after gate failure')
419
+ .option('--program-govern-until-stable', 'Enable post-run governance loop until gate/anomaly stability is reached')
420
+ .option('--program-govern-max-rounds <n>', 'Max governance rounds when --program-govern-until-stable is enabled (default: 3)', parseInt)
421
+ .option('--program-govern-max-minutes <n>', 'Max elapsed minutes for governance loop (default: 60)', parseInt)
422
+ .option('--program-govern-anomaly-weeks <n>', 'KPI trend lookback weeks for governance anomaly checks (default: 8)', parseInt)
423
+ .option('--program-govern-anomaly-period <period>', 'KPI trend period for governance anomaly checks: week|day', 'week')
424
+ .option('--no-program-govern-anomaly', 'Disable anomaly-triggered governance decisions (gate-only)')
425
+ .option('--program-govern-use-action <n>', 'Pinned remediation action index used by governance rounds (default: memory/default)', parseInt)
426
+ .option('--no-program-govern-auto-action', 'Disable automatic remediation action selection/execution inside governance loop')
419
427
  .option('--recovery-memory-scope <scope>', 'Recovery memory scope key (default: auto: project + git branch)')
420
428
  .option('--program-audit-out <path>', 'Write program audit JSON with recovery and coordination trace')
421
429
  .option('--program-kpi-out <path>', 'Write program KPI snapshot JSON to file')
@@ -429,6 +437,14 @@ function registerAutoCommands(program) {
429
437
  const programRecoverMaxRounds = normalizeRecoverMaxRounds(options.programRecoverMaxRounds);
430
438
  const programRecoverMaxMinutes = normalizeRecoverMaxMinutes(options.programRecoverMaxMinutes, '--program-recover-max-minutes');
431
439
  const programRecoverResumeStrategy = normalizeResumeStrategy(options.programRecoverResumeStrategy);
440
+ const programGovernUntilStable = Boolean(options.programGovernUntilStable);
441
+ const programGovernMaxRounds = normalizeProgramGovernMaxRounds(options.programGovernMaxRounds);
442
+ const programGovernMaxMinutes = normalizeProgramGovernMaxMinutes(options.programGovernMaxMinutes);
443
+ const programGovernAnomalyEnabled = options.programGovernAnomaly !== false;
444
+ const programGovernAnomalyWeeks = normalizeProgramGovernAnomalyWeeks(options.programGovernAnomalyWeeks);
445
+ const programGovernAnomalyPeriod = normalizeAutoKpiTrendPeriod(options.programGovernAnomalyPeriod);
446
+ const programGovernUseAction = normalizeProgramGovernUseAction(options.programGovernUseAction);
447
+ const programGovernAutoActionEnabled = options.programGovernAutoAction !== false;
432
448
  const programGatePolicy = resolveProgramGatePolicy({
433
449
  profile: options.programGateProfile,
434
450
  minSuccessRate: options.programMinSuccessRate,
@@ -559,60 +575,70 @@ function registerAutoCommands(program) {
559
575
  summary.program_completed_at = new Date(programCompletedAt).toISOString();
560
576
  summary.program_elapsed_ms = Math.max(0, programCompletedAt - programStartedAt);
561
577
 
562
- summary.program_gate = evaluateProgramConvergenceGate(summary, {
563
- profile: programGatePolicy.profile,
564
- minSuccessRate: programGatePolicy.minSuccessRate,
565
- maxRiskLevel: programGatePolicy.maxRiskLevel,
566
- maxElapsedMinutes: programGatePolicy.maxElapsedMinutes,
567
- maxAgentBudget: programGatePolicy.maxAgentBudget,
568
- maxTotalSubSpecs: programGatePolicy.maxTotalSubSpecs
578
+ await applyProgramGateOutcome(summary, {
579
+ projectPath,
580
+ options: programOptions,
581
+ programGatePolicy,
582
+ gateFallbackChain,
583
+ enableAutoRemediation: options.programGateAutoRemediate !== false
569
584
  });
570
- let effectiveGatePassed = summary.program_gate.passed;
571
- let effectiveGateSource = 'primary';
572
- let matchedFallbackProfile = null;
573
- summary.program_gate_fallbacks = [];
574
- if (!effectiveGatePassed && gateFallbackChain.length > 0) {
575
- for (const fallbackProfile of gateFallbackChain) {
576
- const fallbackResult = evaluateProgramConvergenceGate(summary, {
577
- profile: fallbackProfile,
578
- maxElapsedMinutes: programGatePolicy.maxElapsedMinutes,
579
- maxAgentBudget: programGatePolicy.maxAgentBudget,
580
- maxTotalSubSpecs: programGatePolicy.maxTotalSubSpecs
581
- });
582
- summary.program_gate_fallbacks.push(fallbackResult);
583
- if (fallbackResult.passed) {
584
- effectiveGatePassed = true;
585
- effectiveGateSource = 'fallback-chain';
586
- matchedFallbackProfile = fallbackProfile;
587
- break;
588
- }
589
- }
590
- }
591
- summary.program_gate_fallback = summary.program_gate_fallbacks.length > 0
592
- ? summary.program_gate_fallbacks[0]
593
- : null;
594
- summary.program_gate_effective = {
595
- passed: effectiveGatePassed,
596
- source: effectiveGateSource,
597
- primary_passed: Boolean(summary.program_gate && summary.program_gate.passed),
598
- fallback_profile: matchedFallbackProfile,
599
- fallback_chain: gateFallbackChain,
600
- fallback_passed: matchedFallbackProfile !== null,
601
- attempted_fallback_count: summary.program_gate_fallbacks.length
602
- };
603
- if (
604
- options.programGateAutoRemediate !== false &&
605
- (
606
- !summary.program_gate_effective.passed ||
607
- isSpecSessionBudgetHardFailure(summary) ||
608
- isSpecSessionGrowthGuardHardFailure(summary)
609
- )
610
- ) {
611
- summary.program_gate_auto_remediation = await applyProgramGateAutoRemediation(summary, {
585
+
586
+ if (programGovernUntilStable) {
587
+ const governanceResult = await runProgramGovernanceLoop({
588
+ enabled: true,
589
+ summary,
612
590
  projectPath,
613
- options: programOptions
591
+ programOptions,
592
+ baseGoalsResult: goalsResult,
593
+ maxRounds: programGovernMaxRounds,
594
+ maxMinutes: programGovernMaxMinutes,
595
+ anomalyEnabled: programGovernAnomalyEnabled,
596
+ anomalyWeeks: programGovernAnomalyWeeks,
597
+ anomalyPeriod: programGovernAnomalyPeriod,
598
+ programGatePolicy,
599
+ gateFallbackChain,
600
+ recoveryMemoryScope,
601
+ recoverResumeStrategy: programRecoverResumeStrategy,
602
+ recoverMaxRounds: programRecoverMaxRounds,
603
+ recoverMaxMinutes: programRecoverMaxMinutes,
604
+ programRecoverUseAction: options.programRecoverUseAction,
605
+ programGateAutoRemediate: options.programGateAutoRemediate !== false,
606
+ governUseAction: programGovernUseAction,
607
+ governAutoActionEnabled: programGovernAutoActionEnabled
614
608
  });
609
+ summary = governanceResult.summary;
610
+ summary.program_governance = governanceResult.governance;
611
+ } else {
612
+ summary.program_governance = {
613
+ enabled: false,
614
+ anomaly_enabled: programGovernAnomalyEnabled,
615
+ anomaly_weeks: programGovernAnomalyWeeks,
616
+ anomaly_period: programGovernAnomalyPeriod,
617
+ auto_action_enabled: programGovernAutoActionEnabled,
618
+ action_selection_enabled: false,
619
+ pinned_action_index: programGovernUseAction,
620
+ max_rounds: programGovernMaxRounds,
621
+ max_minutes: programGovernMaxMinutes,
622
+ performed_rounds: 0,
623
+ converged: Boolean(
624
+ summary &&
625
+ summary.program_gate_effective &&
626
+ summary.program_gate_effective.passed &&
627
+ !isSpecSessionBudgetHardFailure(summary) &&
628
+ !isSpecSessionGrowthGuardHardFailure(summary)
629
+ ),
630
+ exhausted: false,
631
+ stop_reason: 'disabled',
632
+ history: []
633
+ };
615
634
  }
635
+
636
+ const finalProgramCompletedAt = Date.now();
637
+ summary.program_completed_at = new Date(finalProgramCompletedAt).toISOString();
638
+ summary.program_elapsed_ms = Math.max(0, finalProgramCompletedAt - programStartedAt);
639
+
640
+ await maybeWriteProgramKpi(summary, options.programKpiOut, projectPath);
641
+ await maybeWriteOutput(summary, options.out, projectPath);
616
642
  await maybeWriteProgramAudit(summary, options.programAuditOut, projectPath);
617
643
 
618
644
  printCloseLoopBatchSummary(summary, programOptions);
@@ -787,60 +813,13 @@ function registerAutoCommands(program) {
787
813
  recoveryResult.summary.program_started_at = new Date(recoveryStartedAt).toISOString();
788
814
  recoveryResult.summary.program_completed_at = new Date(recoveryCompletedAt).toISOString();
789
815
  recoveryResult.summary.program_elapsed_ms = Math.max(0, recoveryCompletedAt - recoveryStartedAt);
790
- recoveryResult.summary.program_gate = evaluateProgramConvergenceGate(recoveryResult.summary, {
791
- profile: programGatePolicy.profile,
792
- minSuccessRate: programGatePolicy.minSuccessRate,
793
- maxRiskLevel: programGatePolicy.maxRiskLevel,
794
- maxElapsedMinutes: programGatePolicy.maxElapsedMinutes,
795
- maxAgentBudget: programGatePolicy.maxAgentBudget,
796
- maxTotalSubSpecs: programGatePolicy.maxTotalSubSpecs
816
+ await applyProgramGateOutcome(recoveryResult.summary, {
817
+ projectPath,
818
+ options,
819
+ programGatePolicy,
820
+ gateFallbackChain,
821
+ enableAutoRemediation: options.programGateAutoRemediate !== false
797
822
  });
798
- let effectiveRecoveryGatePassed = recoveryResult.summary.program_gate.passed;
799
- let effectiveRecoveryGateSource = 'primary';
800
- let matchedRecoveryFallbackProfile = null;
801
- recoveryResult.summary.program_gate_fallbacks = [];
802
- if (!effectiveRecoveryGatePassed && gateFallbackChain.length > 0) {
803
- for (const fallbackProfile of gateFallbackChain) {
804
- const fallbackResult = evaluateProgramConvergenceGate(recoveryResult.summary, {
805
- profile: fallbackProfile,
806
- maxElapsedMinutes: programGatePolicy.maxElapsedMinutes,
807
- maxAgentBudget: programGatePolicy.maxAgentBudget,
808
- maxTotalSubSpecs: programGatePolicy.maxTotalSubSpecs
809
- });
810
- recoveryResult.summary.program_gate_fallbacks.push(fallbackResult);
811
- if (fallbackResult.passed) {
812
- effectiveRecoveryGatePassed = true;
813
- effectiveRecoveryGateSource = 'fallback-chain';
814
- matchedRecoveryFallbackProfile = fallbackProfile;
815
- break;
816
- }
817
- }
818
- }
819
- recoveryResult.summary.program_gate_fallback = recoveryResult.summary.program_gate_fallbacks.length > 0
820
- ? recoveryResult.summary.program_gate_fallbacks[0]
821
- : null;
822
- recoveryResult.summary.program_gate_effective = {
823
- passed: effectiveRecoveryGatePassed,
824
- source: effectiveRecoveryGateSource,
825
- primary_passed: Boolean(recoveryResult.summary.program_gate && recoveryResult.summary.program_gate.passed),
826
- fallback_profile: matchedRecoveryFallbackProfile,
827
- fallback_chain: gateFallbackChain,
828
- fallback_passed: matchedRecoveryFallbackProfile !== null,
829
- attempted_fallback_count: recoveryResult.summary.program_gate_fallbacks.length
830
- };
831
- if (
832
- options.programGateAutoRemediate !== false &&
833
- (
834
- !recoveryResult.summary.program_gate_effective.passed ||
835
- isSpecSessionBudgetHardFailure(recoveryResult.summary) ||
836
- isSpecSessionGrowthGuardHardFailure(recoveryResult.summary)
837
- )
838
- ) {
839
- recoveryResult.summary.program_gate_auto_remediation = await applyProgramGateAutoRemediation(recoveryResult.summary, {
840
- projectPath,
841
- options
842
- });
843
- }
844
823
  await maybeWriteProgramAudit(recoveryResult.summary, options.programAuditOut, projectPath);
845
824
 
846
825
  printCloseLoopBatchSummary(recoveryResult.summary, recoveryResult.options || options);
@@ -1807,7 +1786,7 @@ async function loadCloseLoopBatchSummaryPayload(projectPath, summaryCandidate) {
1807
1786
  };
1808
1787
  }
1809
1788
 
1810
- function normalizeRecoveryActionIndex(actionCandidate, maxActions) {
1789
+ function normalizeRecoveryActionIndex(actionCandidate, maxActions, optionLabel = '--use-action') {
1811
1790
  if (!Number.isInteger(maxActions) || maxActions <= 0) {
1812
1791
  return 1;
1813
1792
  }
@@ -1819,10 +1798,10 @@ function normalizeRecoveryActionIndex(actionCandidate, maxActions) {
1819
1798
  const parsed = Number(actionCandidate);
1820
1799
  const upperBound = Math.max(20, maxActions);
1821
1800
  if (!Number.isInteger(parsed) || parsed < 1 || parsed > upperBound) {
1822
- throw new Error(`--use-action must be an integer between 1 and ${upperBound}.`);
1801
+ throw new Error(`${optionLabel} must be an integer between 1 and ${upperBound}.`);
1823
1802
  }
1824
1803
  if (parsed > maxActions) {
1825
- throw new Error(`--use-action ${parsed} is out of range. Available remediation actions: 1-${maxActions}.`);
1804
+ throw new Error(`${optionLabel} ${parsed} is out of range. Available remediation actions: 1-${maxActions}.`);
1826
1805
  }
1827
1806
  return parsed;
1828
1807
  }
@@ -2359,16 +2338,19 @@ function resolveRecoveryActionSelection(summaryPayload, actionCandidate, context
2359
2338
  const availableActions = Array.isArray(diagnostics.remediation_actions) && diagnostics.remediation_actions.length > 0
2360
2339
  ? diagnostics.remediation_actions
2361
2340
  : buildProgramRemediationActions(summaryPayload || {}, []);
2341
+ const optionLabel = typeof context.optionLabel === 'string' && context.optionLabel.trim()
2342
+ ? context.optionLabel.trim()
2343
+ : '--use-action';
2362
2344
  let selectedIndex = null;
2363
2345
  let selectionSource = 'default';
2364
2346
  let memorySelection = null;
2365
2347
  let selectionExplain = null;
2366
2348
  if (actionCandidate !== undefined && actionCandidate !== null) {
2367
- selectedIndex = normalizeRecoveryActionIndex(actionCandidate, availableActions.length);
2349
+ selectedIndex = normalizeRecoveryActionIndex(actionCandidate, availableActions.length, optionLabel);
2368
2350
  selectionSource = 'explicit';
2369
2351
  selectionExplain = {
2370
2352
  mode: 'explicit',
2371
- reason: '--use-action provided',
2353
+ reason: `${optionLabel} provided`,
2372
2354
  selected_index: selectedIndex
2373
2355
  };
2374
2356
  } else {
@@ -2884,6 +2866,50 @@ function normalizeProgramMaxTotalSubSpecs(countCandidate) {
2884
2866
  return parsed;
2885
2867
  }
2886
2868
 
2869
+ function normalizeProgramGovernMaxRounds(roundsCandidate) {
2870
+ if (roundsCandidate === undefined || roundsCandidate === null) {
2871
+ return 3;
2872
+ }
2873
+ const parsed = Number(roundsCandidate);
2874
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 20) {
2875
+ throw new Error('--program-govern-max-rounds must be an integer between 1 and 20.');
2876
+ }
2877
+ return parsed;
2878
+ }
2879
+
2880
+ function normalizeProgramGovernMaxMinutes(minutesCandidate) {
2881
+ if (minutesCandidate === undefined || minutesCandidate === null) {
2882
+ return 60;
2883
+ }
2884
+ const parsed = Number(minutesCandidate);
2885
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 10080) {
2886
+ throw new Error('--program-govern-max-minutes must be an integer between 1 and 10080.');
2887
+ }
2888
+ return parsed;
2889
+ }
2890
+
2891
+ function normalizeProgramGovernAnomalyWeeks(weeksCandidate) {
2892
+ if (weeksCandidate === undefined || weeksCandidate === null) {
2893
+ return 8;
2894
+ }
2895
+ const parsed = Number(weeksCandidate);
2896
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 260) {
2897
+ throw new Error('--program-govern-anomaly-weeks must be an integer between 1 and 260.');
2898
+ }
2899
+ return parsed;
2900
+ }
2901
+
2902
+ function normalizeProgramGovernUseAction(actionCandidate) {
2903
+ if (actionCandidate === undefined || actionCandidate === null) {
2904
+ return null;
2905
+ }
2906
+ const parsed = Number(actionCandidate);
2907
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 20) {
2908
+ throw new Error('--program-govern-use-action must be an integer between 1 and 20.');
2909
+ }
2910
+ return parsed;
2911
+ }
2912
+
2887
2913
  function resolveProgramGatePolicy(policy = {}) {
2888
2914
  const profile = normalizeProgramGateProfile(policy.profile);
2889
2915
  const profilePolicy = PROGRAM_GATE_PROFILE_POLICY[profile];
@@ -4172,6 +4198,38 @@ function printCloseLoopBatchSummary(summary, options) {
4172
4198
  `source=${summary.auto_recovery.selection_source || 'default'})`
4173
4199
  ));
4174
4200
  }
4201
+ if (summary.program_governance && summary.program_governance.enabled) {
4202
+ console.log(chalk.gray(
4203
+ ` Program governance: ${summary.program_governance.performed_rounds}/` +
4204
+ `${summary.program_governance.max_rounds} rounds, stop=${summary.program_governance.stop_reason}`
4205
+ ));
4206
+ if (summary.program_governance.action_selection_enabled) {
4207
+ console.log(chalk.gray(
4208
+ ` Governance action selection: ` +
4209
+ `${summary.program_governance.auto_action_enabled ? 'auto' : 'manual-only'}, ` +
4210
+ `pinned=${summary.program_governance.pinned_action_index || 'none'}`
4211
+ ));
4212
+ }
4213
+ if (Array.isArray(summary.program_governance.history) && summary.program_governance.history.length > 0) {
4214
+ const latestRound = summary.program_governance.history[summary.program_governance.history.length - 1];
4215
+ if (latestRound && latestRound.selected_action) {
4216
+ console.log(chalk.gray(
4217
+ ` Governance selected action: #${latestRound.selected_action_index || 'n/a'} ${latestRound.selected_action}`
4218
+ ));
4219
+ }
4220
+ }
4221
+ if (summary.program_governance.exhausted) {
4222
+ console.log(chalk.yellow(' Program governance exhausted before reaching stable state.'));
4223
+ }
4224
+ }
4225
+ if (Array.isArray(summary.program_kpi_anomalies) && summary.program_kpi_anomalies.length > 0) {
4226
+ const highCount = summary.program_kpi_anomalies
4227
+ .filter(item => `${item && item.severity ? item.severity : ''}`.trim().toLowerCase() === 'high')
4228
+ .length;
4229
+ console.log(chalk.gray(
4230
+ ` Program KPI anomalies: total=${summary.program_kpi_anomalies.length}, high=${highCount}`
4231
+ ));
4232
+ }
4175
4233
  if (summary.program_coordination) {
4176
4234
  console.log(chalk.gray(
4177
4235
  ` Master/Sub sync: masters=${summary.program_coordination.master_spec_count}, ` +
@@ -4421,6 +4479,505 @@ function evaluateProgramConvergenceGate(summary, policy = {}) {
4421
4479
  };
4422
4480
  }
4423
4481
 
4482
+ async function applyProgramGateOutcome(summary, context = {}) {
4483
+ const projectPath = context && context.projectPath ? context.projectPath : process.cwd();
4484
+ const options = context && context.options && typeof context.options === 'object'
4485
+ ? context.options
4486
+ : {};
4487
+ const resolvedPolicy = resolveProgramGatePolicy(context && context.programGatePolicy ? context.programGatePolicy : {});
4488
+ const gateFallbackChain = Array.isArray(context && context.gateFallbackChain)
4489
+ ? context.gateFallbackChain
4490
+ : [];
4491
+ const enableAutoRemediation = context && context.enableAutoRemediation !== undefined
4492
+ ? Boolean(context.enableAutoRemediation)
4493
+ : true;
4494
+
4495
+ summary.program_gate = evaluateProgramConvergenceGate(summary, {
4496
+ profile: resolvedPolicy.profile,
4497
+ minSuccessRate: resolvedPolicy.minSuccessRate,
4498
+ maxRiskLevel: resolvedPolicy.maxRiskLevel,
4499
+ maxElapsedMinutes: resolvedPolicy.maxElapsedMinutes,
4500
+ maxAgentBudget: resolvedPolicy.maxAgentBudget,
4501
+ maxTotalSubSpecs: resolvedPolicy.maxTotalSubSpecs
4502
+ });
4503
+
4504
+ let effectiveGatePassed = summary.program_gate.passed;
4505
+ let effectiveGateSource = 'primary';
4506
+ let matchedFallbackProfile = null;
4507
+ summary.program_gate_fallbacks = [];
4508
+ if (!effectiveGatePassed && gateFallbackChain.length > 0) {
4509
+ for (const fallbackProfile of gateFallbackChain) {
4510
+ const fallbackResult = evaluateProgramConvergenceGate(summary, {
4511
+ profile: fallbackProfile,
4512
+ maxElapsedMinutes: resolvedPolicy.maxElapsedMinutes,
4513
+ maxAgentBudget: resolvedPolicy.maxAgentBudget,
4514
+ maxTotalSubSpecs: resolvedPolicy.maxTotalSubSpecs
4515
+ });
4516
+ summary.program_gate_fallbacks.push(fallbackResult);
4517
+ if (fallbackResult.passed) {
4518
+ effectiveGatePassed = true;
4519
+ effectiveGateSource = 'fallback-chain';
4520
+ matchedFallbackProfile = fallbackProfile;
4521
+ break;
4522
+ }
4523
+ }
4524
+ }
4525
+ summary.program_gate_fallback = summary.program_gate_fallbacks.length > 0
4526
+ ? summary.program_gate_fallbacks[0]
4527
+ : null;
4528
+ summary.program_gate_effective = {
4529
+ passed: effectiveGatePassed,
4530
+ source: effectiveGateSource,
4531
+ primary_passed: Boolean(summary.program_gate && summary.program_gate.passed),
4532
+ fallback_profile: matchedFallbackProfile,
4533
+ fallback_chain: gateFallbackChain,
4534
+ fallback_passed: matchedFallbackProfile !== null,
4535
+ attempted_fallback_count: summary.program_gate_fallbacks.length
4536
+ };
4537
+
4538
+ if (
4539
+ enableAutoRemediation &&
4540
+ (
4541
+ !summary.program_gate_effective.passed ||
4542
+ isSpecSessionBudgetHardFailure(summary) ||
4543
+ isSpecSessionGrowthGuardHardFailure(summary)
4544
+ )
4545
+ ) {
4546
+ summary.program_gate_auto_remediation = await applyProgramGateAutoRemediation(summary, {
4547
+ projectPath,
4548
+ options
4549
+ });
4550
+ }
4551
+
4552
+ return summary;
4553
+ }
4554
+
4555
+ function hasRecoverableProgramGoals(summary) {
4556
+ const failedStatuses = getBatchFailureStatusSet();
4557
+ const results = Array.isArray(summary && summary.results) ? summary.results : [];
4558
+ return results.some(item => failedStatuses.has(`${item && item.status ? item.status : ''}`.trim().toLowerCase()));
4559
+ }
4560
+
4561
+ function buildProgramAnomalyGovernancePatch(summary, anomalies, options = {}) {
4562
+ const sourceAnomalies = Array.isArray(anomalies) ? anomalies : [];
4563
+ const highAnomalies = sourceAnomalies.filter(item => `${item && item.severity ? item.severity : ''}`.trim().toLowerCase() === 'high');
4564
+ const patch = {};
4565
+ const reasons = [];
4566
+
4567
+ const anomalyTypes = new Set(highAnomalies.map(item => `${item && item.type ? item.type : ''}`.trim().toLowerCase()));
4568
+ if (anomalyTypes.has('success-rate-drop')) {
4569
+ const currentRetryRounds = normalizeBatchRetryRounds(options.batchRetryRounds);
4570
+ patch.batchRetryRounds = Math.min(5, Math.max(1, currentRetryRounds + 1));
4571
+ patch.batchRetryUntilComplete = true;
4572
+ reasons.push('increase retry rounds due to success-rate-drop anomaly');
4573
+ }
4574
+
4575
+ if (anomalyTypes.has('failed-goals-spike')) {
4576
+ const currentParallelCandidate = options.batchParallel !== undefined && options.batchParallel !== null
4577
+ ? options.batchParallel
4578
+ : (summary && summary.batch_parallel ? summary.batch_parallel : 1);
4579
+ const currentParallel = normalizeBatchParallel(currentParallelCandidate);
4580
+ if (currentParallel > 1) {
4581
+ patch.batchParallel = currentParallel - 1;
4582
+ reasons.push(`reduce batch parallel from ${currentParallel} to ${patch.batchParallel} due to failed-goals-spike`);
4583
+ }
4584
+
4585
+ const currentAgentBudgetCandidate = options.batchAgentBudget !== undefined && options.batchAgentBudget !== null
4586
+ ? options.batchAgentBudget
4587
+ : (summary && summary.resource_plan ? summary.resource_plan.agent_budget : null);
4588
+ const currentAgentBudget = normalizeBatchAgentBudget(currentAgentBudgetCandidate);
4589
+ if (currentAgentBudget !== null && currentAgentBudget > 1) {
4590
+ patch.batchAgentBudget = currentAgentBudget - 1;
4591
+ reasons.push(`reduce batch agent budget from ${currentAgentBudget} to ${patch.batchAgentBudget} due to failed-goals-spike`);
4592
+ }
4593
+ }
4594
+
4595
+ if (anomalyTypes.has('spec-growth-spike')) {
4596
+ patch.specSessionBudgetHardFail = true;
4597
+ reasons.push('enable spec-session budget hard-fail due to spec-growth-spike');
4598
+ if (options.specSessionMaxCreated === undefined || options.specSessionMaxCreated === null) {
4599
+ const estimatedCreated = Number(summary && summary.spec_session_budget && summary.spec_session_budget.estimated_created) || 0;
4600
+ patch.specSessionMaxCreated = Math.max(1, Math.ceil(estimatedCreated * 0.8));
4601
+ reasons.push(`set specSessionMaxCreated=${patch.specSessionMaxCreated} due to spec-growth-spike`);
4602
+ }
4603
+ }
4604
+
4605
+ return {
4606
+ patch,
4607
+ reasons,
4608
+ anomaly_count: highAnomalies.length,
4609
+ anomaly_types: [...anomalyTypes]
4610
+ };
4611
+ }
4612
+
4613
+ function applyProgramGovernancePatch(baseOptions, patch) {
4614
+ const merged = { ...baseOptions };
4615
+ const sourcePatch = patch && typeof patch === 'object' ? patch : {};
4616
+ for (const [key, value] of Object.entries(sourcePatch)) {
4617
+ if (value === undefined) {
4618
+ continue;
4619
+ }
4620
+ merged[key] = value;
4621
+ }
4622
+ return merged;
4623
+ }
4624
+
4625
+ function buildProgramGovernanceReplayGoalsResult(baseGoalsResult, round, summary) {
4626
+ const source = baseGoalsResult && typeof baseGoalsResult === 'object'
4627
+ ? baseGoalsResult
4628
+ : { file: '(generated-from-goal)', goals: [] };
4629
+ const sourceSummary = summary && typeof summary === 'object' ? summary : {};
4630
+ return {
4631
+ ...source,
4632
+ file: source.file || '(generated-from-goal)',
4633
+ resumedFromSummary: {
4634
+ file: sourceSummary.batch_session && sourceSummary.batch_session.file
4635
+ ? sourceSummary.batch_session.file
4636
+ : '(program-governance-replay)',
4637
+ strategy: 'program-governance-replay',
4638
+ round,
4639
+ previous_status: sourceSummary.status || null,
4640
+ previous_total_goals: Number(sourceSummary.total_goals) || null,
4641
+ previous_processed_goals: Number(sourceSummary.processed_goals) || null
4642
+ }
4643
+ };
4644
+ }
4645
+
4646
+ async function runProgramGovernanceLoop(context = {}) {
4647
+ let summary = context.summary && typeof context.summary === 'object' ? context.summary : {};
4648
+ const projectPath = context.projectPath || process.cwd();
4649
+ const baseProgramOptions = context.programOptions && typeof context.programOptions === 'object'
4650
+ ? context.programOptions
4651
+ : {};
4652
+ const baseGoalsResult = context.baseGoalsResult && typeof context.baseGoalsResult === 'object'
4653
+ ? context.baseGoalsResult
4654
+ : { file: '(generated-from-goal)', goals: [] };
4655
+ const enabled = Boolean(context.enabled);
4656
+ const maxRounds = normalizeProgramGovernMaxRounds(context.maxRounds);
4657
+ const maxDurationMinutes = normalizeProgramGovernMaxMinutes(context.maxMinutes);
4658
+ const maxDurationMs = maxDurationMinutes * 60 * 1000;
4659
+ const anomalyEnabled = context.anomalyEnabled !== false;
4660
+ const anomalyWeeks = normalizeProgramGovernAnomalyWeeks(context.anomalyWeeks);
4661
+ const anomalyPeriod = normalizeAutoKpiTrendPeriod(context.anomalyPeriod);
4662
+ const governUseAction = normalizeProgramGovernUseAction(context.governUseAction);
4663
+ const governAutoActionEnabled = context.governAutoActionEnabled !== false;
4664
+ const governActionEnabled = governAutoActionEnabled || governUseAction !== null;
4665
+ const programGatePolicy = resolveProgramGatePolicy(context.programGatePolicy || {});
4666
+ const gateFallbackChain = Array.isArray(context.gateFallbackChain) ? context.gateFallbackChain : [];
4667
+ const recoveryMemoryScope = context.recoveryMemoryScope || null;
4668
+ const normalizedRecoveryScope = normalizeRecoveryMemoryToken(recoveryMemoryScope || '') || 'default-scope';
4669
+ const recoverResumeStrategy = normalizeResumeStrategy(context.recoverResumeStrategy || 'pending');
4670
+ const recoverMaxRounds = normalizeRecoverMaxRounds(context.recoverMaxRounds);
4671
+ const recoverMaxMinutes = normalizeRecoverMaxMinutes(
4672
+ context.recoverMaxMinutes,
4673
+ '--program-recover-max-minutes'
4674
+ );
4675
+ const recoverMaxDurationMs = recoverMaxMinutes === null ? null : recoverMaxMinutes * 60 * 1000;
4676
+ const governanceStartedAt = Date.now();
4677
+ const history = [];
4678
+ let exhausted = false;
4679
+ let stopReason = enabled ? 'stable' : 'disabled';
4680
+ let settled = false;
4681
+
4682
+ if (!enabled) {
4683
+ return {
4684
+ summary,
4685
+ governance: {
4686
+ enabled: false,
4687
+ anomaly_enabled: anomalyEnabled,
4688
+ anomaly_weeks: anomalyWeeks,
4689
+ anomaly_period: anomalyPeriod,
4690
+ auto_action_enabled: governAutoActionEnabled,
4691
+ action_selection_enabled: false,
4692
+ pinned_action_index: governUseAction,
4693
+ max_rounds: maxRounds,
4694
+ max_minutes: maxDurationMinutes,
4695
+ performed_rounds: 0,
4696
+ converged: Boolean(
4697
+ summary &&
4698
+ summary.program_gate_effective &&
4699
+ summary.program_gate_effective.passed &&
4700
+ !isSpecSessionBudgetHardFailure(summary) &&
4701
+ !isSpecSessionGrowthGuardHardFailure(summary)
4702
+ ),
4703
+ exhausted: false,
4704
+ stop_reason: 'disabled',
4705
+ history: []
4706
+ }
4707
+ };
4708
+ }
4709
+
4710
+ for (let round = 1; round <= maxRounds; round += 1) {
4711
+ const elapsedBeforeRound = Date.now() - governanceStartedAt;
4712
+ if (elapsedBeforeRound >= maxDurationMs) {
4713
+ exhausted = true;
4714
+ stopReason = 'time-budget-exhausted';
4715
+ break;
4716
+ }
4717
+
4718
+ let trendResult = null;
4719
+ let anomalies = [];
4720
+ if (anomalyEnabled) {
4721
+ trendResult = await buildAutoKpiTrend(projectPath, {
4722
+ weeks: anomalyWeeks,
4723
+ mode: 'program',
4724
+ period: anomalyPeriod
4725
+ });
4726
+ anomalies = Array.isArray(trendResult.anomalies) ? trendResult.anomalies : [];
4727
+ summary.program_kpi_trend = {
4728
+ generated_at: trendResult.generated_at,
4729
+ weeks: trendResult.weeks,
4730
+ period_unit: trendResult.period_unit,
4731
+ total_runs: trendResult.total_runs,
4732
+ overall: trendResult.overall,
4733
+ anomaly_detection: trendResult.anomaly_detection || null
4734
+ };
4735
+ summary.program_kpi_anomalies = anomalies;
4736
+ }
4737
+
4738
+ const gateFailed = Boolean(
4739
+ !summary.program_gate_effective ||
4740
+ !summary.program_gate_effective.passed ||
4741
+ isSpecSessionBudgetHardFailure(summary) ||
4742
+ isSpecSessionGrowthGuardHardFailure(summary)
4743
+ );
4744
+ const highSeverityAnomalies = anomalies.filter(item => `${item && item.severity ? item.severity : ''}`.trim().toLowerCase() === 'high');
4745
+ const anomalyFailed = anomalyEnabled && highSeverityAnomalies.length > 0;
4746
+ if (!gateFailed && !anomalyFailed) {
4747
+ stopReason = 'stable';
4748
+ settled = true;
4749
+ break;
4750
+ }
4751
+
4752
+ const gatePatch = summary && summary.program_gate_auto_remediation && summary.program_gate_auto_remediation.next_run_patch
4753
+ ? summary.program_gate_auto_remediation.next_run_patch
4754
+ : {};
4755
+ const anomalyPatch = buildProgramAnomalyGovernancePatch(summary, highSeverityAnomalies, baseProgramOptions);
4756
+ let governanceActionSelection = null;
4757
+ let governanceActionPatch = {};
4758
+ if (governActionEnabled) {
4759
+ const recoveryMemory = await loadCloseLoopRecoveryMemory(projectPath);
4760
+ const recoverySignature = buildRecoveryMemorySignature(summary, {
4761
+ scope: normalizedRecoveryScope
4762
+ });
4763
+ const recoveryMemoryEntry = getRecoveryMemoryEntry(recoveryMemory.payload, recoverySignature);
4764
+ governanceActionSelection = resolveRecoveryActionSelection(summary, governUseAction, {
4765
+ recoveryMemoryEntry,
4766
+ optionLabel: '--program-govern-use-action'
4767
+ });
4768
+ governanceActionPatch = governanceActionSelection &&
4769
+ governanceActionSelection.appliedPatch &&
4770
+ typeof governanceActionSelection.appliedPatch === 'object'
4771
+ ? governanceActionSelection.appliedPatch
4772
+ : {};
4773
+ }
4774
+ const roundPatch = {
4775
+ ...(governanceActionPatch && typeof governanceActionPatch === 'object' ? governanceActionPatch : {}),
4776
+ ...(anomalyPatch.patch || {}),
4777
+ ...(gatePatch && typeof gatePatch === 'object' ? gatePatch : {})
4778
+ };
4779
+ if (Object.keys(roundPatch).length === 0) {
4780
+ stopReason = 'no-actionable-patch';
4781
+ history.push({
4782
+ round,
4783
+ status_before: summary.status,
4784
+ status_after: summary.status,
4785
+ trigger: {
4786
+ gate_failed: gateFailed,
4787
+ anomaly_failed: anomalyFailed,
4788
+ anomaly_count: highSeverityAnomalies.length
4789
+ },
4790
+ selected_action_index: governanceActionSelection ? governanceActionSelection.selectedIndex : null,
4791
+ selected_action: governanceActionSelection && governanceActionSelection.selectedAction
4792
+ ? governanceActionSelection.selectedAction.action
4793
+ : null,
4794
+ selected_action_priority: governanceActionSelection && governanceActionSelection.selectedAction
4795
+ ? governanceActionSelection.selectedAction.priority
4796
+ : null,
4797
+ action_selection_source: governanceActionSelection ? governanceActionSelection.selectionSource : null,
4798
+ action_selection_explain: governanceActionSelection ? governanceActionSelection.selectionExplain || null : null,
4799
+ execution_mode: 'none',
4800
+ applied_patch: null,
4801
+ notes: [
4802
+ 'No actionable governance patch generated.'
4803
+ ]
4804
+ });
4805
+ break;
4806
+ }
4807
+
4808
+ const roundOptions = applyProgramGovernancePatch(baseProgramOptions, roundPatch);
4809
+ roundOptions.out = null;
4810
+ roundOptions.programKpiOut = null;
4811
+ roundOptions.programAuditOut = null;
4812
+
4813
+ const statusBefore = summary.status;
4814
+ const failedGoalsBefore = Number(summary.failed_goals) || 0;
4815
+ const selectedGovernanceActionIndex = governanceActionSelection ? governanceActionSelection.selectedIndex : null;
4816
+ let executionMode = 'program-replay';
4817
+ let roundSummary = null;
4818
+ if (hasRecoverableProgramGoals(summary)) {
4819
+ executionMode = 'recover-cycle';
4820
+ const roundSourceSummary = summary.batch_session && summary.batch_session.file
4821
+ ? await loadCloseLoopBatchSummaryPayload(projectPath, summary.batch_session.file)
4822
+ : {
4823
+ file: '(program-governance-derived-summary)',
4824
+ payload: summary
4825
+ };
4826
+ const recoveryResult = await executeCloseLoopRecoveryCycle({
4827
+ projectPath,
4828
+ sourceSummary: roundSourceSummary,
4829
+ baseOptions: {
4830
+ ...roundOptions,
4831
+ useAction: selectedGovernanceActionIndex || context.programRecoverUseAction
4832
+ },
4833
+ recoverAutonomousEnabled: true,
4834
+ resumeStrategy: recoverResumeStrategy,
4835
+ recoverUntilComplete: true,
4836
+ recoverMaxRounds,
4837
+ recoverMaxDurationMs,
4838
+ recoveryMemoryScope,
4839
+ actionCandidate: selectedGovernanceActionIndex || context.programRecoverUseAction
4840
+ });
4841
+ roundSummary = mergeProgramRecoveryIntoProgramSummary(summary, recoveryResult.summary, {
4842
+ enabled: true,
4843
+ triggered: true,
4844
+ governance_round: round,
4845
+ recover_until_complete: true,
4846
+ source: 'governance-recover-cycle'
4847
+ });
4848
+ roundSummary.resource_plan = recoveryResult.summary && recoveryResult.summary.resource_plan
4849
+ ? recoveryResult.summary.resource_plan
4850
+ : roundSummary.resource_plan;
4851
+ roundSummary.batch_parallel = Number(recoveryResult.summary && recoveryResult.summary.batch_parallel) || roundSummary.batch_parallel;
4852
+ } else {
4853
+ const replayGoalsResult = buildProgramGovernanceReplayGoalsResult(baseGoalsResult, round, summary);
4854
+ const replaySummary = await executeCloseLoopBatch(
4855
+ replayGoalsResult,
4856
+ roundOptions,
4857
+ projectPath,
4858
+ 'auto-close-loop-program'
4859
+ );
4860
+ roundSummary = {
4861
+ ...replaySummary,
4862
+ auto_recovery: summary && summary.auto_recovery ? summary.auto_recovery : null
4863
+ };
4864
+ }
4865
+
4866
+ roundSummary.program_kpi = buildProgramKpiSnapshot(roundSummary);
4867
+ roundSummary.program_diagnostics = buildProgramDiagnostics(roundSummary);
4868
+ roundSummary.program_coordination = buildProgramCoordinationSnapshot(roundSummary);
4869
+ await applyProgramGateOutcome(roundSummary, {
4870
+ projectPath,
4871
+ options: roundOptions,
4872
+ programGatePolicy,
4873
+ gateFallbackChain,
4874
+ enableAutoRemediation: context.programGateAutoRemediate !== false
4875
+ });
4876
+
4877
+ const failedGoalsAfter = Number(roundSummary.failed_goals) || 0;
4878
+ history.push({
4879
+ round,
4880
+ status_before: statusBefore,
4881
+ status_after: roundSummary.status,
4882
+ trigger: {
4883
+ gate_failed: gateFailed,
4884
+ anomaly_failed: anomalyFailed,
4885
+ anomaly_count: highSeverityAnomalies.length
4886
+ },
4887
+ selected_action_index: selectedGovernanceActionIndex,
4888
+ selected_action: governanceActionSelection && governanceActionSelection.selectedAction
4889
+ ? governanceActionSelection.selectedAction.action
4890
+ : null,
4891
+ selected_action_priority: governanceActionSelection && governanceActionSelection.selectedAction
4892
+ ? governanceActionSelection.selectedAction.priority
4893
+ : null,
4894
+ action_selection_source: governanceActionSelection ? governanceActionSelection.selectionSource : null,
4895
+ action_selection_explain: governanceActionSelection ? governanceActionSelection.selectionExplain || null : null,
4896
+ execution_mode: executionMode,
4897
+ applied_patch: roundPatch,
4898
+ patch_reasons: [
4899
+ ...(governanceActionSelection && governanceActionSelection.selectionExplain
4900
+ ? [`governance-action: ${governanceActionSelection.selectionExplain.reason}`]
4901
+ : []),
4902
+ ...(Array.isArray(anomalyPatch.reasons) ? anomalyPatch.reasons : []),
4903
+ ...(summary.program_gate_auto_remediation && Array.isArray(summary.program_gate_auto_remediation.reasons)
4904
+ ? summary.program_gate_auto_remediation.reasons
4905
+ : [])
4906
+ ],
4907
+ failed_goals_before: failedGoalsBefore,
4908
+ failed_goals_after: failedGoalsAfter
4909
+ });
4910
+
4911
+ summary = roundSummary;
4912
+ if (
4913
+ summary.program_gate_effective &&
4914
+ summary.program_gate_effective.passed &&
4915
+ !isSpecSessionBudgetHardFailure(summary) &&
4916
+ !isSpecSessionGrowthGuardHardFailure(summary)
4917
+ ) {
4918
+ if (!anomalyEnabled) {
4919
+ stopReason = 'gate-stable';
4920
+ break;
4921
+ }
4922
+ const postTrend = await buildAutoKpiTrend(projectPath, {
4923
+ weeks: anomalyWeeks,
4924
+ mode: 'program',
4925
+ period: anomalyPeriod
4926
+ });
4927
+ const postAnomalies = Array.isArray(postTrend.anomalies) ? postTrend.anomalies : [];
4928
+ summary.program_kpi_trend = {
4929
+ generated_at: postTrend.generated_at,
4930
+ weeks: postTrend.weeks,
4931
+ period_unit: postTrend.period_unit,
4932
+ total_runs: postTrend.total_runs,
4933
+ overall: postTrend.overall,
4934
+ anomaly_detection: postTrend.anomaly_detection || null
4935
+ };
4936
+ summary.program_kpi_anomalies = postAnomalies;
4937
+ const hasHighPostAnomaly = postAnomalies.some(item => `${item && item.severity ? item.severity : ''}`.trim().toLowerCase() === 'high');
4938
+ if (!hasHighPostAnomaly) {
4939
+ stopReason = 'stable';
4940
+ settled = true;
4941
+ break;
4942
+ }
4943
+ }
4944
+ }
4945
+
4946
+ if (!settled && history.length >= maxRounds && stopReason === 'stable') {
4947
+ stopReason = 'round-limit-reached';
4948
+ exhausted = true;
4949
+ }
4950
+ if (!settled && history.length >= maxRounds && stopReason !== 'stable') {
4951
+ exhausted = true;
4952
+ }
4953
+
4954
+ return {
4955
+ summary,
4956
+ governance: {
4957
+ enabled: true,
4958
+ anomaly_enabled: anomalyEnabled,
4959
+ anomaly_weeks: anomalyWeeks,
4960
+ anomaly_period: anomalyPeriod,
4961
+ auto_action_enabled: governAutoActionEnabled,
4962
+ action_selection_enabled: governActionEnabled,
4963
+ pinned_action_index: governUseAction,
4964
+ max_rounds: maxRounds,
4965
+ max_minutes: maxDurationMinutes,
4966
+ performed_rounds: history.length,
4967
+ converged: Boolean(
4968
+ summary &&
4969
+ summary.program_gate_effective &&
4970
+ summary.program_gate_effective.passed &&
4971
+ !isSpecSessionBudgetHardFailure(summary) &&
4972
+ !isSpecSessionGrowthGuardHardFailure(summary)
4973
+ ),
4974
+ exhausted,
4975
+ stop_reason: stopReason,
4976
+ history
4977
+ }
4978
+ };
4979
+ }
4980
+
4424
4981
  async function applyProgramGateAutoRemediation(summary, context = {}) {
4425
4982
  const projectPath = context && context.projectPath ? context.projectPath : process.cwd();
4426
4983
  const options = context && context.options && typeof context.options === 'object'
@@ -4718,6 +5275,9 @@ async function maybeWriteProgramKpi(summary, outCandidate, projectPath) {
4718
5275
  program_diagnostics: summary.program_diagnostics,
4719
5276
  program_coordination: summary.program_coordination || null,
4720
5277
  auto_recovery: summary.auto_recovery || null,
5278
+ program_governance: summary.program_governance || null,
5279
+ program_kpi_trend: summary.program_kpi_trend || null,
5280
+ program_kpi_anomalies: Array.isArray(summary.program_kpi_anomalies) ? summary.program_kpi_anomalies : [],
4721
5281
  goal_input_guard: summary.goal_input_guard || null,
4722
5282
  spec_session_budget: summary.spec_session_budget || null,
4723
5283
  spec_session_growth_guard: summary.spec_session_growth_guard || null,
@@ -4765,6 +5325,9 @@ async function maybeWriteProgramAudit(summary, outCandidate, projectPath) {
4765
5325
  program_gate_fallbacks: Array.isArray(summary && summary.program_gate_fallbacks) ? summary.program_gate_fallbacks : [],
4766
5326
  program_gate_effective: summary && summary.program_gate_effective ? summary.program_gate_effective : null,
4767
5327
  auto_recovery: summary && summary.auto_recovery ? summary.auto_recovery : null,
5328
+ program_governance: summary && summary.program_governance ? summary.program_governance : null,
5329
+ program_kpi_trend: summary && summary.program_kpi_trend ? summary.program_kpi_trend : null,
5330
+ program_kpi_anomalies: Array.isArray(summary && summary.program_kpi_anomalies) ? summary.program_kpi_anomalies : [],
4768
5331
  recovery_cycle: summary && summary.recovery_cycle ? summary.recovery_cycle : null,
4769
5332
  recovery_plan: summary && summary.recovery_plan ? summary.recovery_plan : null,
4770
5333
  recovery_memory: summary && summary.recovery_memory ? summary.recovery_memory : null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiro-spec-engine",
3
- "version": "1.47.0",
3
+ "version": "1.47.1",
4
4
  "description": "kiro-spec-engine (kse) - A CLI tool and npm package for spec-driven development with AI coding assistants. NOT the Kiro IDE desktop application.",
5
5
  "main": "index.js",
6
6
  "bin": {