scene-capability-engine 3.6.32 → 3.6.36

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 (83) hide show
  1. package/CHANGELOG.md +86 -1
  2. package/README.md +119 -122
  3. package/README.zh.md +123 -121
  4. package/bin/scene-capability-engine.js +11 -0
  5. package/docs/README.md +21 -32
  6. package/docs/auto-refactor-index.md +384 -0
  7. package/docs/command-reference.md +94 -2
  8. package/docs/magicball-adaptation-task-checklist-v1.md +385 -0
  9. package/docs/magicball-app-bundle-sqlite-and-command-draft.md +539 -0
  10. package/docs/magicball-capability-iteration-api.md +2 -0
  11. package/docs/magicball-capability-iteration-ui.md +2 -0
  12. package/docs/magicball-capability-library.md +2 -0
  13. package/docs/magicball-cli-invocation-examples.md +336 -0
  14. package/docs/magicball-frontend-state-and-command-mapping.md +244 -0
  15. package/docs/magicball-integration-doc-index.md +137 -0
  16. package/docs/magicball-integration-issue-tracker.md +218 -0
  17. package/docs/magicball-mode-home-and-ontology-empty-state-playbook.md +249 -0
  18. package/docs/magicball-sce-adaptation-guide.md +203 -0
  19. package/docs/magicball-three-mode-alignment-plan.md +551 -0
  20. package/docs/magicball-ui-surface-checklist.md +126 -0
  21. package/docs/magicball-write-auth-adaptation-guide.md +328 -0
  22. package/docs/refactor-completion-roadmap.md +116 -0
  23. package/docs/zh/README.md +27 -30
  24. package/docs/zh/refactor-completion-roadmap.md +116 -0
  25. package/lib/app/registry-config.js +73 -0
  26. package/lib/app/registry-sync-service.js +228 -0
  27. package/lib/auto/archive-schema-service.js +276 -0
  28. package/lib/auto/archive-summary.js +60 -0
  29. package/lib/auto/batch-goal-input-service.js +543 -0
  30. package/lib/auto/batch-output.js +201 -0
  31. package/lib/auto/batch-summary-storage-service.js +110 -0
  32. package/lib/auto/close-loop-batch-service.js +116 -0
  33. package/lib/auto/close-loop-controller-service.js +287 -0
  34. package/lib/auto/close-loop-program-service.js +283 -0
  35. package/lib/auto/close-loop-recovery-service.js +191 -0
  36. package/lib/auto/close-loop-session-storage-service.js +50 -0
  37. package/lib/auto/controller-lock-service.js +55 -0
  38. package/lib/auto/controller-output.js +32 -0
  39. package/lib/auto/controller-queue-service.js +127 -0
  40. package/lib/auto/controller-session-storage-service.js +105 -0
  41. package/lib/auto/governance-advisory-service.js +208 -0
  42. package/lib/auto/governance-close-loop-service.js +411 -0
  43. package/lib/auto/governance-maintenance-presenter.js +162 -0
  44. package/lib/auto/governance-maintenance-service.js +112 -0
  45. package/lib/auto/governance-session-presenter.js +70 -0
  46. package/lib/auto/governance-session-storage-service.js +198 -0
  47. package/lib/auto/governance-signals.js +139 -0
  48. package/lib/auto/governance-stats-presenter.js +337 -0
  49. package/lib/auto/governance-stats-service.js +115 -0
  50. package/lib/auto/governance-summary.js +703 -0
  51. package/lib/auto/handoff-capability-matrix-service.js +281 -0
  52. package/lib/auto/handoff-evidence-review-service.js +251 -0
  53. package/lib/auto/handoff-release-evidence-service.js +190 -0
  54. package/lib/auto/handoff-release-gate-history-loaders-service.js +502 -0
  55. package/lib/auto/handoff-release-gate-history-service.js +257 -0
  56. package/lib/auto/handoff-reporting-service.js +1407 -0
  57. package/lib/auto/handoff-run-service.js +486 -0
  58. package/lib/auto/handoff-snapshots-service.js +645 -0
  59. package/lib/auto/observability-service.js +132 -0
  60. package/lib/auto/output-writer.js +34 -0
  61. package/lib/auto/program-auto-remediation-service.js +130 -0
  62. package/lib/auto/program-diagnostics.js +138 -0
  63. package/lib/auto/program-governance-helpers.js +306 -0
  64. package/lib/auto/program-governance-loop-service.js +413 -0
  65. package/lib/auto/program-output.js +106 -0
  66. package/lib/auto/program-summary.js +183 -0
  67. package/lib/auto/recovery-memory-service.js +684 -0
  68. package/lib/auto/recovery-selection-service.js +52 -0
  69. package/lib/auto/retention-policy.js +98 -0
  70. package/lib/auto/session-persistence-service.js +106 -0
  71. package/lib/auto/session-presenter.js +105 -0
  72. package/lib/auto/session-prune-service.js +190 -0
  73. package/lib/auto/session-query-service.js +249 -0
  74. package/lib/auto/spec-protection.js +141 -0
  75. package/lib/commands/app.js +911 -0
  76. package/lib/commands/assurance.js +212 -0
  77. package/lib/commands/auto.js +1091 -11063
  78. package/lib/commands/mode.js +321 -0
  79. package/lib/commands/ontology.js +415 -0
  80. package/lib/commands/pm.js +422 -0
  81. package/lib/ontology/seed-profiles.js +160 -0
  82. package/lib/state/sce-state-store.js +3369 -1200
  83. package/package.json +1 -1
