@msn-control/liftoff 0.9.9 → 0.10.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.
Files changed (111) hide show
  1. package/DEVELOPER.md +142 -0
  2. package/README.md +26 -18
  3. package/assets/governance/single-maintainer-gitflow/policy.md +13 -11
  4. package/dist/args.js +23 -1
  5. package/dist/args.js.map +1 -1
  6. package/dist/artifact-lifecycle.d.ts +1 -1
  7. package/dist/artifact-lifecycle.js +5 -0
  8. package/dist/artifact-lifecycle.js.map +1 -1
  9. package/dist/catalogs.d.ts +2 -0
  10. package/dist/catalogs.js +13 -1
  11. package/dist/catalogs.js.map +1 -1
  12. package/dist/commands.js +365 -17
  13. package/dist/commands.js.map +1 -1
  14. package/dist/file-system.js +100 -13
  15. package/dist/file-system.js.map +1 -1
  16. package/dist/governance-activation/activation-state.d.ts +32 -0
  17. package/dist/governance-activation/activation-state.js +165 -0
  18. package/dist/governance-activation/activation-state.js.map +1 -0
  19. package/dist/governance-activation/approvals.d.ts +188 -0
  20. package/dist/governance-activation/approvals.js +508 -0
  21. package/dist/governance-activation/approvals.js.map +1 -0
  22. package/dist/governance-activation/canonical-json.d.ts +3 -0
  23. package/dist/governance-activation/canonical-json.js +36 -0
  24. package/dist/governance-activation/canonical-json.js.map +1 -0
  25. package/dist/governance-activation/commands.d.ts +10 -0
  26. package/dist/governance-activation/commands.js +1023 -0
  27. package/dist/governance-activation/commands.js.map +1 -0
  28. package/dist/governance-activation/compatibility.d.ts +68 -0
  29. package/dist/governance-activation/compatibility.js +447 -0
  30. package/dist/governance-activation/compatibility.js.map +1 -0
  31. package/dist/governance-activation/credentials.d.ts +160 -0
  32. package/dist/governance-activation/credentials.js +403 -0
  33. package/dist/governance-activation/credentials.js.map +1 -0
  34. package/dist/governance-activation/doctor.d.ts +10 -0
  35. package/dist/governance-activation/doctor.js +379 -0
  36. package/dist/governance-activation/doctor.js.map +1 -0
  37. package/dist/governance-activation/evidence.d.ts +49 -0
  38. package/dist/governance-activation/evidence.js +246 -0
  39. package/dist/governance-activation/evidence.js.map +1 -0
  40. package/dist/governance-activation/graph.d.ts +397 -0
  41. package/dist/governance-activation/graph.js +441 -0
  42. package/dist/governance-activation/graph.js.map +1 -0
  43. package/dist/governance-activation/identity.d.ts +35 -0
  44. package/dist/governance-activation/identity.js +89 -0
  45. package/dist/governance-activation/identity.js.map +1 -0
  46. package/dist/governance-activation/index.d.ts +18 -0
  47. package/dist/governance-activation/index.js +19 -0
  48. package/dist/governance-activation/index.js.map +1 -0
  49. package/dist/governance-activation/migration.d.ts +44 -0
  50. package/dist/governance-activation/migration.js +268 -0
  51. package/dist/governance-activation/migration.js.map +1 -0
  52. package/dist/governance-activation/readiness.d.ts +23 -0
  53. package/dist/governance-activation/readiness.js +158 -0
  54. package/dist/governance-activation/readiness.js.map +1 -0
  55. package/dist/governance-activation/reconciliation.d.ts +11 -0
  56. package/dist/governance-activation/reconciliation.js +58 -0
  57. package/dist/governance-activation/reconciliation.js.map +1 -0
  58. package/dist/governance-activation/release-integrity.d.ts +11 -0
  59. package/dist/governance-activation/release-integrity.js +49 -0
  60. package/dist/governance-activation/release-integrity.js.map +1 -0
  61. package/dist/governance-activation/seed-lifecycle.d.ts +116 -0
  62. package/dist/governance-activation/seed-lifecycle.js +609 -0
  63. package/dist/governance-activation/seed-lifecycle.js.map +1 -0
  64. package/dist/governance-activation/source-of-truth.d.ts +164 -0
  65. package/dist/governance-activation/source-of-truth.js +986 -0
  66. package/dist/governance-activation/source-of-truth.js.map +1 -0
  67. package/dist/governance-activation/task-projection.d.ts +21 -0
  68. package/dist/governance-activation/task-projection.js +75 -0
  69. package/dist/governance-activation/task-projection.js.map +1 -0
  70. package/dist/governance-activation/transitions.d.ts +163 -0
  71. package/dist/governance-activation/transitions.js +2128 -0
  72. package/dist/governance-activation/transitions.js.map +1 -0
  73. package/dist/governance-activation/type-contract.d.ts +1 -0
  74. package/dist/governance-activation/type-contract.js +27 -0
  75. package/dist/governance-activation/type-contract.js.map +1 -0
  76. package/dist/governance-activation/types.d.ts +390 -0
  77. package/dist/governance-activation/types.js +108 -0
  78. package/dist/governance-activation/types.js.map +1 -0
  79. package/dist/governance-activation/validators.d.ts +17 -0
  80. package/dist/governance-activation/validators.js +1305 -0
  81. package/dist/governance-activation/validators.js.map +1 -0
  82. package/dist/init-filesystem.js +5 -1
  83. package/dist/init-filesystem.js.map +1 -1
  84. package/dist/interactive.js +4 -3
  85. package/dist/interactive.js.map +1 -1
  86. package/dist/planner.js +5 -5
  87. package/dist/planner.js.map +1 -1
  88. package/dist/power-apps-templates.js +14 -1
  89. package/dist/power-apps-templates.js.map +1 -1
  90. package/dist/process-runner.d.ts +3 -0
  91. package/dist/process-runner.js +68 -10
  92. package/dist/process-runner.js.map +1 -1
  93. package/dist/repository-governance.d.ts +13 -3
  94. package/dist/repository-governance.js +357 -51
  95. package/dist/repository-governance.js.map +1 -1
  96. package/dist/telemetry/contract.d.ts +1 -1
  97. package/dist/telemetry/contract.js +6 -0
  98. package/dist/telemetry/contract.js.map +1 -1
  99. package/dist/templates.js +320 -25
  100. package/dist/templates.js.map +1 -1
  101. package/dist/types.d.ts +15 -7
  102. package/docs/cli-reference.md +61 -13
  103. package/docs/configuration-and-manifests.md +43 -16
  104. package/docs/existing-repositories.md +22 -11
  105. package/docs/getting-started.md +46 -10
  106. package/docs/project-structure.md +35 -6
  107. package/docs/repository-governance.md +200 -245
  108. package/docs/safety-and-consent.md +24 -2
  109. package/docs/troubleshooting.md +59 -4
  110. package/docs/workloads.md +1 -1
  111. package/package.json +2 -1