@@ -0,0 +1,287 @@
1
+ async function runCloseLoopController(queueFile, options = {}, context = {}, dependencies = {}) {
2
+ const {
3
+ loadControllerGoalQueue,
4
+ normalizeControllerMaxCycles,
5
+ normalizeControllerMaxMinutes,
6
+ normalizeControllerPollSeconds,
7
+ normalizeControllerDequeueLimit,
8
+ writeControllerGoalQueue,
9
+ acquireControllerLock,
10
+ refreshControllerLock,
11
+ releaseControllerLock,
12
+ sleepForMs,
13
+ executeCloseLoopProgramGoal,
14
+ appendControllerGoalArchive,
15
+ maybePersistCloseLoopControllerSummary,
16
+ maybeWriteOutput,
17
+ now = () => Date.now()
18
+ } = dependencies;
19
+
20
+ const clock = typeof now === 'function' ? now : () => Date.now();
21
+ const projectPath = context.projectPath || process.cwd();
22
+ const resumedSession = context.resumedSession || null;
23
+ const queueInput = typeof queueFile === 'string' && queueFile.trim()
24
+ ? queueFile.trim()
25
+ : resumedSession && resumedSession.payload && typeof resumedSession.payload.queue_file === 'string'
26
+ ? resumedSession.payload.queue_file
27
+ : null;
28
+ const queueFormatCandidate = (
29
+ options.queueFormat === 'auto' &&
30
+ resumedSession &&
31
+ resumedSession.payload &&
32
+ typeof resumedSession.payload.queue_format === 'string' &&
33
+ resumedSession.payload.queue_format.trim()
34
+ )
35
+ ? resumedSession.payload.queue_format
36
+ : options.queueFormat;
37
+ const queuePayload = await loadControllerGoalQueue(projectPath, queueInput, queueFormatCandidate, {
38
+ dedupe: options.controllerDedupe !== false
39
+ });
40
+ const maxCycles = normalizeControllerMaxCycles(options.maxCycles);
41
+ const maxMinutes = normalizeControllerMaxMinutes(options.maxMinutes);
42
+ const maxDurationMs = maxMinutes * 60 * 1000;
43
+ const pollSeconds = normalizeControllerPollSeconds(options.pollSeconds);
44
+ const dequeueLimit = normalizeControllerDequeueLimit(options.dequeueLimit);
45
+ const waitOnEmpty = Boolean(options.waitOnEmpty);
46
+ const stopOnGoalFailure = Boolean(options.stopOnGoalFailure);
47
+ const startedAtMs = clock();
48
+ const startedAtIso = new Date(startedAtMs).toISOString();
49
+ const history = [];
50
+ const results = [];
51
+ let performedCycles = 0;
52
+ let stopReason = 'completed';
53
+ let exhausted = false;
54
+ let haltRequested = false;
55
+ let doneArchiveFile = null;
56
+ let failedArchiveFile = null;
57
+ let dedupeDroppedGoals = Number(queuePayload.duplicate_count) || 0;
58
+ let lockState = null;
59
+
60
+ if (options.controllerDedupe !== false && queuePayload.duplicate_count > 0) {
61
+ await writeControllerGoalQueue(queuePayload.file, queuePayload.format, queuePayload.goals);
62
+ }
63
+
64
+ lockState = await acquireControllerLock(projectPath, queuePayload.file, options);
65
+
66
+ try {
67
+ for (let cycle = 1; cycle <= maxCycles; cycle += 1) {
68
+ if ((clock() - startedAtMs) >= maxDurationMs) {
69
+ exhausted = true;
70
+ stopReason = 'time-budget-exhausted';
71
+ break;
72
+ }
73
+
74
+ await refreshControllerLock(lockState);
75
+
76
+ const currentQueue = await loadControllerGoalQueue(projectPath, queuePayload.file, queuePayload.format, {
77
+ dedupe: options.controllerDedupe !== false
78
+ });
79
+ const pendingGoals = currentQueue.goals;
80
+ dedupeDroppedGoals += Number(currentQueue.duplicate_count) || 0;
81
+
82
+ if (options.controllerDedupe !== false && currentQueue.duplicate_count > 0) {
83
+ await writeControllerGoalQueue(currentQueue.file, currentQueue.format, pendingGoals);
84
+ }
85
+
86
+ if (pendingGoals.length === 0) {
87
+ history.push({
88
+ cycle,
89
+ queue_before: 0,
90
+ dequeued: 0,
91
+ queue_after: 0,
92
+ status: waitOnEmpty ? 'idle-wait' : 'empty-stop'
93
+ });
94
+ performedCycles += 1;
95
+ if (!waitOnEmpty) {
96
+ stopReason = 'queue-empty';
97
+ break;
98
+ }
99
+ await sleepForMs(pollSeconds * 1000);
100
+ continue;
101
+ }
102
+
103
+ const effectiveDequeueLimit = dequeueLimit === null ? pendingGoals.length : dequeueLimit;
104
+ const dequeuedGoals = pendingGoals.slice(0, effectiveDequeueLimit);
105
+ const remainingGoals = pendingGoals.slice(dequeuedGoals.length);
106
+ await writeControllerGoalQueue(currentQueue.file, currentQueue.format, remainingGoals);
107
+
108
+ const cycleRecord = {
109
+ cycle,
110
+ queue_before: pendingGoals.length,
111
+ dequeued: dequeuedGoals.length,
112
+ queue_after: remainingGoals.length,
113
+ processed: 0,
114
+ completed: 0,
115
+ failed: 0,
116
+ status: 'processed'
117
+ };
118
+
119
+ for (let index = 0; index < dequeuedGoals.length; index += 1) {
120
+ const goal = dequeuedGoals[index];
121
+ const goalStartedAt = clock();
122
+ let goalResult = {
123
+ cycle,
124
+ queue_index: index + 1,
125
+ goal,
126
+ status: 'failed',
127
+ error: null
128
+ };
129
+
130
+ try {
131
+ const perGoalOptions = {
132
+ ...options,
133
+ out: null,
134
+ programKpiOut: null,
135
+ programAuditOut: null,
136
+ json: false
137
+ };
138
+ const programResult = await executeCloseLoopProgramGoal(goal, perGoalOptions, {
139
+ projectPath,
140
+ printSummary: options.controllerPrintProgramSummary === true,
141
+ writeOutputs: false
142
+ });
143
+ const programSummary = programResult.summary || {};
144
+ const failed = programResult.exitCode !== 0;
145
+ goalResult = {
146
+ ...goalResult,
147
+ status: failed ? 'failed' : 'completed',
148
+ program_status: programSummary.status || null,
149
+ program_gate_passed: Boolean(
150
+ programSummary.program_gate_effective &&
151
+ programSummary.program_gate_effective.passed
152
+ ),
153
+ governance_stop_reason: programSummary.program_governance
154
+ ? programSummary.program_governance.stop_reason
155
+ : null,
156
+ batch_session_file: programSummary.batch_session && programSummary.batch_session.file
157
+ ? programSummary.batch_session.file
158
+ : null
159
+ };
160
+ } catch (error) {
161
+ goalResult.error = error.message;
162
+ }
163
+
164
+ goalResult.elapsed_ms = Math.max(0, clock() - goalStartedAt);
165
+ results.push(goalResult);
166
+ cycleRecord.processed += 1;
167
+
168
+ if (goalResult.status === 'completed') {
169
+ cycleRecord.completed += 1;
170
+ doneArchiveFile = await appendControllerGoalArchive(
171
+ options.controllerDoneFile,
172
+ projectPath,
173
+ goal,
174
+ {
175
+ status: 'completed',
176
+ program_status: goalResult.program_status,
177
+ gate_passed: goalResult.program_gate_passed
178
+ }
179
+ ) || doneArchiveFile;
180
+ } else {
181
+ cycleRecord.failed += 1;
182
+ failedArchiveFile = await appendControllerGoalArchive(
183
+ options.controllerFailedFile,
184
+ projectPath,
185
+ goal,
186
+ {
187
+ status: 'failed',
188
+ program_status: goalResult.program_status,
189
+ gate_passed: goalResult.program_gate_passed
190
+ }
191
+ ) || failedArchiveFile;
192
+ if (stopOnGoalFailure) {
193
+ haltRequested = true;
194
+ }
195
+ }
196
+
197
+ if (haltRequested) {
198
+ break;
199
+ }
200
+ }
201
+
202
+ if (haltRequested) {
203
+ cycleRecord.status = 'stopped-on-goal-failure';
204
+ stopReason = 'goal-failure';
205
+ }
206
+ history.push(cycleRecord);
207
+ performedCycles += 1;
208
+
209
+ if (haltRequested) {
210
+ break;
211
+ }
212
+ }
213
+ } finally {
214
+ await releaseControllerLock(lockState);
215
+ }
216
+
217
+ const finalQueue = await loadControllerGoalQueue(projectPath, queuePayload.file, queuePayload.format, {
218
+ dedupe: options.controllerDedupe !== false
219
+ });
220
+ const pendingGoals = finalQueue.goals.length;
221
+ dedupeDroppedGoals += Number(finalQueue.duplicate_count) || 0;
222
+ if (options.controllerDedupe !== false && finalQueue.duplicate_count > 0) {
223
+ await writeControllerGoalQueue(finalQueue.file, finalQueue.format, finalQueue.goals);
224
+ }
225
+
226
+ if (!exhausted && stopReason === 'completed') {
227
+ if (performedCycles >= maxCycles && (pendingGoals > 0 || waitOnEmpty)) {
228
+ exhausted = true;
229
+ stopReason = 'cycle-limit-reached';
230
+ } else if (pendingGoals === 0 && results.length === 0) {
231
+ stopReason = 'queue-empty';
232
+ }
233
+ }
234
+
235
+ const completedGoals = results.filter(item => item.status === 'completed').length;
236
+ const failedGoals = results.filter(item => item.status !== 'completed').length;
237
+ const status = failedGoals === 0
238
+ ? 'completed'
239
+ : completedGoals === 0
240
+ ? 'failed'
241
+ : 'partial-failed';
242
+ const summary = {
243
+ mode: 'auto-close-loop-controller',
244
+ status,
245
+ queue_file: queuePayload.file,
246
+ queue_format: queuePayload.format,
247
+ started_at: startedAtIso,
248
+ completed_at: new Date(clock()).toISOString(),
249
+ elapsed_ms: Math.max(0, clock() - startedAtMs),
250
+ wait_on_empty: waitOnEmpty,
251
+ poll_seconds: pollSeconds,
252
+ dequeue_limit: dequeueLimit === null ? 'all' : dequeueLimit,
253
+ max_cycles: maxCycles,
254
+ max_minutes: maxMinutes,
255
+ cycles_performed: performedCycles,
256
+ exhausted,
257
+ stop_reason: stopReason,
258
+ processed_goals: results.length,
259
+ completed_goals: completedGoals,
260
+ failed_goals: failedGoals,
261
+ pending_goals: pendingGoals,
262
+ dedupe_enabled: options.controllerDedupe !== false,
263
+ dedupe_dropped_goals: dedupeDroppedGoals,
264
+ lock_enabled: options.controllerLock !== false,
265
+ lock_file: lockState && lockState.file ? lockState.file : null,
266
+ lock_ttl_seconds: lockState && Number.isInteger(lockState.ttl_seconds) ? lockState.ttl_seconds : null,
267
+ resumed_from_controller_session: resumedSession
268
+ ? {
269
+ id: resumedSession.id,
270
+ file: resumedSession.file || null,
271
+ created_at: resumedSession.created_at || null
272
+ }
273
+ : null,
274
+ done_archive_file: doneArchiveFile,
275
+ failed_archive_file: failedArchiveFile,
276
+ history,
277
+ results
278
+ };
279
+
280
+ await maybePersistCloseLoopControllerSummary(summary, options, projectPath);
281
+ await maybeWriteOutput(summary, options.controllerOut, projectPath);
282
+ return summary;
283
+ }
284
+
285
+ module.exports = {
286
+ runCloseLoopController
287
+ };
@@ -0,0 +1,283 @@
1
+ async function executeCloseLoopProgramGoal(goal, options = {}, context = {}, dependencies = {}) {
2
+ const {
3
+ normalizeRecoverMaxRounds,
4
+ normalizeRecoverMaxMinutes,
5
+ normalizeResumeStrategy,
6
+ normalizeProgramGovernMaxRounds,
7
+ normalizeProgramGovernMaxMinutes,
8
+ normalizeProgramGovernAnomalyWeeks,
9
+ normalizeAutoKpiTrendPeriod,
10
+ normalizeProgramGovernUseAction,
11
+ resolveProgramGatePolicy,
12
+ normalizeProgramGateFallbackProfile,
13
+ resolveProgramGateFallbackChain,
14
+ resolveRecoveryMemoryScope,
15
+ normalizeBatchRetryMaxRounds,
16
+ normalizeBatchSessionKeep,
17
+ normalizeBatchSessionOlderThanDays,
18
+ normalizeSpecKeep,
19
+ normalizeOlderThanDays,
20
+ normalizeSpecSessionProtectWindowDays,
21
+ normalizeSpecSessionMaxTotal,
22
+ normalizeSpecSessionMaxCreated,
23
+ normalizeSpecSessionMaxCreatedPerGoal,
24
+ normalizeSpecSessionMaxDuplicateGoals,
25
+ sanitizeBatchSessionId,
26
+ buildCloseLoopBatchGoalsFromGoal,
27
+ executeCloseLoopBatch,
28
+ executeCloseLoopRecoveryCycle,
29
+ mergeProgramRecoveryIntoProgramSummary,
30
+ buildProgramKpiSnapshot,
31
+ buildProgramDiagnostics,
32
+ buildProgramCoordinationSnapshot,
33
+ maybeWriteProgramKpi,
34
+ maybeWriteOutput,
35
+ maybePersistCloseLoopBatchSummary,
36
+ applyProgramGateOutcome,
37
+ runProgramGovernanceLoop,
38
+ isSpecSessionBudgetHardFailure,
39
+ isSpecSessionGrowthGuardHardFailure,
40
+ maybeWriteProgramAudit,
41
+ printCloseLoopBatchSummary,
42
+ now = () => Date.now(),
43
+ cwd = () => process.cwd()
44
+ } = dependencies;
45
+
46
+ const programStartedAt = now();
47
+ const projectPath = context.projectPath || cwd();
48
+ const shouldPrintSummary = context.printSummary !== false;
49
+ const writeOutputs = context.writeOutputs !== false;
50
+ const programAutonomousEnabled = options.batchAutonomous !== false;
51
+ const programAutoRecoverEnabled = options.programAutoRecover !== false;
52
+ const programRecoverMaxRounds = normalizeRecoverMaxRounds(options.programRecoverMaxRounds);
53
+ const programRecoverMaxMinutes = normalizeRecoverMaxMinutes(options.programRecoverMaxMinutes, '--program-recover-max-minutes');
54
+ const programRecoverResumeStrategy = normalizeResumeStrategy(options.programRecoverResumeStrategy);
55
+ const programGovernUntilStable = Boolean(options.programGovernUntilStable);
56
+ const programGovernMaxRounds = normalizeProgramGovernMaxRounds(options.programGovernMaxRounds);
57
+ const programGovernMaxMinutes = normalizeProgramGovernMaxMinutes(options.programGovernMaxMinutes);
58
+ const programGovernAnomalyEnabled = options.programGovernAnomaly !== false;
59
+ const programGovernAnomalyWeeks = normalizeProgramGovernAnomalyWeeks(options.programGovernAnomalyWeeks);
60
+ const programGovernAnomalyPeriod = normalizeAutoKpiTrendPeriod(options.programGovernAnomalyPeriod);
61
+ const programGovernUseAction = normalizeProgramGovernUseAction(options.programGovernUseAction);
62
+ const programGovernAutoActionEnabled = options.programGovernAutoAction !== false;
63
+ const programGatePolicy = resolveProgramGatePolicy({
64
+ profile: options.programGateProfile,
65
+ minSuccessRate: options.programMinSuccessRate,
66
+ maxRiskLevel: options.programMaxRiskLevel,
67
+ maxElapsedMinutes: options.programMaxElapsedMinutes,
68
+ maxAgentBudget: options.programMaxAgentBudget,
69
+ maxTotalSubSpecs: options.programMaxTotalSubSpecs
70
+ });
71
+ const gateFallbackProfile = normalizeProgramGateFallbackProfile(options.programGateFallbackProfile);
72
+ const gateFallbackChain = resolveProgramGateFallbackChain(options.programGateFallbackChain, gateFallbackProfile);
73
+ const recoveryMemoryScope = await resolveRecoveryMemoryScope(projectPath, options.recoveryMemoryScope);
74
+ if (options.resume) {
75
+ throw new Error('--resume is not supported in close-loop-program. Use close-loop --resume or remove --resume.');
76
+ }
77
+ if (options.sessionId) {
78
+ throw new Error('--session-id is not supported in close-loop-program. Session ids are generated per goal.');
79
+ }
80
+ if (
81
+ options.batchRetryMaxRounds !== undefined &&
82
+ options.batchRetryMaxRounds !== null &&
83
+ !options.batchRetryUntilComplete &&
84
+ !programAutonomousEnabled
85
+ ) {
86
+ throw new Error('--batch-retry-max-rounds requires --batch-retry-until-complete.');
87
+ }
88
+ if (options.batchRetryMaxRounds !== undefined && options.batchRetryMaxRounds !== null) {
89
+ normalizeBatchRetryMaxRounds(options.batchRetryMaxRounds);
90
+ }
91
+ if (options.batchSessionKeep !== undefined && options.batchSessionKeep !== null) {
92
+ normalizeBatchSessionKeep(options.batchSessionKeep);
93
+ }
94
+ if (options.batchSessionOlderThanDays !== undefined && options.batchSessionOlderThanDays !== null) {
95
+ normalizeBatchSessionOlderThanDays(options.batchSessionOlderThanDays);
96
+ }
97
+ if (options.specSessionKeep !== undefined && options.specSessionKeep !== null) {
98
+ normalizeSpecKeep(options.specSessionKeep);
99
+ }
100
+ if (options.specSessionOlderThanDays !== undefined && options.specSessionOlderThanDays !== null) {
101
+ normalizeOlderThanDays(options.specSessionOlderThanDays);
102
+ }
103
+ if (options.specSessionProtectWindowDays !== undefined && options.specSessionProtectWindowDays !== null) {
104
+ normalizeSpecSessionProtectWindowDays(options.specSessionProtectWindowDays);
105
+ }
106
+ if (options.specSessionMaxTotal !== undefined && options.specSessionMaxTotal !== null) {
107
+ normalizeSpecSessionMaxTotal(options.specSessionMaxTotal);
108
+ }
109
+ if (options.specSessionMaxCreated !== undefined && options.specSessionMaxCreated !== null) {
110
+ normalizeSpecSessionMaxCreated(options.specSessionMaxCreated);
111
+ }
112
+ if (options.specSessionMaxCreatedPerGoal !== undefined && options.specSessionMaxCreatedPerGoal !== null) {
113
+ normalizeSpecSessionMaxCreatedPerGoal(options.specSessionMaxCreatedPerGoal);
114
+ }
115
+ if (options.specSessionMaxDuplicateGoals !== undefined && options.specSessionMaxDuplicateGoals !== null) {
116
+ normalizeSpecSessionMaxDuplicateGoals(options.specSessionMaxDuplicateGoals);
117
+ }
118
+ if (options.batchSessionId !== undefined && options.batchSessionId !== null) {
119
+ const sanitizedBatchSessionId = sanitizeBatchSessionId(options.batchSessionId);
120
+ if (!sanitizedBatchSessionId) {
121
+ throw new Error('--batch-session-id is invalid after sanitization.');
122
+ }
123
+ }
124
+
125
+ const goalsResult = buildCloseLoopBatchGoalsFromGoal(goal, options.programGoals, {
126
+ minQualityScore: options.programMinQualityScore,
127
+ enforceQualityGate: Boolean(options.programQualityGate)
128
+ });
129
+ const programOptions = {
130
+ ...options,
131
+ batchAutonomous: programAutonomousEnabled
132
+ };
133
+ const initialSummary = await executeCloseLoopBatch(goalsResult, programOptions, projectPath, 'auto-close-loop-program');
134
+ let summary = {
135
+ ...initialSummary,
136
+ auto_recovery: {
137
+ enabled: programAutoRecoverEnabled,
138
+ triggered: false,
139
+ converged: initialSummary.status === 'completed',
140
+ source_status: initialSummary.status
141
+ }
142
+ };
143
+
144
+ if (programAutoRecoverEnabled && initialSummary.status !== 'completed') {
145
+ const recoveryResult = await executeCloseLoopRecoveryCycle({
146
+ projectPath,
147
+ sourceSummary: {
148
+ file: initialSummary.batch_session && initialSummary.batch_session.file
149
+ ? initialSummary.batch_session.file
150
+ : '(auto-close-loop-program-in-memory)',
151
+ payload: initialSummary
152
+ },
153
+ baseOptions: {
154
+ ...programOptions,
155
+ useAction: options.programRecoverUseAction
156
+ },
157
+ recoverAutonomousEnabled: true,
158
+ resumeStrategy: programRecoverResumeStrategy,
159
+ recoverUntilComplete: true,
160
+ recoverMaxRounds: programRecoverMaxRounds,
161
+ recoverMaxDurationMs: programRecoverMaxMinutes === null ? null : programRecoverMaxMinutes * 60 * 1000,
162
+ recoveryMemoryScope,
163
+ actionCandidate: options.programRecoverUseAction
164
+ });
165
+
166
+ summary = mergeProgramRecoveryIntoProgramSummary(
167
+ initialSummary,
168
+ recoveryResult.summary,
169
+ {
170
+ enabled: true,
171
+ triggered: true,
172
+ recover_until_complete: true,
173
+ recover_max_rounds: programRecoverMaxRounds,
174
+ recover_max_minutes: programRecoverMaxMinutes,
175
+ resume_strategy: programRecoverResumeStrategy
176
+ }
177
+ );
178
+ summary.program_kpi = buildProgramKpiSnapshot(summary);
179
+ summary.program_diagnostics = buildProgramDiagnostics(summary);
180
+ summary.program_coordination = buildProgramCoordinationSnapshot(summary);
181
+ if (writeOutputs) {
182
+ await maybeWriteProgramKpi(summary, options.programKpiOut, projectPath);
183
+ await maybeWriteOutput(summary, options.out, projectPath);
184
+ }
185
+ if (programOptions.batchSession !== false) {
186
+ await maybePersistCloseLoopBatchSummary(summary, programOptions, projectPath);
187
+ }
188
+ }
189
+
190
+ const programCompletedAt = now();
191
+ summary.program_started_at = new Date(programStartedAt).toISOString();
192
+ summary.program_completed_at = new Date(programCompletedAt).toISOString();
193
+ summary.program_elapsed_ms = Math.max(0, programCompletedAt - programStartedAt);
194
+
195
+ await applyProgramGateOutcome(summary, {
196
+ projectPath,
197
+ options: programOptions,
198
+ programGatePolicy,
199
+ gateFallbackChain,
200
+ enableAutoRemediation: options.programGateAutoRemediate !== false
201
+ });
202
+
203
+ if (programGovernUntilStable) {
204
+ const governanceResult = await runProgramGovernanceLoop({
205
+ enabled: true,
206
+ summary,
207
+ projectPath,
208
+ programOptions,
209
+ baseGoalsResult: goalsResult,
210
+ maxRounds: programGovernMaxRounds,
211
+ maxMinutes: programGovernMaxMinutes,
212
+ anomalyEnabled: programGovernAnomalyEnabled,
213
+ anomalyWeeks: programGovernAnomalyWeeks,
214
+ anomalyPeriod: programGovernAnomalyPeriod,
215
+ programGatePolicy,
216
+ gateFallbackChain,
217
+ recoveryMemoryScope,
218
+ recoverResumeStrategy: programRecoverResumeStrategy,
219
+ recoverMaxRounds: programRecoverMaxRounds,
220
+ recoverMaxMinutes: programRecoverMaxMinutes,
221
+ programRecoverUseAction: options.programRecoverUseAction,
222
+ programGateAutoRemediate: options.programGateAutoRemediate !== false,
223
+ governUseAction: programGovernUseAction,
224
+ governAutoActionEnabled: programGovernAutoActionEnabled
225
+ });
226
+ summary = governanceResult.summary;
227
+ summary.program_governance = governanceResult.governance;
228
+ } else {
229
+ summary.program_governance = {
230
+ enabled: false,
231
+ anomaly_enabled: programGovernAnomalyEnabled,
232
+ anomaly_weeks: programGovernAnomalyWeeks,
233
+ anomaly_period: programGovernAnomalyPeriod,
234
+ auto_action_enabled: programGovernAutoActionEnabled,
235
+ action_selection_enabled: false,
236
+ pinned_action_index: programGovernUseAction,
237
+ max_rounds: programGovernMaxRounds,
238
+ max_minutes: programGovernMaxMinutes,
239
+ performed_rounds: 0,
240
+ converged: Boolean(
241
+ summary &&
242
+ summary.program_gate_effective &&
243
+ summary.program_gate_effective.passed &&
244
+ !isSpecSessionBudgetHardFailure(summary) &&
245
+ !isSpecSessionGrowthGuardHardFailure(summary)
246
+ ),
247
+ exhausted: false,
248
+ stop_reason: 'disabled',
249
+ history: []
250
+ };
251
+ }
252
+
253
+ const finalProgramCompletedAt = now();
254
+ summary.program_completed_at = new Date(finalProgramCompletedAt).toISOString();
255
+ summary.program_elapsed_ms = Math.max(0, finalProgramCompletedAt - programStartedAt);
256
+
257
+ if (writeOutputs) {
258
+ await maybeWriteProgramKpi(summary, options.programKpiOut, projectPath);
259
+ await maybeWriteOutput(summary, options.out, projectPath);
260
+ await maybeWriteProgramAudit(summary, options.programAuditOut, projectPath);
261
+ }
262
+
263
+ if (shouldPrintSummary) {
264
+ printCloseLoopBatchSummary(summary, programOptions);
265
+ }
266
+
267
+ const exitCode = (
268
+ summary.status !== 'completed' ||
269
+ !summary.program_gate_effective.passed ||
270
+ isSpecSessionBudgetHardFailure(summary) ||
271
+ isSpecSessionGrowthGuardHardFailure(summary)
272
+ ) ? 1 : 0;
273
+
274
+ return {
275
+ summary,
276
+ options: programOptions,
277
+ exitCode
278
+ };
279
+ }
280
+
281
+ module.exports = {
282
+ executeCloseLoopProgramGoal
283
+ };