@@ -0,0 +1,2128 @@
1
+ import { readdir, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { NodeCommandRunner } from '../process-runner.js';
4
+ import { applyProjectFileTransaction, readProjectFile, resolveProjectPath, validateArtifactPathParts } from '../file-system.js';
5
+ import { archiveGeneratedSeedForPhase, generatedSeedChangeName, validateGeneratedSeedForPhase, verifyGeneratedSeedBaselineForPhase } from './seed-lifecycle.js';
6
+ import { evaluateApprovalForTransitionPlan, transitionPlanForPhase } from './approvals.js';
7
+ import { canonicalJson, canonicalSha256 } from './canonical-json.js';
8
+ import { credentialPolicyPathParts, detectCredentialLeaks } from './credentials.js';
9
+ import { evidenceHeaderDigest, selectLatestPhaseEvidence } from './evidence.js';
10
+ import { phaseContractDigests } from './graph.js';
11
+ import { buildApprovedPhase0FactsFromState, renderGovernanceChangeWritePlan, stateWithSelectedActiveChange } from './source-of-truth.js';
12
+ import { phaseIds } from './types.js';
13
+ import { validateEvidenceHeader, validateSavedTransitionPlan, validateUserActivationState } from './validators.js';
14
+ import { activationStateContentHash } from './activation-state.js';
15
+ export const governancePlanDirectoryPathParts = ['governance', 'plans'];
16
+ const planLifetimeMs = 15 * 60 * 1000;
17
+ const dayMs = 24 * 60 * 60 * 1000;
18
+ const remoteImportPayloadKind = 'remote-import-verified.v1';
19
+ const workflowSourcePayloadKind = 'workflow-source-ready.v1';
20
+ const greenRedPayloadKind = 'green-red-proof.v1';
21
+ const bootstrapStateDisposedPayloadKind = 'bootstrap-state-disposed.v1';
22
+ const engineProducer = 'liftoff-governance-transition-engine';
23
+ function errorMessage(error) {
24
+ return error instanceof Error ? error.message : String(error);
25
+ }
26
+ function commandSucceeded(result) {
27
+ return result.status === 0 && !result.timedOut && !result.errorCode;
28
+ }
29
+ function commandFailure(result) {
30
+ if (result.timedOut) {
31
+ return `${result.displayCommand} timed out`;
32
+ }
33
+ if (result.errorCode || result.errorMessage) {
34
+ return `${result.displayCommand}: ${[result.errorCode, result.errorMessage].filter(Boolean).join(': ')}`;
35
+ }
36
+ return `${result.displayCommand} exited ${result.status ?? 'unknown'}${result.stderr ? `: ${result.stderr.trim()}` : ''}`;
37
+ }
38
+ function phaseById(graph, phaseId) {
39
+ const phase = graph.phases.find((entry) => entry.id === phaseId);
40
+ if (!phase) {
41
+ throw new Error(`Unknown phase ${phaseId}.`);
42
+ }
43
+ return phase;
44
+ }
45
+ function safeTimestamp(value) {
46
+ return value.replace(/[^0-9A-Za-z]/g, '');
47
+ }
48
+ export function transitionPlanPathParts(plan) {
49
+ return validateArtifactPathParts([
50
+ ...governancePlanDirectoryPathParts,
51
+ `${plan.phaseId}-${safeTimestamp(plan.createdAt)}-${plan.planDigest.slice(0, 12)}.json`
52
+ ], 'Governance transition plan path');
53
+ }
54
+ function evidencePathParts(evidenceId) {
55
+ return validateArtifactPathParts(['governance', 'evidence', `${evidenceId}.json`], 'Governance evidence path');
56
+ }
57
+ function activationStatePathParts() {
58
+ return validateArtifactPathParts(['governance', 'activation-state.json'], 'Activation state path');
59
+ }
60
+ function cloneState(state) {
61
+ return JSON.parse(JSON.stringify(state));
62
+ }
63
+ function isRecord(value) {
64
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
65
+ }
66
+ function phaseOrder(phaseId) {
67
+ return phaseIds.indexOf(phaseId);
68
+ }
69
+ function transitionDestination(type, identity, extras = {}) {
70
+ return { type, identity, ...extras };
71
+ }
72
+ function operation(input) {
73
+ return input;
74
+ }
75
+ const allowedActionsByPhase = {
76
+ 'seed-valid': [
77
+ 'openspec.seed.validate',
78
+ 'governance.evidence.write',
79
+ 'governance.activation-state.write'
80
+ ],
81
+ 'seed-verified': [
82
+ 'openspec.seed.baseline-verify',
83
+ 'governance.evidence.write',
84
+ 'governance.activation-state.write'
85
+ ],
86
+ 'seed-archived': [
87
+ 'openspec.seed.archive',
88
+ 'governance.evidence.write',
89
+ 'governance.activation-state.write'
90
+ ],
91
+ committed: [
92
+ 'git.init',
93
+ 'git.add-reviewed',
94
+ 'git.commit-reviewed',
95
+ 'git.verify-existing-commit',
96
+ 'governance.evidence.write',
97
+ 'governance.activation-state.write'
98
+ ],
99
+ pushed: [
100
+ 'git.push-approved-ref',
101
+ 'git.verify-existing-push',
102
+ 'governance.evidence.write',
103
+ 'governance.activation-state.write'
104
+ ],
105
+ 'phase-0-complete': [
106
+ 'github.phase0.discover',
107
+ 'azure.phase0.discover',
108
+ 'governance.evidence.write',
109
+ 'governance.activation-state.write'
110
+ ],
111
+ 'activation-approved': [
112
+ 'openspec.governance.create-change',
113
+ 'spec-kit.governance.create-change',
114
+ 'governance.activation-state.write'
115
+ ],
116
+ 'credential-ready': [
117
+ 'github.credential.verify-policy',
118
+ 'github.credential.enroll-masked',
119
+ 'governance.evidence.write',
120
+ 'governance.activation-state.write'
121
+ ],
122
+ 'provider-ready': [
123
+ 'azure.provider.ensure-ready',
124
+ 'governance.evidence.write',
125
+ 'governance.activation-state.write'
126
+ ],
127
+ 'state-path-selected': [
128
+ 'azure.state-path.select',
129
+ 'governance.evidence.write',
130
+ 'governance.activation-state.write'
131
+ ],
132
+ 'existing-private-path': [
133
+ 'azure.existing-private-path.verify',
134
+ 'governance.evidence.write',
135
+ 'governance.activation-state.write'
136
+ ],
137
+ 'bootstrap-local': [
138
+ 'azure.bootstrap-local.apply',
139
+ 'github.bootstrap-local.configure',
140
+ 'governance.evidence.write',
141
+ 'governance.activation-state.write'
142
+ ],
143
+ 'runner-ready': [
144
+ 'github.runner.ensure-ready',
145
+ 'governance.evidence.write',
146
+ 'governance.activation-state.write'
147
+ ],
148
+ 'private-backend-proof': [
149
+ 'github.runner.backend-proof',
150
+ 'azure.remote-state.read',
151
+ 'governance.evidence.write',
152
+ 'governance.activation-state.write'
153
+ ],
154
+ 'remote-import-verified': [
155
+ 'azure.remote-import.verify',
156
+ 'governance.evidence.write',
157
+ 'governance.activation-state.write'
158
+ ],
159
+ 'remote-ready': [
160
+ 'azure.remote-ready.verify',
161
+ 'governance.evidence.write',
162
+ 'governance.activation-state.write'
163
+ ],
164
+ 'application-foundation': [
165
+ 'azure.application-foundation.apply',
166
+ 'openspec.governance.update',
167
+ 'governance.evidence.write',
168
+ 'governance.activation-state.write'
169
+ ],
170
+ 'workflow-source-ready': [
171
+ 'local.workflow-source.write',
172
+ 'local.ruleset-source.write',
173
+ 'governance.evidence.write',
174
+ 'governance.activation-state.write'
175
+ ],
176
+ 'dev-proof': [
177
+ 'github.checks.dev-proof',
178
+ 'governance.evidence.write',
179
+ 'governance.activation-state.write'
180
+ ],
181
+ 'staging-qualified': [
182
+ 'github.checks.staging',
183
+ 'azure.staging.readback',
184
+ 'governance.evidence.write',
185
+ 'governance.activation-state.write'
186
+ ],
187
+ 'production-rehearsed': [
188
+ 'github.checks.production-rehearsal',
189
+ 'azure.production-readback',
190
+ 'governance.evidence.write',
191
+ 'governance.activation-state.write'
192
+ ],
193
+ 'green-red-proof': [
194
+ 'github.checks.green-red-proof',
195
+ 'governance.evidence.write',
196
+ 'governance.activation-state.write'
197
+ ],
198
+ 'enforcement-approved': [
199
+ 'governance.activation-state.write'
200
+ ],
201
+ 'rulesets-applied': [
202
+ 'github.ruleset.apply',
203
+ 'github.ruleset.readback',
204
+ 'governance.evidence.write',
205
+ 'governance.activation-state.write'
206
+ ],
207
+ 'live-readback': [
208
+ 'github.ruleset.readback',
209
+ 'governance.evidence.write',
210
+ 'governance.activation-state.write'
211
+ ],
212
+ 'bootstrap-state-disposed': [
213
+ 'local.bootstrap-state.dispose',
214
+ 'governance.evidence.write',
215
+ 'governance.activation-state.write'
216
+ ]
217
+ };
218
+ function isAllowedAction(phaseId, actionId) {
219
+ return allowedActionsByPhase[phaseId].includes(actionId);
220
+ }
221
+ function assertOperationAllowed(phase, op) {
222
+ if (op.phaseId !== phase.id) {
223
+ throw new Error(`Operation ${op.actionId} belongs to ${op.phaseId}, not ${phase.id}.`);
224
+ }
225
+ if (!isAllowedAction(phase.id, op.actionId)) {
226
+ throw new Error(`Operation ${op.actionId} is not allowlisted for phase ${phase.id}.`);
227
+ }
228
+ const allowed = op.remote ? phase.allowedMutations.remote : phase.allowedMutations.local;
229
+ if (!allowed.includes(op.mutationClass)) {
230
+ throw new Error(`Operation ${op.actionId} mutation ${op.mutationClass} is not declared in phase ${phase.id} ${op.remote ? 'remote' : 'local'} mutations.`);
231
+ }
232
+ }
233
+ function assertPlanOperationsAllowed(plan, phase) {
234
+ for (const op of plan.operations) {
235
+ assertOperationAllowed(phase, op);
236
+ }
237
+ }
238
+ function evidenceWriteOperation(phase, pathParts) {
239
+ return operation({
240
+ adapter: 'local-evidence',
241
+ actionId: 'governance.evidence.write',
242
+ mutationClass: 'write-evidence',
243
+ phaseId: phase.id,
244
+ inputs: { pathParts },
245
+ destination: transitionDestination('local', pathParts.join('/'), { pathParts }),
246
+ remote: false,
247
+ destructive: false
248
+ });
249
+ }
250
+ function stateWriteOperation(phase) {
251
+ return operation({
252
+ adapter: 'local-evidence',
253
+ actionId: 'governance.activation-state.write',
254
+ mutationClass: 'write-activation-state',
255
+ phaseId: phase.id,
256
+ inputs: { pathParts: activationStatePathParts() },
257
+ destination: transitionDestination('local', 'governance/activation-state.json', {
258
+ pathParts: activationStatePathParts()
259
+ }),
260
+ remote: false,
261
+ destructive: false
262
+ });
263
+ }
264
+ async function allProjectFiles(projectRoot) {
265
+ const files = [];
266
+ async function visit(current, relative) {
267
+ const entries = await readdir(current, { withFileTypes: true });
268
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name, 'en'))) {
269
+ if (entry.name === '.git' ||
270
+ entry.name === 'node_modules' ||
271
+ entry.name === '.cache' ||
272
+ (relative === '' && entry.name === 'governance')) {
273
+ continue;
274
+ }
275
+ const child = path.join(current, entry.name);
276
+ const childRelative = relative ? `${relative}/${entry.name}` : entry.name;
277
+ if (entry.isDirectory()) {
278
+ await visit(child, childRelative);
279
+ }
280
+ else if (entry.isFile()) {
281
+ files.push(childRelative);
282
+ }
283
+ }
284
+ }
285
+ await visit(projectRoot, '');
286
+ return files;
287
+ }
288
+ function parsePorcelainStatus(output) {
289
+ const entries = output.split('\0').filter(Boolean);
290
+ const parsed = [];
291
+ for (const entry of entries) {
292
+ const index = entry.slice(0, 1);
293
+ const worktree = entry.slice(1, 2);
294
+ const rawPath = entry.slice(3);
295
+ const renamed = rawPath.includes('\0') ? rawPath.split('\0').at(-1) : rawPath;
296
+ parsed.push({ index, worktree, path: renamed });
297
+ }
298
+ return parsed.filter((entry) => !entry.path.startsWith('governance/'));
299
+ }
300
+ async function runCommand(runner, command, cwd) {
301
+ return await runner.run(command, { cwd });
302
+ }
303
+ async function runGit(runner, projectRoot, args) {
304
+ return await runCommand(runner, { executable: 'git', args }, projectRoot);
305
+ }
306
+ async function inspectGitRepository(projectRoot, runner) {
307
+ const rootResult = await runGit(runner, projectRoot, ['rev-parse', '--show-toplevel']);
308
+ if (!commandSucceeded(rootResult)) {
309
+ return {
310
+ insideWorkTree: false,
311
+ root: null,
312
+ branch: null,
313
+ head: null,
314
+ upstream: null,
315
+ status: [],
316
+ remotes: [],
317
+ issues: []
318
+ };
319
+ }
320
+ const root = path.resolve(rootResult.stdout.trim());
321
+ if (root !== path.resolve(projectRoot)) {
322
+ return {
323
+ insideWorkTree: true,
324
+ root,
325
+ branch: null,
326
+ head: null,
327
+ upstream: null,
328
+ status: [],
329
+ remotes: [],
330
+ issues: [`Git repository root ${root} does not match Liftoff project root ${projectRoot}.`]
331
+ };
332
+ }
333
+ const branchResult = await runGit(runner, projectRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
334
+ const branch = commandSucceeded(branchResult) ? branchResult.stdout.trim() : null;
335
+ const headResult = await runGit(runner, projectRoot, ['rev-parse', '--verify', 'HEAD']);
336
+ const head = commandSucceeded(headResult) ? headResult.stdout.trim() : null;
337
+ const upstreamResult = await runGit(runner, projectRoot, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
338
+ const upstream = commandSucceeded(upstreamResult) ? upstreamResult.stdout.trim() : null;
339
+ const statusResult = await runGit(runner, projectRoot, ['status', '--porcelain=v1', '-z', '--untracked-files=all']);
340
+ const status = commandSucceeded(statusResult) ? parsePorcelainStatus(statusResult.stdout) : [];
341
+ const remoteResult = await runGit(runner, projectRoot, ['remote', '-v']);
342
+ const remotes = commandSucceeded(remoteResult) ? parseRemotes(remoteResult.stdout) : [];
343
+ const issues = [];
344
+ if (!branch) {
345
+ issues.push('Detached HEAD or unborn branch cannot be reviewed for deterministic governance commit/push.');
346
+ }
347
+ if (!commandSucceeded(statusResult)) {
348
+ issues.push(commandFailure(statusResult));
349
+ }
350
+ if (status.some((entry) => entry.index === 'D' || entry.worktree === 'D')) {
351
+ issues.push('Unknown file deletion is outside the reviewed initial commit plan.');
352
+ }
353
+ return { insideWorkTree: true, root, branch, head, upstream, status, remotes, issues };
354
+ }
355
+ function parseRemotes(output) {
356
+ const remotes = new Map();
357
+ for (const line of output.split(/\r?\n/u)) {
358
+ const match = line.match(/^(\S+)\s+(\S+)\s+\((fetch|push)\)$/u);
359
+ if (match?.[3] === 'fetch') {
360
+ remotes.set(match[1], match[2]);
361
+ }
362
+ }
363
+ return [...remotes.entries()]
364
+ .map(([name, url]) => ({ name, url }))
365
+ .sort((left, right) => left.name.localeCompare(right.name, 'en'));
366
+ }
367
+ function gitStatusDigest(inspection) {
368
+ return canonicalSha256({
369
+ branch: inspection.branch,
370
+ head: inspection.head,
371
+ status: inspection.status,
372
+ remotes: inspection.remotes
373
+ });
374
+ }
375
+ async function remoteHead(runner, projectRoot, remote, branch) {
376
+ const result = await runGit(runner, projectRoot, ['ls-remote', '--heads', remote, branch]);
377
+ if (!commandSucceeded(result)) {
378
+ throw new Error(commandFailure(result));
379
+ }
380
+ const first = result.stdout.trim().split(/\s+/u)[0];
381
+ return first && /^[a-f0-9]{40}$/u.test(first) ? first : null;
382
+ }
383
+ async function isAncestor(runner, projectRoot, ancestor, descendant) {
384
+ const result = await runGit(runner, projectRoot, ['merge-base', '--is-ancestor', ancestor, descendant]);
385
+ return result.status === 0;
386
+ }
387
+ function defaultBranch(state) {
388
+ return state.repository.defaultBranch || 'main';
389
+ }
390
+ async function gitCommitOperations(inspection, phase, runner) {
391
+ const git = await inspectGitRepository(inspection.projectRoot, runner);
392
+ if (git.issues.length > 0) {
393
+ throw new Error(git.issues.join(' '));
394
+ }
395
+ const branch = defaultBranch(inspection.state);
396
+ if (!git.insideWorkTree) {
397
+ const files = await allProjectFiles(inspection.projectRoot);
398
+ return [
399
+ operation({
400
+ adapter: 'git',
401
+ actionId: 'git.init',
402
+ mutationClass: 'git-commit',
403
+ phaseId: phase.id,
404
+ inputs: { branch },
405
+ destination: transitionDestination('local', '.git'),
406
+ remote: false,
407
+ destructive: false
408
+ }),
409
+ operation({
410
+ adapter: 'git',
411
+ actionId: 'git.add-reviewed',
412
+ mutationClass: 'git-commit',
413
+ phaseId: phase.id,
414
+ inputs: { paths: files, statusDigest: canonicalSha256(files) },
415
+ destination: transitionDestination('local', 'git-index'),
416
+ remote: false,
417
+ destructive: false
418
+ }),
419
+ operation({
420
+ adapter: 'git',
421
+ actionId: 'git.commit-reviewed',
422
+ mutationClass: 'git-commit',
423
+ phaseId: phase.id,
424
+ inputs: {
425
+ message: 'Initial Liftoff baseline',
426
+ branch,
427
+ paths: files
428
+ },
429
+ destination: transitionDestination('local', branch, { ref: branch }),
430
+ remote: false,
431
+ destructive: false
432
+ })
433
+ ];
434
+ }
435
+ if (git.branch !== branch) {
436
+ throw new Error(`Expected branch ${branch}, found ${git.branch ?? 'detached'}.`);
437
+ }
438
+ if (git.upstream) {
439
+ const divergence = await runGit(runner, inspection.projectRoot, ['rev-list', '--left-right', '--count', `${git.upstream}...HEAD`]);
440
+ if (!commandSucceeded(divergence)) {
441
+ throw new Error(commandFailure(divergence));
442
+ }
443
+ const [behind, ahead] = divergence.stdout.trim().split(/\s+/u).map((value) => Number.parseInt(value, 10));
444
+ if ((behind ?? 0) > 0 || (ahead ?? 0) > 0) {
445
+ throw new Error(`Branch ${branch} diverges from ${git.upstream}; refusing commit before explicit reconciliation.`);
446
+ }
447
+ }
448
+ if (git.status.length === 0 && git.head) {
449
+ return [
450
+ operation({
451
+ adapter: 'git',
452
+ actionId: 'git.verify-existing-commit',
453
+ mutationClass: 'read-worktree',
454
+ phaseId: phase.id,
455
+ inputs: { head: git.head, branch, statusDigest: gitStatusDigest(git) },
456
+ destination: transitionDestination('local', branch, { ref: branch }),
457
+ remote: false,
458
+ destructive: false
459
+ })
460
+ ];
461
+ }
462
+ const paths = git.status.map((entry) => entry.path).sort((left, right) => left.localeCompare(right, 'en'));
463
+ return [
464
+ operation({
465
+ adapter: 'git',
466
+ actionId: 'git.add-reviewed',
467
+ mutationClass: 'git-commit',
468
+ phaseId: phase.id,
469
+ inputs: { paths, statusDigest: gitStatusDigest(git) },
470
+ destination: transitionDestination('local', 'git-index'),
471
+ remote: false,
472
+ destructive: false
473
+ }),
474
+ operation({
475
+ adapter: 'git',
476
+ actionId: 'git.commit-reviewed',
477
+ mutationClass: 'git-commit',
478
+ phaseId: phase.id,
479
+ inputs: {
480
+ message: 'Initial Liftoff baseline',
481
+ branch,
482
+ paths
483
+ },
484
+ destination: transitionDestination('local', branch, { ref: branch }),
485
+ remote: false,
486
+ destructive: false
487
+ })
488
+ ];
489
+ }
490
+ async function gitPushOperations(inspection, phase, runner) {
491
+ const git = await inspectGitRepository(inspection.projectRoot, runner);
492
+ if (git.issues.length > 0) {
493
+ throw new Error(git.issues.join(' '));
494
+ }
495
+ if (!git.insideWorkTree || !git.head) {
496
+ throw new Error('A reviewed local commit is required before push.');
497
+ }
498
+ const branch = defaultBranch(inspection.state);
499
+ if (git.branch !== branch) {
500
+ throw new Error(`Expected branch ${branch}, found ${git.branch ?? 'detached'}.`);
501
+ }
502
+ if (git.status.length > 0) {
503
+ throw new Error(`Worktree has unreviewed changes outside governance/plans: ${git.status.map((entry) => entry.path).join(', ')}.`);
504
+ }
505
+ const remoteNames = new Set(git.remotes.map((remote) => remote.name));
506
+ if (!remoteNames.has('origin') || git.remotes.length !== 1) {
507
+ throw new Error(`Expected exactly one approved origin remote; found ${git.remotes.map((remote) => remote.name).join(', ') || 'none'}.`);
508
+ }
509
+ const remoteSha = await remoteHead(runner, inspection.projectRoot, 'origin', branch);
510
+ if (remoteSha && remoteSha !== git.head && !(await isAncestor(runner, inspection.projectRoot, remoteSha, git.head))) {
511
+ throw new Error('Remote branch is not a fast-forward target; force, reset, rebase, and non-fast-forward push are forbidden.');
512
+ }
513
+ return [
514
+ operation({
515
+ adapter: 'git',
516
+ actionId: remoteSha === git.head ? 'git.verify-existing-push' : 'git.push-approved-ref',
517
+ mutationClass: remoteSha === git.head ? 'github-read' : 'git-push',
518
+ phaseId: phase.id,
519
+ inputs: {
520
+ remote: 'origin',
521
+ branch,
522
+ localHead: git.head,
523
+ remoteHead: remoteSha,
524
+ statusDigest: gitStatusDigest(git)
525
+ },
526
+ destination: transitionDestination('repository', inspection.state.repository.name, {
527
+ repository: inspection.state.repository.name,
528
+ ref: `refs/heads/${branch}`
529
+ }),
530
+ remote: true,
531
+ destructive: false
532
+ })
533
+ ];
534
+ }
535
+ async function phaseOperations(inspection, phase, runner, createdAt) {
536
+ const statePath = activationStatePathParts();
537
+ const baseEvidencePath = evidencePathParts(`${phase.id}-${safeTimestamp(createdAt)}`);
538
+ const writeOps = (includeEvidence = true) => [
539
+ ...(includeEvidence ? [evidenceWriteOperation(phase, baseEvidencePath)] : []),
540
+ stateWriteOperation(phase)
541
+ ];
542
+ switch (phase.id) {
543
+ case 'seed-valid':
544
+ return [
545
+ operation({
546
+ adapter: 'selected-spec-workflow',
547
+ actionId: 'openspec.seed.validate',
548
+ mutationClass: 'read-worktree',
549
+ phaseId: phase.id,
550
+ inputs: { changeName: generatedSeedChangeName(inspection.manifest) },
551
+ destination: transitionDestination('local', inspection.projectRoot),
552
+ remote: false,
553
+ destructive: false
554
+ }),
555
+ ...writeOps()
556
+ ];
557
+ case 'seed-verified':
558
+ return [
559
+ operation({
560
+ adapter: 'selected-spec-workflow',
561
+ actionId: 'openspec.seed.baseline-verify',
562
+ mutationClass: 'read-worktree',
563
+ phaseId: phase.id,
564
+ inputs: { changeName: generatedSeedChangeName(inspection.manifest) },
565
+ destination: transitionDestination('local', inspection.projectRoot),
566
+ remote: false,
567
+ destructive: false
568
+ }),
569
+ ...writeOps()
570
+ ];
571
+ case 'seed-archived':
572
+ return [
573
+ operation({
574
+ adapter: 'selected-spec-workflow',
575
+ actionId: 'openspec.seed.archive',
576
+ mutationClass: 'write-openspec-seed',
577
+ phaseId: phase.id,
578
+ inputs: { changeName: generatedSeedChangeName(inspection.manifest) },
579
+ destination: transitionDestination('local', 'openspec/changes'),
580
+ remote: false,
581
+ destructive: false
582
+ }),
583
+ ...writeOps()
584
+ ];
585
+ case 'committed':
586
+ return [...await gitCommitOperations(inspection, phase, runner), ...writeOps()];
587
+ case 'pushed':
588
+ return [...await gitPushOperations(inspection, phase, runner), ...writeOps()];
589
+ case 'phase-0-complete':
590
+ return [
591
+ operation({
592
+ adapter: 'github',
593
+ actionId: 'github.phase0.discover',
594
+ mutationClass: 'github-read',
595
+ phaseId: phase.id,
596
+ inputs: { repository: inspection.state.repository.name },
597
+ destination: transitionDestination('repository', inspection.state.repository.name, {
598
+ repository: inspection.state.repository.name
599
+ }),
600
+ remote: true,
601
+ destructive: false
602
+ }),
603
+ operation({
604
+ adapter: 'azure-opentofu',
605
+ actionId: 'azure.phase0.discover',
606
+ mutationClass: 'azure-read',
607
+ phaseId: phase.id,
608
+ inputs: { cloud: 'cloud' in inspection.manifest.project.workload ? inspection.manifest.project.workload.cloud : 'power-platform' },
609
+ destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
610
+ subscriptionId: 'phase-0-discovered-subscription'
611
+ }),
612
+ remote: true,
613
+ destructive: false
614
+ }),
615
+ ...writeOps()
616
+ ];
617
+ case 'activation-approved': {
618
+ const actionId = inspection.manifest.project.specWorkflow === 'openspec'
619
+ ? 'openspec.governance.create-change'
620
+ : 'spec-kit.governance.create-change';
621
+ return [
622
+ operation({
623
+ adapter: 'selected-spec-workflow',
624
+ actionId,
625
+ mutationClass: 'write-openspec-governance',
626
+ phaseId: phase.id,
627
+ inputs: { workflowKind: inspection.manifest.project.specWorkflow },
628
+ destination: transitionDestination('local', 'governance change'),
629
+ remote: false,
630
+ destructive: false
631
+ }),
632
+ stateWriteOperation(phase)
633
+ ];
634
+ }
635
+ case 'credential-ready':
636
+ return [
637
+ operation({
638
+ adapter: 'github',
639
+ actionId: 'github.credential.verify-policy',
640
+ mutationClass: 'github-read',
641
+ phaseId: phase.id,
642
+ inputs: { policyPathParts: credentialPolicyPathParts },
643
+ destination: transitionDestination('repository', inspection.state.repository.name, {
644
+ repository: inspection.state.repository.name
645
+ }),
646
+ remote: true,
647
+ destructive: false
648
+ }),
649
+ ...writeOps()
650
+ ];
651
+ case 'provider-ready':
652
+ return [
653
+ operation({
654
+ adapter: 'azure-opentofu',
655
+ actionId: 'azure.provider.ensure-ready',
656
+ mutationClass: 'azure-provider-register',
657
+ phaseId: phase.id,
658
+ inputs: { retainedCapability: true },
659
+ destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
660
+ subscriptionId: 'phase-0-discovered-subscription'
661
+ }),
662
+ remote: true,
663
+ destructive: false
664
+ }),
665
+ ...writeOps()
666
+ ];
667
+ case 'state-path-selected':
668
+ return [
669
+ operation({
670
+ adapter: 'azure-opentofu',
671
+ actionId: 'azure.state-path.select',
672
+ mutationClass: 'azure-read',
673
+ phaseId: phase.id,
674
+ inputs: { allowed: ['existing-private', 'bootstrap-local'] },
675
+ destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
676
+ subscriptionId: 'phase-0-discovered-subscription'
677
+ }),
678
+ remote: true,
679
+ destructive: false
680
+ }),
681
+ ...writeOps()
682
+ ];
683
+ case 'existing-private-path':
684
+ return [
685
+ operation({
686
+ adapter: 'azure-opentofu',
687
+ actionId: 'azure.existing-private-path.verify',
688
+ mutationClass: 'azure-read',
689
+ phaseId: phase.id,
690
+ inputs: { statePath: 'existing-private' },
691
+ destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
692
+ subscriptionId: 'phase-0-discovered-subscription'
693
+ }),
694
+ remote: true,
695
+ destructive: false
696
+ }),
697
+ ...writeOps()
698
+ ];
699
+ case 'bootstrap-local':
700
+ return [
701
+ operation({
702
+ adapter: 'azure-opentofu',
703
+ actionId: 'azure.bootstrap-local.apply',
704
+ mutationClass: 'azure-network-provision',
705
+ phaseId: phase.id,
706
+ inputs: { boundedLocalBootstrap: true },
707
+ destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
708
+ subscriptionId: 'phase-0-discovered-subscription'
709
+ }),
710
+ remote: true,
711
+ destructive: false
712
+ }),
713
+ operation({
714
+ adapter: 'github',
715
+ actionId: 'github.bootstrap-local.configure',
716
+ mutationClass: 'github-write',
717
+ phaseId: phase.id,
718
+ inputs: { repository: inspection.state.repository.name },
719
+ destination: transitionDestination('repository', inspection.state.repository.name, {
720
+ repository: inspection.state.repository.name
721
+ }),
722
+ remote: true,
723
+ destructive: false
724
+ }),
725
+ ...writeOps()
726
+ ];
727
+ case 'runner-ready':
728
+ return [
729
+ operation({
730
+ adapter: 'github',
731
+ actionId: 'github.runner.ensure-ready',
732
+ mutationClass: 'github-write',
733
+ phaseId: phase.id,
734
+ inputs: { restrictedRunner: true },
735
+ destination: transitionDestination('repository', inspection.state.repository.name, {
736
+ repository: inspection.state.repository.name
737
+ }),
738
+ remote: true,
739
+ destructive: false
740
+ }),
741
+ ...writeOps()
742
+ ];
743
+ case 'private-backend-proof':
744
+ return [
745
+ operation({
746
+ adapter: 'github',
747
+ actionId: 'github.runner.backend-proof',
748
+ mutationClass: 'github-read',
749
+ phaseId: phase.id,
750
+ inputs: { requiredConclusion: 'success' },
751
+ destination: transitionDestination('repository', inspection.state.repository.name, {
752
+ repository: inspection.state.repository.name
753
+ }),
754
+ remote: true,
755
+ destructive: false
756
+ }),
757
+ ...writeOps()
758
+ ];
759
+ case 'remote-import-verified':
760
+ return [
761
+ operation({
762
+ adapter: 'azure-opentofu',
763
+ actionId: 'azure.remote-import.verify',
764
+ mutationClass: 'azure-state-import',
765
+ phaseId: phase.id,
766
+ inputs: { noChangePlanRequired: true },
767
+ destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
768
+ subscriptionId: 'phase-0-discovered-subscription'
769
+ }),
770
+ remote: true,
771
+ destructive: false
772
+ }),
773
+ ...writeOps()
774
+ ];
775
+ case 'remote-ready':
776
+ return [
777
+ operation({
778
+ adapter: 'azure-opentofu',
779
+ actionId: 'azure.remote-ready.verify',
780
+ mutationClass: 'azure-read',
781
+ phaseId: phase.id,
782
+ inputs: { retainBootstrapStateForDays: 30 },
783
+ destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
784
+ subscriptionId: 'phase-0-discovered-subscription'
785
+ }),
786
+ remote: true,
787
+ destructive: false
788
+ }),
789
+ ...writeOps()
790
+ ];
791
+ case 'application-foundation':
792
+ return [
793
+ operation({
794
+ adapter: 'azure-opentofu',
795
+ actionId: 'azure.application-foundation.apply',
796
+ mutationClass: 'azure-resource-provision',
797
+ phaseId: phase.id,
798
+ inputs: { noProviderUnregisterRollback: true },
799
+ destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
800
+ subscriptionId: 'phase-0-discovered-subscription'
801
+ }),
802
+ remote: true,
803
+ destructive: false
804
+ }),
805
+ ...writeOps()
806
+ ];
807
+ case 'workflow-source-ready':
808
+ return [
809
+ operation({
810
+ adapter: 'local-state',
811
+ actionId: 'local.workflow-source.write',
812
+ mutationClass: 'write-workflows',
813
+ phaseId: phase.id,
814
+ inputs: { path: '.github/workflows' },
815
+ destination: transitionDestination('local', '.github/workflows', {
816
+ pathParts: ['.github', 'workflows']
817
+ }),
818
+ remote: false,
819
+ destructive: false
820
+ }),
821
+ operation({
822
+ adapter: 'local-state',
823
+ actionId: 'local.ruleset-source.write',
824
+ mutationClass: 'write-ruleset-source',
825
+ phaseId: phase.id,
826
+ inputs: { path: '.github/rulesets' },
827
+ destination: transitionDestination('local', '.github/rulesets', {
828
+ pathParts: ['.github', 'rulesets']
829
+ }),
830
+ remote: false,
831
+ destructive: false
832
+ }),
833
+ ...writeOps()
834
+ ];
835
+ case 'dev-proof':
836
+ return [
837
+ operation({
838
+ adapter: 'github',
839
+ actionId: 'github.checks.dev-proof',
840
+ mutationClass: 'github-read',
841
+ phaseId: phase.id,
842
+ inputs: { acceptedConclusions: ['success'] },
843
+ destination: transitionDestination('repository', inspection.state.repository.name, {
844
+ repository: inspection.state.repository.name
845
+ }),
846
+ remote: true,
847
+ destructive: false
848
+ }),
849
+ ...writeOps()
850
+ ];
851
+ case 'staging-qualified':
852
+ return [
853
+ operation({
854
+ adapter: 'github',
855
+ actionId: 'github.checks.staging',
856
+ mutationClass: 'github-read',
857
+ phaseId: phase.id,
858
+ inputs: { acceptedConclusions: ['success'] },
859
+ destination: transitionDestination('repository', inspection.state.repository.name, {
860
+ repository: inspection.state.repository.name
861
+ }),
862
+ remote: true,
863
+ destructive: false
864
+ }),
865
+ ...writeOps()
866
+ ];
867
+ case 'production-rehearsed':
868
+ return [
869
+ operation({
870
+ adapter: 'github',
871
+ actionId: 'github.checks.production-rehearsal',
872
+ mutationClass: 'github-read',
873
+ phaseId: phase.id,
874
+ inputs: { acceptedConclusions: ['success'] },
875
+ destination: transitionDestination('repository', inspection.state.repository.name, {
876
+ repository: inspection.state.repository.name
877
+ }),
878
+ remote: true,
879
+ destructive: false
880
+ }),
881
+ ...writeOps()
882
+ ];
883
+ case 'green-red-proof':
884
+ return [
885
+ operation({
886
+ adapter: 'github',
887
+ actionId: 'github.checks.green-red-proof',
888
+ mutationClass: 'github-read',
889
+ phaseId: phase.id,
890
+ inputs: { required: ['green-success', 'deliberate-red-failure'] },
891
+ destination: transitionDestination('repository', inspection.state.repository.name, {
892
+ repository: inspection.state.repository.name
893
+ }),
894
+ remote: true,
895
+ destructive: false
896
+ }),
897
+ ...writeOps()
898
+ ];
899
+ case 'enforcement-approved':
900
+ return [stateWriteOperation(phase)];
901
+ case 'rulesets-applied':
902
+ return [
903
+ operation({
904
+ adapter: 'github',
905
+ actionId: 'github.ruleset.apply',
906
+ mutationClass: 'github-ruleset-write',
907
+ phaseId: phase.id,
908
+ inputs: { sourceDigest: rulesetSourceDigestFromEvidence(inspection) ?? 'missing' },
909
+ destination: transitionDestination('repository', inspection.state.repository.name, {
910
+ repository: inspection.state.repository.name
911
+ }),
912
+ remote: true,
913
+ destructive: false
914
+ }),
915
+ operation({
916
+ adapter: 'github',
917
+ actionId: 'github.ruleset.readback',
918
+ mutationClass: 'github-read',
919
+ phaseId: phase.id,
920
+ inputs: { sourceDigest: rulesetSourceDigestFromEvidence(inspection) ?? 'missing' },
921
+ destination: transitionDestination('repository', inspection.state.repository.name, {
922
+ repository: inspection.state.repository.name
923
+ }),
924
+ remote: true,
925
+ destructive: false
926
+ }),
927
+ ...writeOps()
928
+ ];
929
+ case 'live-readback':
930
+ return [
931
+ operation({
932
+ adapter: 'github',
933
+ actionId: 'github.ruleset.readback',
934
+ mutationClass: 'github-read',
935
+ phaseId: phase.id,
936
+ inputs: { sourceDigest: rulesetSourceDigestFromEvidence(inspection) ?? 'missing' },
937
+ destination: transitionDestination('repository', inspection.state.repository.name, {
938
+ repository: inspection.state.repository.name
939
+ }),
940
+ remote: true,
941
+ destructive: false
942
+ }),
943
+ ...writeOps()
944
+ ];
945
+ case 'bootstrap-state-disposed': {
946
+ const paths = disposalPathInputs(inspection.state.bootstrapState);
947
+ return [
948
+ operation({
949
+ adapter: 'local-state',
950
+ actionId: 'local.bootstrap-state.dispose',
951
+ mutationClass: 'delete-local-state',
952
+ phaseId: phase.id,
953
+ inputs: paths,
954
+ destination: transitionDestination('local', 'bootstrap-state', {
955
+ pathParts: statePath
956
+ }),
957
+ remote: false,
958
+ destructive: true
959
+ }),
960
+ ...writeOps()
961
+ ];
962
+ }
963
+ }
964
+ }
965
+ function disposalPathInputs(retention) {
966
+ return {
967
+ encryptedStatePathParts: retention?.encryptedStatePathParts ?? [],
968
+ encryptionKeyPathParts: retention?.encryptionKeyPathParts ?? []
969
+ };
970
+ }
971
+ function rulesetSourceDigestFromEvidence(inspection) {
972
+ const record = latestRecordWithPayload(inspection, 'workflow-source-ready');
973
+ if (!record || !isRecord(record.payload)) {
974
+ return null;
975
+ }
976
+ if (record.payload.kind !== workflowSourcePayloadKind) {
977
+ return null;
978
+ }
979
+ const digest = record.payload.rulesetSourceDigest;
980
+ return typeof digest === 'string' && /^[a-f0-9]{64}$/u.test(digest) ? digest : null;
981
+ }
982
+ function latestRecordWithPayload(inspection, phaseId) {
983
+ const records = inspection.evidence.filter((record) => record.header.phaseId === phaseId);
984
+ const selection = selectLatestPhaseEvidence(records, inspection.contexts[phaseId]);
985
+ if (!selection.selected) {
986
+ return null;
987
+ }
988
+ return records.find((record) => record.evidenceId === selection.selected?.evidenceId) ?? null;
989
+ }
990
+ function rollbackPlanForPhase(phase, completed = []) {
991
+ return rollbackPlanFromCompletedOperations(phase.id, phase.rollback.kind, phase.rollback.target, completed);
992
+ }
993
+ export function rollbackPlanFromCompletedOperations(phaseId, strategy, target, completed) {
994
+ const retained = [];
995
+ const cleanupWarnings = [];
996
+ const operations = [];
997
+ for (const op of [...completed].reverse()) {
998
+ if (op.mutationClass === 'azure-provider-register') {
999
+ retained.push(`${op.phaseId}:${op.actionId}:provider-registration`);
1000
+ continue;
1001
+ }
1002
+ if (op.actionId.toLowerCase().includes('unregister')) {
1003
+ cleanupWarnings.push(`Refused to generate provider unregister rollback for ${op.actionId}.`);
1004
+ continue;
1005
+ }
1006
+ if (op.mutationClass === 'github-ruleset-write') {
1007
+ operations.push({
1008
+ adapter: 'github',
1009
+ actionId: 'github.ruleset.disable',
1010
+ mutationClass: 'github-ruleset-write',
1011
+ phaseId,
1012
+ inputs: { fromOperation: op.actionId, cannotExpandScope: true },
1013
+ destination: op.destination,
1014
+ remote: true,
1015
+ destructive: false
1016
+ });
1017
+ continue;
1018
+ }
1019
+ if (op.mutationClass === 'azure-network-provision' || op.mutationClass === 'azure-resource-provision') {
1020
+ operations.push({
1021
+ adapter: 'azure-opentofu',
1022
+ actionId: 'azure.resource.cleanup',
1023
+ mutationClass: op.mutationClass,
1024
+ phaseId,
1025
+ inputs: { fromOperation: op.actionId, noProviderUnregister: true },
1026
+ destination: op.destination,
1027
+ remote: true,
1028
+ destructive: true
1029
+ });
1030
+ continue;
1031
+ }
1032
+ if (op.mutationClass === 'write-local-state' || op.mutationClass === 'delete-local-state') {
1033
+ operations.push({
1034
+ adapter: 'local-state',
1035
+ actionId: 'local.state.rollback',
1036
+ mutationClass: op.mutationClass,
1037
+ phaseId,
1038
+ inputs: { fromOperation: op.actionId },
1039
+ destination: op.destination,
1040
+ remote: false,
1041
+ destructive: op.mutationClass === 'delete-local-state'
1042
+ });
1043
+ }
1044
+ }
1045
+ return {
1046
+ phaseId,
1047
+ strategy,
1048
+ target,
1049
+ operations,
1050
+ retained,
1051
+ cleanupWarnings
1052
+ };
1053
+ }
1054
+ function planDigestFor(input) {
1055
+ return canonicalSha256({
1056
+ phaseId: input.phase.id,
1057
+ transitionDigest: input.transitionDigest,
1058
+ approvalPlanDigest: input.approvalPlanDigest,
1059
+ operations: input.operations
1060
+ });
1061
+ }
1062
+ function stateHash(inspection) {
1063
+ return inspection.loadedState?.contentHash ?? null;
1064
+ }
1065
+ function assertNoSecrets(plan) {
1066
+ const scan = detectCredentialLeaks([{
1067
+ source: 'generated-artifact',
1068
+ label: 'governance transition plan',
1069
+ text: canonicalJson(plan)
1070
+ }]);
1071
+ if (scan.status === 'compromised') {
1072
+ throw new Error(`Governance transition plan contains credential-shaped content: ${scan.leaks.map((leak) => leak.pattern).join(', ')}.`);
1073
+ }
1074
+ }
1075
+ export async function buildSavedTransitionPlan(input) {
1076
+ const phaseId = input.inspection.readiness.nextReadyPhase;
1077
+ if (!phaseId) {
1078
+ return null;
1079
+ }
1080
+ const runner = input.runner ?? new NodeCommandRunner();
1081
+ const phase = phaseById(input.inspection.graph, phaseId);
1082
+ const createdAt = (input.now ?? new Date()).toISOString();
1083
+ const expiresAt = new Date(Date.parse(createdAt) + planLifetimeMs).toISOString();
1084
+ const context = input.inspection.contexts[phaseId];
1085
+ const approvalPlan = transitionPlanForPhase(phase, input.inspection.state, context.transition, input.inspection.projectRoot);
1086
+ const evaluation = evaluateApprovalForTransitionPlan(approvalPlan, input.inspection.approvals, {
1087
+ now: input.now
1088
+ });
1089
+ const operations = await phaseOperations(input.inspection, phase, runner, createdAt);
1090
+ const digest = planDigestFor({
1091
+ phase,
1092
+ transitionDigest: context.transition.transitionDigest,
1093
+ operations,
1094
+ approvalPlanDigest: approvalPlan.planDigest
1095
+ });
1096
+ const plan = validateSavedTransitionPlan({
1097
+ schemaVersion: 1,
1098
+ phaseId,
1099
+ createdAt,
1100
+ expiresAt,
1101
+ identity: input.inspection.state.identity,
1102
+ graphHash: input.inspection.graphHash,
1103
+ stateHash: stateHash(input.inspection),
1104
+ baselineDigest: context.baselineSha,
1105
+ inputDigest: context.inputDigest,
1106
+ transitionDigest: context.transition.transitionDigest,
1107
+ planDigest: digest,
1108
+ mutationClasses: phase.allowedMutations,
1109
+ operations,
1110
+ approval: {
1111
+ gateKind: phase.approvalGate.kind,
1112
+ required: phase.approvalGate.required,
1113
+ evaluation,
1114
+ envelopeId: evaluation.envelopeId,
1115
+ envelopeHash: evaluation.envelopeHash
1116
+ },
1117
+ rollbackPlan: rollbackPlanForPhase(phase),
1118
+ noSecrets: true
1119
+ });
1120
+ assertPlanOperationsAllowed(plan, phase);
1121
+ assertNoSecrets(plan);
1122
+ return plan;
1123
+ }
1124
+ export async function previewApplyNext(input) {
1125
+ let plan = null;
1126
+ const blockers = [];
1127
+ try {
1128
+ plan = await buildSavedTransitionPlan(input);
1129
+ }
1130
+ catch (error) {
1131
+ blockers.push(errorMessage(error));
1132
+ }
1133
+ const nextReadyPhase = input.inspection.readiness.nextReadyPhase;
1134
+ if (!nextReadyPhase || blockers.length > 0 || !plan) {
1135
+ return {
1136
+ schemaVersion: 1,
1137
+ command: 'governance apply-next',
1138
+ projectRoot: input.inspection.projectRoot,
1139
+ execute: input.execute,
1140
+ applied: false,
1141
+ authorized: false,
1142
+ reason: 'blocked',
1143
+ message: blockers[0] ?? 'No phase is ready for execution.',
1144
+ nextReadyPhase,
1145
+ approval: plan?.approval ?? null,
1146
+ proposedMutations: {
1147
+ local: plan?.mutationClasses.local ?? ['none'],
1148
+ remote: plan?.mutationClasses.remote ?? ['none'],
1149
+ operations: plan?.operations ?? []
1150
+ },
1151
+ savedPlan: null,
1152
+ noWrites: true,
1153
+ blockers: blockers.length > 0 ? blockers : ['No phase is ready for execution.']
1154
+ };
1155
+ }
1156
+ if (plan.approval.evaluation.approvalRequired) {
1157
+ return {
1158
+ schemaVersion: 1,
1159
+ command: 'governance apply-next',
1160
+ projectRoot: input.inspection.projectRoot,
1161
+ execute: input.execute,
1162
+ applied: false,
1163
+ authorized: false,
1164
+ reason: 'approval-required',
1165
+ message: plan.approval.evaluation.reasons.join('; '),
1166
+ nextReadyPhase,
1167
+ approval: plan.approval,
1168
+ proposedMutations: {
1169
+ local: plan.mutationClasses.local,
1170
+ remote: plan.mutationClasses.remote,
1171
+ operations: plan.operations
1172
+ },
1173
+ savedPlan: null,
1174
+ noWrites: true,
1175
+ blockers: plan.approval.evaluation.reasons
1176
+ };
1177
+ }
1178
+ return {
1179
+ schemaVersion: 1,
1180
+ command: 'governance apply-next',
1181
+ projectRoot: input.inspection.projectRoot,
1182
+ execute: input.execute,
1183
+ applied: false,
1184
+ authorized: true,
1185
+ reason: input.execute ? 'execute-requested' : 'execute-required',
1186
+ message: input.execute
1187
+ ? 'Execution requested; the plan must be saved and revalidated before any mutation.'
1188
+ : 'Preview only. Rerun with --execute to save this plan and execute at most one phase.',
1189
+ nextReadyPhase,
1190
+ approval: plan.approval,
1191
+ proposedMutations: {
1192
+ local: plan.mutationClasses.local,
1193
+ remote: plan.mutationClasses.remote,
1194
+ operations: plan.operations
1195
+ },
1196
+ savedPlan: null,
1197
+ noWrites: true,
1198
+ blockers: []
1199
+ };
1200
+ }
1201
+ async function saveTransitionPlan(projectRoot, plan) {
1202
+ const validated = validateSavedTransitionPlan(plan);
1203
+ assertNoSecrets(validated);
1204
+ const pathParts = transitionPlanPathParts(validated);
1205
+ const existing = await readProjectFile(projectRoot, pathParts);
1206
+ if (existing !== undefined) {
1207
+ throw new Error(`Refusing to overwrite existing governance transition plan ${pathParts.join('/')}.`);
1208
+ }
1209
+ const content = `${canonicalJson(validated)}\n`;
1210
+ await applyProjectFileTransaction(projectRoot, [{
1211
+ type: 'write',
1212
+ pathParts,
1213
+ content
1214
+ }]);
1215
+ return { pathParts, digest: canonicalSha256(validated) };
1216
+ }
1217
+ function comparePlanFreshness(saved, fresh) {
1218
+ if (!fresh) {
1219
+ return ['No phase remained ready after saving the transition plan.'];
1220
+ }
1221
+ const issues = [];
1222
+ if (saved.phaseId !== fresh.phaseId) {
1223
+ issues.push(`Ready phase changed from ${saved.phaseId} to ${fresh.phaseId}.`);
1224
+ }
1225
+ for (const field of ['stateHash', 'baselineDigest', 'inputDigest', 'transitionDigest']) {
1226
+ if (saved[field] !== fresh[field]) {
1227
+ issues.push(`${field} changed after plan save.`);
1228
+ }
1229
+ }
1230
+ if (canonicalSha256(saved.operations) !== canonicalSha256(fresh.operations)) {
1231
+ issues.push('Proposed operations changed after plan save.');
1232
+ }
1233
+ if (saved.approval.evaluation.approvalRequired || fresh.approval.evaluation.approvalRequired) {
1234
+ issues.push('Approval was not valid immediately before execution.');
1235
+ }
1236
+ return issues;
1237
+ }
1238
+ function evidenceHeaderFor(input) {
1239
+ const context = input.inspection.contexts[input.phase.id];
1240
+ return validateEvidenceHeader({
1241
+ schemaVersion: input.inspection.state.identity.evidenceHeaderSchemaVersion,
1242
+ repositoryId: input.inspection.state.repository.id,
1243
+ identity: input.inspection.state.identity,
1244
+ phaseGraphHash: input.inspection.state.identity.phaseGraphHash,
1245
+ phaseId: input.phase.id,
1246
+ phaseContractDigest: phaseContractDigests(input.inspection.graph)[input.phase.id],
1247
+ inputDigest: input.plan.inputDigest,
1248
+ baselineSha: input.plan.baselineDigest,
1249
+ transition: context.transition,
1250
+ producedAt: input.now.toISOString(),
1251
+ producer: engineProducer,
1252
+ result: input.result
1253
+ });
1254
+ }
1255
+ function appendUnique(values, value) {
1256
+ if (value === null || values.includes(value)) {
1257
+ return [...values];
1258
+ }
1259
+ return [...values, value];
1260
+ }
1261
+ function nextStateForOutcome(input) {
1262
+ const base = cloneState(input.override ?? input.inspection.state);
1263
+ if (input.inspection.sourceOfTruth.status === 'selected' &&
1264
+ input.inspection.sourceOfTruth.recordActiveChangeOnNextMutation) {
1265
+ const selected = stateWithSelectedActiveChange(base, input.inspection.sourceOfTruth.selected);
1266
+ base.activeChange = selected.activeChange;
1267
+ }
1268
+ const phaseState = input.resultState === 'approved' ? 'approved' : input.resultState;
1269
+ base.phases[input.phase.id] = {
1270
+ state: phaseState,
1271
+ updatedAt: input.now.toISOString(),
1272
+ evidence: input.evidenceReference
1273
+ ? [...base.phases[input.phase.id].evidence, input.evidenceReference]
1274
+ : base.phases[input.phase.id].evidence,
1275
+ approvals: appendUnique(base.phases[input.phase.id].approvals, input.plan.approval.envelopeId),
1276
+ blockers: input.blocker ? [input.blocker] : []
1277
+ };
1278
+ base.updatedAt = input.now.toISOString();
1279
+ return validateUserActivationState(base);
1280
+ }
1281
+ function blockedState(input) {
1282
+ const base = cloneState(input.inspection.state);
1283
+ base.phases[input.phase.id] = {
1284
+ state: 'blocked',
1285
+ updatedAt: input.now.toISOString(),
1286
+ evidence: base.phases[input.phase.id].evidence,
1287
+ approvals: appendUnique(base.phases[input.phase.id].approvals, input.plan.approval.envelopeId),
1288
+ blockers: [input.blocker]
1289
+ };
1290
+ base.updatedAt = input.now.toISOString();
1291
+ return validateUserActivationState(base);
1292
+ }
1293
+ async function assertLoadedStateHash(projectRoot, expectedHash) {
1294
+ const bytes = await readProjectFile(projectRoot, activationStatePathParts());
1295
+ const currentHash = bytes === undefined ? null : activationStateContentHash(bytes);
1296
+ if (currentHash !== expectedHash) {
1297
+ throw new Error(`Activation state changed after inspection: expected ${expectedHash ?? 'absent'}, found ${currentHash ?? 'absent'}.`);
1298
+ }
1299
+ }
1300
+ async function writeOutcomeTransaction(input) {
1301
+ await assertLoadedStateHash(input.projectRoot, input.plan.stateHash);
1302
+ const stateContent = `${canonicalJson(validateUserActivationState(input.nextState))}\n`;
1303
+ const mutations = [
1304
+ ...(input.fileMutations ?? []).map((mutation) => ({
1305
+ ...mutation,
1306
+ pathParts: validateArtifactPathParts([...mutation.pathParts], 'Governance transition file mutation path')
1307
+ })),
1308
+ ...(input.evidenceRecord && input.evidencePathParts
1309
+ ? [{
1310
+ type: 'write',
1311
+ pathParts: [...input.evidencePathParts],
1312
+ content: `${canonicalJson(input.evidenceRecord)}\n`
1313
+ }]
1314
+ : []),
1315
+ {
1316
+ type: 'write',
1317
+ pathParts: activationStatePathParts(),
1318
+ content: stateContent
1319
+ }
1320
+ ];
1321
+ if (input.evidencePathParts) {
1322
+ const existing = await readProjectFile(input.projectRoot, [...input.evidencePathParts]);
1323
+ if (existing !== undefined) {
1324
+ throw new Error(`Refusing to overwrite existing governance evidence ${input.evidencePathParts.join('/')}.`);
1325
+ }
1326
+ }
1327
+ await applyProjectFileTransaction(input.projectRoot, mutations);
1328
+ return {
1329
+ stateHash: activationStateContentHash(stateContent),
1330
+ evidence: input.evidenceRecord && input.evidencePathParts
1331
+ ? {
1332
+ evidenceId: input.evidenceRecord.evidenceId,
1333
+ pathParts: input.evidencePathParts,
1334
+ headerDigest: evidenceHeaderDigest(input.evidenceRecord.header),
1335
+ result: input.evidenceRecord.header.result
1336
+ }
1337
+ : null
1338
+ };
1339
+ }
1340
+ async function executeGitOperations(input, operations) {
1341
+ if (input.phase.id !== 'committed' && input.phase.id !== 'pushed') {
1342
+ return null;
1343
+ }
1344
+ const completed = [];
1345
+ for (const op of operations.filter((entry) => entry.adapter === 'git')) {
1346
+ if (op.actionId === 'git.verify-existing-commit' || op.actionId === 'git.verify-existing-push') {
1347
+ completed.push(op);
1348
+ continue;
1349
+ }
1350
+ if (op.actionId === 'git.init') {
1351
+ const result = await runGit(input.runner, input.inspection.projectRoot, ['init', '-b', String(op.inputs.branch)]);
1352
+ if (!commandSucceeded(result)) {
1353
+ return { status: 'blocked', blocker: commandFailure(result), completedOperations: completed };
1354
+ }
1355
+ completed.push(op);
1356
+ }
1357
+ else if (op.actionId === 'git.add-reviewed') {
1358
+ const paths = Array.isArray(op.inputs.paths) ? op.inputs.paths.map(String) : [];
1359
+ if (paths.length === 0) {
1360
+ completed.push(op);
1361
+ continue;
1362
+ }
1363
+ const result = await runGit(input.runner, input.inspection.projectRoot, ['add', '--', ...paths]);
1364
+ if (!commandSucceeded(result)) {
1365
+ return { status: 'blocked', blocker: commandFailure(result), completedOperations: completed };
1366
+ }
1367
+ completed.push(op);
1368
+ }
1369
+ else if (op.actionId === 'git.commit-reviewed') {
1370
+ const message = String(op.inputs.message);
1371
+ const result = await runGit(input.runner, input.inspection.projectRoot, ['commit', '-m', message]);
1372
+ if (!commandSucceeded(result)) {
1373
+ return { status: 'blocked', blocker: commandFailure(result), completedOperations: completed };
1374
+ }
1375
+ completed.push(op);
1376
+ }
1377
+ else if (op.actionId === 'git.push-approved-ref') {
1378
+ const branch = String(op.inputs.branch);
1379
+ const result = await runGit(input.runner, input.inspection.projectRoot, ['push', 'origin', `HEAD:refs/heads/${branch}`]);
1380
+ if (!commandSucceeded(result)) {
1381
+ return { status: 'blocked', blocker: commandFailure(result), completedOperations: completed };
1382
+ }
1383
+ completed.push(op);
1384
+ }
1385
+ }
1386
+ return {
1387
+ status: 'completed',
1388
+ resultState: 'verified',
1389
+ completedOperations: completed
1390
+ };
1391
+ }
1392
+ async function executeSeedOperations(input) {
1393
+ if (!input.phase.id.startsWith('seed-')) {
1394
+ return null;
1395
+ }
1396
+ if (input.phase.id === 'seed-valid') {
1397
+ const result = await validateGeneratedSeedForPhase(input.inspection.projectRoot, input.runner);
1398
+ if (result.status === 'blocked') {
1399
+ return { status: 'blocked', blocker: result.issues[0] ?? 'Seed validation failed.', completedOperations: [] };
1400
+ }
1401
+ return { status: 'completed', resultState: 'verified', completedOperations: input.plan.operations.filter((op) => op.actionId === 'openspec.seed.validate') };
1402
+ }
1403
+ if (input.phase.id === 'seed-verified') {
1404
+ const result = await verifyGeneratedSeedBaselineForPhase(input.inspection.projectRoot, input.runner);
1405
+ if (result.status === 'blocked') {
1406
+ return { status: 'blocked', blocker: result.issues[0] ?? 'Seed baseline verification failed.', completedOperations: [] };
1407
+ }
1408
+ return {
1409
+ status: 'completed',
1410
+ resultState: 'verified',
1411
+ evidencePayload: {
1412
+ kind: 'seed-verified.v1',
1413
+ checks: result.checks.map((check) => ({
1414
+ id: check.id,
1415
+ taskId: check.taskId,
1416
+ status: check.status
1417
+ }))
1418
+ },
1419
+ completedOperations: input.plan.operations.filter((op) => op.actionId === 'openspec.seed.baseline-verify')
1420
+ };
1421
+ }
1422
+ const result = await archiveGeneratedSeedForPhase(input.inspection.projectRoot, input.runner);
1423
+ if (result.status === 'blocked') {
1424
+ return { status: 'blocked', blocker: result.issues[0] ?? 'Seed archive failed.', completedOperations: [] };
1425
+ }
1426
+ return {
1427
+ status: 'completed',
1428
+ resultState: 'verified',
1429
+ evidencePayload: {
1430
+ kind: 'seed-archived.v1',
1431
+ archiveSyncBehavior: result.archiveSyncBehavior ?? null
1432
+ },
1433
+ completedOperations: input.plan.operations.filter((op) => op.actionId === 'openspec.seed.archive')
1434
+ };
1435
+ }
1436
+ async function discoverPhase0(input) {
1437
+ if (input.phase.id !== 'phase-0-complete') {
1438
+ return null;
1439
+ }
1440
+ const gh = await runCommand(input.runner, {
1441
+ executable: 'gh',
1442
+ args: ['repo', 'view', '--json', 'id,nameWithOwner,defaultBranchRef,isPrivate']
1443
+ }, input.inspection.projectRoot);
1444
+ if (!commandSucceeded(gh)) {
1445
+ return {
1446
+ status: 'blocked',
1447
+ blocker: `Phase 0 GitHub read-only discovery failed: ${commandFailure(gh)}`,
1448
+ completedOperations: []
1449
+ };
1450
+ }
1451
+ let repo;
1452
+ try {
1453
+ repo = JSON.parse(gh.stdout);
1454
+ }
1455
+ catch (error) {
1456
+ return {
1457
+ status: 'blocked',
1458
+ blocker: `Phase 0 GitHub discovery returned invalid JSON: ${errorMessage(error)}`,
1459
+ completedOperations: []
1460
+ };
1461
+ }
1462
+ if (!repo.id || !repo.nameWithOwner || !repo.defaultBranchRef?.name) {
1463
+ return {
1464
+ status: 'blocked',
1465
+ blocker: 'Phase 0 GitHub discovery did not return repository id, nameWithOwner, and default branch.',
1466
+ completedOperations: []
1467
+ };
1468
+ }
1469
+ const az = await runCommand(input.runner, {
1470
+ executable: 'az',
1471
+ args: ['account', 'show', '--output', 'json']
1472
+ }, input.inspection.projectRoot);
1473
+ const azureAccount = commandSucceeded(az) ? safeJson(az.stdout) : null;
1474
+ const git = await inspectGitRepository(input.inspection.projectRoot, input.runner);
1475
+ const baselineDigest = canonicalSha256({
1476
+ gitHead: git.head,
1477
+ repositoryId: repo.id,
1478
+ repositoryName: repo.nameWithOwner,
1479
+ defaultBranch: repo.defaultBranchRef.name
1480
+ });
1481
+ const facts = {
1482
+ repositoryId: repo.id,
1483
+ repositoryName: repo.nameWithOwner,
1484
+ defaultBranch: repo.defaultBranchRef.name,
1485
+ baselineDigest,
1486
+ privateStagingDast: false,
1487
+ credentialRequired: false,
1488
+ statePath: 'none',
1489
+ approvedFacts: [
1490
+ { id: 'repository.id', value: repo.id },
1491
+ { id: 'repository.nameWithOwner', value: repo.nameWithOwner },
1492
+ { id: 'repository.defaultBranch', value: repo.defaultBranchRef.name },
1493
+ { id: 'repository.isPrivate', value: repo.isPrivate ?? null },
1494
+ { id: 'azure.accountReadable', value: azureAccount !== null }
1495
+ ]
1496
+ };
1497
+ const nextState = cloneState(input.inspection.state);
1498
+ nextState.repository = {
1499
+ id: facts.repositoryId,
1500
+ name: facts.repositoryName,
1501
+ defaultBranch: facts.defaultBranch
1502
+ };
1503
+ nextState.applicability = {
1504
+ statePath: facts.statePath,
1505
+ privateStagingDast: facts.privateStagingDast,
1506
+ credentialRequired: facts.credentialRequired
1507
+ };
1508
+ return {
1509
+ status: 'completed',
1510
+ resultState: 'verified',
1511
+ evidencePayload: {
1512
+ kind: 'phase-0-discovery.v1',
1513
+ facts: facts.approvedFacts,
1514
+ activationCreateChangePlan: {
1515
+ deterministic: true,
1516
+ graphHash: input.inspection.graphHash
1517
+ }
1518
+ },
1519
+ stateOverride: nextState,
1520
+ completedOperations: input.plan.operations.filter((op) => op.actionId === 'github.phase0.discover' || op.actionId === 'azure.phase0.discover')
1521
+ };
1522
+ }
1523
+ function safeJson(value) {
1524
+ try {
1525
+ return JSON.parse(value);
1526
+ }
1527
+ catch {
1528
+ return null;
1529
+ }
1530
+ }
1531
+ async function executeActivationApproval(input) {
1532
+ if (input.phase.id !== 'activation-approved') {
1533
+ return null;
1534
+ }
1535
+ const state = cloneState(input.inspection.state);
1536
+ const fileMutations = [];
1537
+ if (input.inspection.sourceOfTruth.status === 'none') {
1538
+ const facts = buildApprovedPhase0FactsFromState(input.inspection.manifest, input.inspection.state, input.inspection.evidence);
1539
+ if (!facts || input.inspection.sourceOfTruth.createPlan.status !== 'ready') {
1540
+ return {
1541
+ status: 'blocked',
1542
+ blocker: input.inspection.sourceOfTruth.createPlan.reason,
1543
+ completedOperations: []
1544
+ };
1545
+ }
1546
+ const writePlan = renderGovernanceChangeWritePlan(facts);
1547
+ for (const file of writePlan.files) {
1548
+ const existing = await readProjectFile(input.inspection.projectRoot, [...file.pathParts]);
1549
+ if (existing !== undefined) {
1550
+ return {
1551
+ status: 'blocked',
1552
+ blocker: `Refusing to overwrite existing governance artifact ${file.pathParts.join('/')}.`,
1553
+ completedOperations: []
1554
+ };
1555
+ }
1556
+ fileMutations.push({
1557
+ type: 'write',
1558
+ pathParts: [...file.pathParts],
1559
+ content: file.content
1560
+ });
1561
+ }
1562
+ state.activeChange = {
1563
+ id: writePlan.changeId,
1564
+ kind: writePlan.workflowKind === 'openspec' ? 'openspec' : 'spec-kit'
1565
+ };
1566
+ }
1567
+ else if (input.inspection.sourceOfTruth.status === 'selected') {
1568
+ state.activeChange = {
1569
+ id: input.inspection.sourceOfTruth.selected.changeId,
1570
+ kind: input.inspection.sourceOfTruth.selected.workflowKind === 'openspec' ? 'openspec' : 'spec-kit'
1571
+ };
1572
+ }
1573
+ else {
1574
+ return {
1575
+ status: 'blocked',
1576
+ blocker: 'A compatible governance source of truth is required before activation approval can be recorded.',
1577
+ completedOperations: []
1578
+ };
1579
+ }
1580
+ return {
1581
+ status: 'completed',
1582
+ resultState: 'approved',
1583
+ stateOverride: state,
1584
+ fileMutations,
1585
+ completedOperations: input.plan.operations.filter((op) => op.actionId.endsWith('governance.create-change'))
1586
+ };
1587
+ }
1588
+ async function executeCredentialReady(input) {
1589
+ if (input.phase.id !== 'credential-ready') {
1590
+ return null;
1591
+ }
1592
+ const policy = await readProjectFile(input.inspection.projectRoot, [...credentialPolicyPathParts]);
1593
+ if (policy === undefined) {
1594
+ return {
1595
+ status: 'blocked',
1596
+ blocker: 'Credential enrollment requires a secure masked input channel or an existing selected-repository App; automated noninteractive apply cannot collect a PAT.',
1597
+ completedOperations: []
1598
+ };
1599
+ }
1600
+ const scan = detectCredentialLeaks([{
1601
+ source: 'imported-evidence',
1602
+ label: credentialPolicyPathParts.join('/'),
1603
+ text: policy.toString('utf8')
1604
+ }]);
1605
+ if (scan.status === 'compromised') {
1606
+ return {
1607
+ status: 'blocked',
1608
+ blocker: scan.guidance.join(' '),
1609
+ completedOperations: []
1610
+ };
1611
+ }
1612
+ return {
1613
+ status: 'completed',
1614
+ resultState: 'verified',
1615
+ evidencePayload: {
1616
+ kind: 'credential-ready.v1',
1617
+ policyPathParts: credentialPolicyPathParts,
1618
+ payloadFree: true,
1619
+ policyDigest: canonicalSha256(policy.toString('utf8'))
1620
+ },
1621
+ completedOperations: input.plan.operations.filter((op) => op.actionId === 'github.credential.verify-policy')
1622
+ };
1623
+ }
1624
+ function remoteImportRetention(input) {
1625
+ if (input.phase.id !== 'remote-ready') {
1626
+ return null;
1627
+ }
1628
+ if (input.inspection.state.applicability.statePath !== 'bootstrap-local') {
1629
+ return {
1630
+ status: 'completed',
1631
+ resultState: 'verified',
1632
+ evidencePayload: { kind: 'remote-ready.v1', retention: 'not-applicable' },
1633
+ completedOperations: input.plan.operations.filter((op) => op.actionId === 'azure.remote-ready.verify')
1634
+ };
1635
+ }
1636
+ const importRecord = latestRecordWithPayload(input.inspection, 'remote-import-verified');
1637
+ if (!importRecord || !isRecord(importRecord.payload) || importRecord.payload.kind !== remoteImportPayloadKind) {
1638
+ return {
1639
+ status: 'blocked',
1640
+ blocker: 'Remote import retention requires immutable remote-import evidence with state and key identifiers.',
1641
+ completedOperations: []
1642
+ };
1643
+ }
1644
+ const encryptedStatePathParts = pathPartLists(importRecord.payload.encryptedStatePathParts, 'encryptedStatePathParts');
1645
+ const encryptionKeyPathParts = pathPartLists(importRecord.payload.encryptionKeyPathParts, 'encryptionKeyPathParts');
1646
+ const retainedAt = input.now.toISOString();
1647
+ const disposeAfter = new Date(input.now.getTime() + 30 * dayMs).toISOString();
1648
+ const state = cloneState(input.inspection.state);
1649
+ state.bootstrapState = {
1650
+ status: 'retained',
1651
+ remoteImportEvidenceId: importRecord.evidenceId,
1652
+ remoteImportEvidenceDigest: evidenceHeaderDigest(importRecord.header),
1653
+ retainedAt,
1654
+ disposeAfter,
1655
+ encryptedStatePathParts,
1656
+ encryptionKeyPathParts
1657
+ };
1658
+ return {
1659
+ status: 'completed',
1660
+ resultState: 'retained',
1661
+ stateOverride: state,
1662
+ evidencePayload: {
1663
+ kind: 'remote-ready.v1',
1664
+ retention: state.bootstrapState
1665
+ },
1666
+ completedOperations: input.plan.operations.filter((op) => op.actionId === 'azure.remote-ready.verify')
1667
+ };
1668
+ }
1669
+ function pathPartLists(value, label) {
1670
+ if (!Array.isArray(value)) {
1671
+ throw new Error(`${label} must be an array of path-part arrays.`);
1672
+ }
1673
+ return value.map((entry, index) => validateArtifactPathParts(entry, `${label}[${index}]`));
1674
+ }
1675
+ async function executeBootstrapStateDisposal(input) {
1676
+ if (input.phase.id !== 'bootstrap-state-disposed') {
1677
+ return null;
1678
+ }
1679
+ const retention = input.inspection.state.bootstrapState;
1680
+ if (!retention || input.inspection.state.applicability.statePath !== 'bootstrap-local') {
1681
+ return {
1682
+ status: 'completed',
1683
+ resultState: 'inapplicable',
1684
+ evidencePayload: { kind: bootstrapStateDisposedPayloadKind, reason: 'no retained bootstrap state' },
1685
+ completedOperations: []
1686
+ };
1687
+ }
1688
+ if (retention.status === 'disposed') {
1689
+ return {
1690
+ status: 'completed',
1691
+ resultState: 'disposed',
1692
+ evidencePayload: { kind: bootstrapStateDisposedPayloadKind, reason: 'already disposed', deletionEvidenceId: retention.deletionEvidenceId ?? null },
1693
+ completedOperations: []
1694
+ };
1695
+ }
1696
+ if (Date.parse(retention.disposeAfter) > input.now.getTime()) {
1697
+ return {
1698
+ status: 'blocked',
1699
+ blocker: `Retained bootstrap state is not disposable until ${retention.disposeAfter}.`,
1700
+ completedOperations: []
1701
+ };
1702
+ }
1703
+ const importRecord = input.inspection.evidence.find((record) => record.evidenceId === retention.remoteImportEvidenceId);
1704
+ if (!importRecord || evidenceHeaderDigest(importRecord.header) !== retention.remoteImportEvidenceDigest) {
1705
+ return {
1706
+ status: 'blocked',
1707
+ blocker: 'Destructive disposal requires the immutable remote-import evidence referenced by retained bootstrap state.',
1708
+ completedOperations: []
1709
+ };
1710
+ }
1711
+ if (!isRecord(importRecord.payload) || importRecord.payload.kind !== remoteImportPayloadKind) {
1712
+ return {
1713
+ status: 'blocked',
1714
+ blocker: 'Destructive disposal requires verified remote backend and no-change evidence.',
1715
+ completedOperations: []
1716
+ };
1717
+ }
1718
+ const remoteBackendDigest = importRecord.payload.remoteBackendDigest;
1719
+ const noChangePlanDigest = importRecord.payload.noChangePlanDigest;
1720
+ if (typeof remoteBackendDigest !== 'string' ||
1721
+ !/^[a-f0-9]{64}$/u.test(remoteBackendDigest) ||
1722
+ typeof noChangePlanDigest !== 'string' ||
1723
+ !/^[a-f0-9]{64}$/u.test(noChangePlanDigest)) {
1724
+ return {
1725
+ status: 'blocked',
1726
+ blocker: 'Destructive disposal requires payload-free remote backend and no-change plan digests.',
1727
+ completedOperations: []
1728
+ };
1729
+ }
1730
+ const allPaths = [...retention.encryptedStatePathParts, ...retention.encryptionKeyPathParts]
1731
+ .map((parts) => validateArtifactPathParts([...parts], 'Bootstrap disposal path'));
1732
+ const incompleteCleanup = [];
1733
+ for (const parts of allPaths) {
1734
+ try {
1735
+ const target = await resolveProjectPath(input.inspection.projectRoot, parts);
1736
+ const details = await stat(target);
1737
+ if (!details.isFile()) {
1738
+ incompleteCleanup.push(`${parts.join('/')} is not a regular file.`);
1739
+ }
1740
+ }
1741
+ catch (error) {
1742
+ incompleteCleanup.push(`${parts.join('/')} was already absent before disposal: ${errorMessage(error)}`);
1743
+ }
1744
+ }
1745
+ const state = cloneState(input.inspection.state);
1746
+ state.bootstrapState = {
1747
+ ...retention,
1748
+ status: 'disposed',
1749
+ disposedAt: input.now.toISOString(),
1750
+ deletionEvidenceId: `${input.phase.id}-${safeTimestamp(input.now.toISOString())}`,
1751
+ incompleteCleanup
1752
+ };
1753
+ return {
1754
+ status: 'completed',
1755
+ resultState: 'disposed',
1756
+ stateOverride: state,
1757
+ evidencePayload: {
1758
+ kind: bootstrapStateDisposedPayloadKind,
1759
+ deletedPathParts: allPaths,
1760
+ remoteBackendDigest,
1761
+ noChangePlanDigest,
1762
+ incompleteCleanup,
1763
+ payloadFree: true
1764
+ },
1765
+ fileMutations: allPaths.map((parts) => ({ type: 'delete', pathParts: parts })),
1766
+ completedOperations: input.plan.operations.filter((op) => op.actionId === 'local.bootstrap-state.dispose'),
1767
+ cleanupWarnings: incompleteCleanup
1768
+ };
1769
+ }
1770
+ function assertGreenRedProof(input) {
1771
+ const record = latestRecordWithPayload(input.inspection, 'green-red-proof');
1772
+ if (!record || !isRecord(record.payload) || record.payload.kind !== greenRedPayloadKind) {
1773
+ throw new Error('Ruleset enforcement requires current green-red-proof evidence with typed proof payload.');
1774
+ }
1775
+ const green = isRecord(record.payload.green) ? record.payload.green : null;
1776
+ const red = isRecord(record.payload.deliberateRed) ? record.payload.deliberateRed : null;
1777
+ const greenConclusion = green?.conclusion;
1778
+ const redConclusion = red?.conclusion;
1779
+ const badConclusions = [greenConclusion, redConclusion].filter((value) => value === 'skipped' || value === 'cancelled' || value === 'neutral');
1780
+ if (badConclusions.length > 0) {
1781
+ throw new Error('Skipped, cancelled, and neutral checks cannot satisfy green/red proof.');
1782
+ }
1783
+ if (!red || greenConclusion !== 'success' || redConclusion !== 'failure' || red.deliberate !== true) {
1784
+ throw new Error('Ruleset enforcement requires green success and deliberate red failure proof.');
1785
+ }
1786
+ return record;
1787
+ }
1788
+ async function executeRulesetPhase(input) {
1789
+ if (input.phase.id !== 'rulesets-applied' && input.phase.id !== 'live-readback') {
1790
+ return null;
1791
+ }
1792
+ try {
1793
+ assertGreenRedProof(input);
1794
+ }
1795
+ catch (error) {
1796
+ return { status: 'blocked', blocker: errorMessage(error), completedOperations: [] };
1797
+ }
1798
+ const sourceDigest = rulesetSourceDigestFromEvidence(input.inspection);
1799
+ if (!sourceDigest) {
1800
+ return {
1801
+ status: 'blocked',
1802
+ blocker: 'Ruleset enforcement requires a saved ruleset source digest from workflow-source-ready evidence.',
1803
+ completedOperations: []
1804
+ };
1805
+ }
1806
+ const adapter = input.adapters.githubRulesets;
1807
+ if (!adapter) {
1808
+ return {
1809
+ status: 'blocked',
1810
+ blocker: 'No injected GitHub ruleset adapter is configured; refusing live ruleset calls in this execution context.',
1811
+ completedOperations: []
1812
+ };
1813
+ }
1814
+ const envelopeId = input.plan.approval.envelopeId;
1815
+ if (input.phase.id === 'rulesets-applied' && !envelopeId) {
1816
+ return {
1817
+ status: 'blocked',
1818
+ blocker: 'Ruleset enforcement requires a persisted enforcement approval envelope.',
1819
+ completedOperations: []
1820
+ };
1821
+ }
1822
+ const completed = [];
1823
+ const write = input.phase.id === 'rulesets-applied'
1824
+ ? await adapter.applyRuleset({
1825
+ repository: input.inspection.state.repository.name,
1826
+ sourceDigest,
1827
+ approvalEnvelopeId: envelopeId ?? 'ungated-readback'
1828
+ })
1829
+ : await adapter.readRuleset({
1830
+ repository: input.inspection.state.repository.name,
1831
+ sourceDigest
1832
+ });
1833
+ completed.push(...input.plan.operations.filter((op) => input.phase.id === 'rulesets-applied'
1834
+ ? op.actionId === 'github.ruleset.apply' || op.actionId === 'github.ruleset.readback'
1835
+ : op.actionId === 'github.ruleset.readback'));
1836
+ if (write.sourceDigest !== sourceDigest || write.readbackDigest !== sourceDigest) {
1837
+ return {
1838
+ status: 'blocked',
1839
+ blocker: 'Post-write live ruleset readback did not match the saved source digest.',
1840
+ completedOperations: completed
1841
+ };
1842
+ }
1843
+ const header = evidenceHeaderFor({
1844
+ inspection: input.inspection,
1845
+ phase: input.phase,
1846
+ plan: input.plan,
1847
+ result: 'verified',
1848
+ now: input.now
1849
+ });
1850
+ const liveReadback = {
1851
+ schemaVersion: input.inspection.state.identity.evidenceHeaderSchemaVersion,
1852
+ repositoryId: header.repositoryId,
1853
+ identity: header.identity,
1854
+ phaseGraphHash: header.phaseGraphHash,
1855
+ phaseId: header.phaseId,
1856
+ baselineSha: header.baselineSha,
1857
+ inputDigest: header.inputDigest,
1858
+ transition: header.transition,
1859
+ observedAt: input.now.toISOString(),
1860
+ provider: 'github',
1861
+ resourceType: 'ruleset',
1862
+ resourceId: write.resourceId,
1863
+ sourceDigest,
1864
+ readbackDigest: write.readbackDigest,
1865
+ matches: true
1866
+ };
1867
+ return {
1868
+ status: 'completed',
1869
+ resultState: 'verified',
1870
+ liveReadback: [liveReadback],
1871
+ evidencePayload: {
1872
+ kind: `${input.phase.id}.v1`,
1873
+ sourceDigest,
1874
+ resourceId: write.resourceId,
1875
+ readbackDigest: write.readbackDigest
1876
+ },
1877
+ completedOperations: completed
1878
+ };
1879
+ }
1880
+ async function executeBuiltInPhase(input) {
1881
+ const custom = input.adapters.phases?.[input.phase.id];
1882
+ if (custom) {
1883
+ return await custom.execute(input);
1884
+ }
1885
+ for (const attempt of [
1886
+ executeSeedOperations,
1887
+ async (execution) => executeGitOperations(execution, execution.plan.operations),
1888
+ discoverPhase0,
1889
+ executeActivationApproval,
1890
+ executeCredentialReady,
1891
+ async (execution) => remoteImportRetention(execution),
1892
+ executeBootstrapStateDisposal,
1893
+ executeRulesetPhase
1894
+ ]) {
1895
+ const outcome = await attempt(input);
1896
+ if (outcome) {
1897
+ return outcome;
1898
+ }
1899
+ }
1900
+ return {
1901
+ status: 'blocked',
1902
+ blocker: `No production adapter is configured for ${input.phase.id}; refusing success-shaped fallback.`,
1903
+ completedOperations: []
1904
+ };
1905
+ }
1906
+ function sourceOfTruthAllowsPhase(inspection, phaseId) {
1907
+ if (phaseOrder(phaseId) <= phaseOrder('phase-0-complete')) {
1908
+ return null;
1909
+ }
1910
+ const source = inspection.sourceOfTruth;
1911
+ if (phaseId === 'activation-approved' && (source.status === 'none' || source.status === 'selected')) {
1912
+ return null;
1913
+ }
1914
+ if (source.status === 'selected' && source.reconciliation.status === 'not-required') {
1915
+ return null;
1916
+ }
1917
+ if (source.status === 'seed-blocked') {
1918
+ return source.blockers.join('; ');
1919
+ }
1920
+ if (source.status === 'ambiguous' || source.status === 'incompatible') {
1921
+ return source.blockers.join('; ');
1922
+ }
1923
+ if (source.status === 'none') {
1924
+ return source.createPlan.reason;
1925
+ }
1926
+ return 'Active governance source of truth is not ready.';
1927
+ }
1928
+ export async function executeApplyNext(input) {
1929
+ const runner = input.runner ?? new NodeCommandRunner();
1930
+ const adapters = input.adapters ?? {};
1931
+ const now = input.now ?? new Date();
1932
+ const initialPlan = await buildSavedTransitionPlan({ inspection: input.inspection, runner, now });
1933
+ if (!initialPlan) {
1934
+ const preview = await previewApplyNext({ inspection: input.inspection, runner, now, execute: true });
1935
+ return {
1936
+ ...preview,
1937
+ applied: false,
1938
+ noWrites: false,
1939
+ executedOperations: [],
1940
+ evidence: null,
1941
+ stateHash: null,
1942
+ rollbackPlan: rollbackPlanFromCompletedOperations(input.inspection.readiness.nextReadyPhase ?? 'seed-valid', 'none', null, []),
1943
+ cleanupWarnings: []
1944
+ };
1945
+ }
1946
+ const phase = phaseById(input.inspection.graph, initialPlan.phaseId);
1947
+ assertPlanOperationsAllowed(initialPlan, phase);
1948
+ if (initialPlan.approval.evaluation.approvalRequired) {
1949
+ const preview = await previewApplyNext({ inspection: input.inspection, runner, now, execute: true });
1950
+ return {
1951
+ ...preview,
1952
+ applied: false,
1953
+ noWrites: false,
1954
+ executedOperations: [],
1955
+ evidence: null,
1956
+ stateHash: null,
1957
+ rollbackPlan: initialPlan.rollbackPlan,
1958
+ cleanupWarnings: []
1959
+ };
1960
+ }
1961
+ const saved = await saveTransitionPlan(input.inspection.projectRoot, initialPlan);
1962
+ const freshInspection = await input.reinspect();
1963
+ const freshPlan = await buildSavedTransitionPlan({ inspection: freshInspection, runner, now });
1964
+ const freshnessIssues = comparePlanFreshness(initialPlan, freshPlan);
1965
+ if (freshnessIssues.length > 0) {
1966
+ return {
1967
+ schemaVersion: 1,
1968
+ command: 'governance apply-next',
1969
+ projectRoot: input.inspection.projectRoot,
1970
+ execute: true,
1971
+ applied: false,
1972
+ authorized: false,
1973
+ reason: 'stale-after-plan-save',
1974
+ message: freshnessIssues.join(' '),
1975
+ nextReadyPhase: freshInspection.readiness.nextReadyPhase,
1976
+ approval: initialPlan.approval,
1977
+ proposedMutations: {
1978
+ local: initialPlan.mutationClasses.local,
1979
+ remote: initialPlan.mutationClasses.remote,
1980
+ operations: initialPlan.operations
1981
+ },
1982
+ savedPlan: saved,
1983
+ noWrites: false,
1984
+ blockers: freshnessIssues,
1985
+ executedOperations: [],
1986
+ evidence: null,
1987
+ stateHash: null,
1988
+ rollbackPlan: initialPlan.rollbackPlan,
1989
+ cleanupWarnings: []
1990
+ };
1991
+ }
1992
+ const sourceBlocker = sourceOfTruthAllowsPhase(freshInspection, initialPlan.phaseId);
1993
+ if (sourceBlocker) {
1994
+ const nextState = blockedState({ inspection: freshInspection, phase, plan: initialPlan, blocker: sourceBlocker, now });
1995
+ const write = await writeOutcomeTransaction({
1996
+ projectRoot: freshInspection.projectRoot,
1997
+ plan: initialPlan,
1998
+ nextState
1999
+ });
2000
+ return executionBlockedResult(freshInspection, initialPlan, saved, sourceBlocker, [], write.stateHash);
2001
+ }
2002
+ const outcome = await executeBuiltInPhase({
2003
+ inspection: freshInspection,
2004
+ plan: initialPlan,
2005
+ phase,
2006
+ runner,
2007
+ adapters,
2008
+ now
2009
+ });
2010
+ const completedOperations = outcome.completedOperations ?? [];
2011
+ if (outcome.status === 'blocked') {
2012
+ const blocker = outcome.blocker ?? `Phase ${phase.id} blocked.`;
2013
+ const nextState = blockedState({ inspection: freshInspection, phase, plan: initialPlan, blocker, now });
2014
+ const write = await writeOutcomeTransaction({
2015
+ projectRoot: freshInspection.projectRoot,
2016
+ plan: initialPlan,
2017
+ nextState
2018
+ });
2019
+ return executionBlockedResult(freshInspection, initialPlan, saved, blocker, completedOperations, write.stateHash, outcome.cleanupWarnings ?? []);
2020
+ }
2021
+ const resultState = outcome.resultState ?? 'verified';
2022
+ let evidenceRecord;
2023
+ let evidenceParts;
2024
+ let evidenceReference;
2025
+ if (resultState !== 'approved') {
2026
+ const evidenceId = `${phase.id}-${safeTimestamp(now.toISOString())}`;
2027
+ const header = evidenceHeaderFor({
2028
+ inspection: freshInspection,
2029
+ phase,
2030
+ plan: initialPlan,
2031
+ result: resultState,
2032
+ now
2033
+ });
2034
+ evidenceRecord = {
2035
+ evidenceId,
2036
+ header,
2037
+ ...(outcome.liveReadback ? { liveReadback: outcome.liveReadback } : {}),
2038
+ ...(outcome.evidencePayload !== undefined ? { payload: outcome.evidencePayload } : {})
2039
+ };
2040
+ evidenceParts = evidencePathParts(evidenceId);
2041
+ evidenceReference = {
2042
+ phaseId: phase.id,
2043
+ evidenceId,
2044
+ headerDigest: evidenceHeaderDigest(header),
2045
+ result: header.result
2046
+ };
2047
+ }
2048
+ const nextState = nextStateForOutcome({
2049
+ inspection: freshInspection,
2050
+ phase,
2051
+ plan: initialPlan,
2052
+ resultState,
2053
+ evidenceReference,
2054
+ override: outcome.stateOverride,
2055
+ now
2056
+ });
2057
+ const write = await writeOutcomeTransaction({
2058
+ projectRoot: freshInspection.projectRoot,
2059
+ plan: initialPlan,
2060
+ nextState,
2061
+ evidenceRecord,
2062
+ evidencePathParts: evidenceParts,
2063
+ fileMutations: outcome.fileMutations
2064
+ });
2065
+ const rollbackPlan = rollbackPlanForPhase(phase, completedOperations);
2066
+ return {
2067
+ schemaVersion: 1,
2068
+ command: 'governance apply-next',
2069
+ projectRoot: freshInspection.projectRoot,
2070
+ execute: true,
2071
+ applied: true,
2072
+ authorized: true,
2073
+ reason: 'phase-executed',
2074
+ message: `Executed one phase: ${phase.id}.`,
2075
+ nextReadyPhase: phase.id,
2076
+ approval: initialPlan.approval,
2077
+ proposedMutations: {
2078
+ local: initialPlan.mutationClasses.local,
2079
+ remote: initialPlan.mutationClasses.remote,
2080
+ operations: initialPlan.operations
2081
+ },
2082
+ savedPlan: saved,
2083
+ noWrites: false,
2084
+ blockers: [],
2085
+ executedOperations: [
2086
+ ...completedOperations,
2087
+ ...(evidenceParts ? [evidenceWriteOperation(phase, evidenceParts)] : []),
2088
+ stateWriteOperation(phase)
2089
+ ],
2090
+ evidence: write.evidence,
2091
+ stateHash: write.stateHash,
2092
+ rollbackPlan,
2093
+ cleanupWarnings: [...rollbackPlan.cleanupWarnings, ...(outcome.cleanupWarnings ?? [])]
2094
+ };
2095
+ }
2096
+ function executionBlockedResult(inspection, plan, saved, blocker, completedOperations, stateHashValue, cleanupWarnings = []) {
2097
+ const phase = phaseById(inspection.graph, plan.phaseId);
2098
+ const rollbackPlan = rollbackPlanForPhase(phase, completedOperations);
2099
+ return {
2100
+ schemaVersion: 1,
2101
+ command: 'governance apply-next',
2102
+ projectRoot: inspection.projectRoot,
2103
+ execute: true,
2104
+ applied: false,
2105
+ authorized: false,
2106
+ reason: 'blocked',
2107
+ message: blocker,
2108
+ nextReadyPhase: plan.phaseId,
2109
+ approval: plan.approval,
2110
+ proposedMutations: {
2111
+ local: plan.mutationClasses.local,
2112
+ remote: plan.mutationClasses.remote,
2113
+ operations: plan.operations
2114
+ },
2115
+ savedPlan: saved,
2116
+ noWrites: false,
2117
+ blockers: [blocker],
2118
+ executedOperations: [
2119
+ ...completedOperations,
2120
+ stateWriteOperation(phase)
2121
+ ],
2122
+ evidence: null,
2123
+ stateHash: stateHashValue,
2124
+ rollbackPlan,
2125
+ cleanupWarnings: [...rollbackPlan.cleanupWarnings, ...cleanupWarnings]
2126
+ };
2127
+ }
2128
+ //# sourceMappingURL=transitions.js.map