@msn-control/liftoff 0.10.4 → 0.11.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 (643) hide show
  1. package/DEVELOPER.md +220 -77
  2. package/README.md +28 -28
  3. package/assets/supported-stack.json +3 -136
  4. package/dist/adapters/azure/governance-assessment.d.ts +2 -0
  5. package/dist/adapters/azure/governance-assessment.js +21 -0
  6. package/dist/adapters/azure/governance-assessment.js.map +1 -0
  7. package/dist/adapters/filesystem/atomic-write.d.ts +2 -0
  8. package/dist/adapters/filesystem/atomic-write.js +74 -0
  9. package/dist/adapters/filesystem/atomic-write.js.map +1 -0
  10. package/dist/adapters/filesystem/errors.d.ts +2 -0
  11. package/dist/adapters/filesystem/errors.js +8 -0
  12. package/dist/adapters/filesystem/errors.js.map +1 -0
  13. package/dist/adapters/filesystem/manifest-file.d.ts +1 -0
  14. package/dist/adapters/filesystem/manifest-file.js +21 -0
  15. package/dist/adapters/filesystem/manifest-file.js.map +1 -0
  16. package/dist/adapters/filesystem/project-config.d.ts +4 -0
  17. package/dist/adapters/filesystem/project-config.js +157 -0
  18. package/dist/adapters/filesystem/project-config.js.map +1 -0
  19. package/dist/adapters/filesystem/project-discovery.d.ts +2 -0
  20. package/dist/adapters/filesystem/project-discovery.js +30 -0
  21. package/dist/adapters/filesystem/project-discovery.js.map +1 -0
  22. package/dist/adapters/filesystem/project-files.d.ts +6 -0
  23. package/dist/adapters/filesystem/project-files.js +68 -0
  24. package/dist/adapters/filesystem/project-files.js.map +1 -0
  25. package/dist/adapters/filesystem/project-lock.d.ts +8 -0
  26. package/dist/adapters/filesystem/project-lock.js +164 -0
  27. package/dist/adapters/filesystem/project-lock.js.map +1 -0
  28. package/dist/adapters/filesystem/project-paths.d.ts +2 -0
  29. package/dist/adapters/filesystem/project-paths.js +60 -0
  30. package/dist/adapters/filesystem/project-paths.js.map +1 -0
  31. package/dist/adapters/filesystem/project-transaction.d.ts +23 -0
  32. package/dist/adapters/filesystem/project-transaction.js +158 -0
  33. package/dist/adapters/filesystem/project-transaction.js.map +1 -0
  34. package/dist/adapters/git/governance-assessment.d.ts +22 -0
  35. package/dist/adapters/git/governance-assessment.js +161 -0
  36. package/dist/adapters/git/governance-assessment.js.map +1 -0
  37. package/dist/adapters/github/governance-assessment.d.ts +2 -0
  38. package/dist/adapters/github/governance-assessment.js +18 -0
  39. package/dist/adapters/github/governance-assessment.js.map +1 -0
  40. package/dist/adapters/packaged-assets/package-root.d.ts +3 -0
  41. package/dist/adapters/packaged-assets/package-root.js +21 -0
  42. package/dist/adapters/packaged-assets/package-root.js.map +1 -0
  43. package/dist/adapters/packaged-assets/supported-stack.d.ts +4 -0
  44. package/dist/adapters/packaged-assets/supported-stack.js +18 -0
  45. package/dist/adapters/packaged-assets/supported-stack.js.map +1 -0
  46. package/dist/adapters/packaged-assets/template-assets.d.ts +23 -0
  47. package/dist/adapters/packaged-assets/template-assets.js +36 -0
  48. package/dist/adapters/packaged-assets/template-assets.js.map +1 -0
  49. package/dist/adapters/process/shell-command.d.ts +5 -0
  50. package/dist/adapters/process/shell-command.js +21 -0
  51. package/dist/adapters/process/shell-command.js.map +1 -0
  52. package/dist/application/context.d.ts +20 -0
  53. package/dist/application/context.js +2 -0
  54. package/dist/application/context.js.map +1 -0
  55. package/dist/application/diagnose/doctor.d.ts +21 -0
  56. package/dist/application/diagnose/doctor.js +586 -0
  57. package/dist/application/diagnose/doctor.js.map +1 -0
  58. package/dist/application/diagnose/generated-project.d.ts +1 -0
  59. package/dist/application/diagnose/generated-project.js +105 -0
  60. package/dist/application/diagnose/generated-project.js.map +1 -0
  61. package/dist/application/diagnose/validate.d.ts +6 -0
  62. package/dist/application/diagnose/validate.js +27 -0
  63. package/dist/application/diagnose/validate.js.map +1 -0
  64. package/dist/application/initialize/fixture.d.ts +2 -0
  65. package/dist/application/initialize/fixture.js +39 -0
  66. package/dist/application/initialize/fixture.js.map +1 -0
  67. package/dist/application/initialize/inputs.d.ts +3 -0
  68. package/dist/application/initialize/inputs.js +35 -0
  69. package/dist/application/initialize/inputs.js.map +1 -0
  70. package/dist/application/initialize/use-case.d.ts +23 -0
  71. package/dist/application/initialize/use-case.js +360 -0
  72. package/dist/application/initialize/use-case.js.map +1 -0
  73. package/dist/application/migrate/use-case.d.ts +7 -0
  74. package/dist/application/migrate/use-case.js +378 -0
  75. package/dist/application/migrate/use-case.js.map +1 -0
  76. package/dist/application/project/catalog.d.ts +46 -0
  77. package/dist/application/project/catalog.js +12 -0
  78. package/dist/application/project/catalog.js.map +1 -0
  79. package/dist/application/project/manifest.d.ts +2 -0
  80. package/dist/application/project/manifest.js +29 -0
  81. package/dist/application/project/manifest.js.map +1 -0
  82. package/dist/application/project/planning.d.ts +6 -0
  83. package/dist/application/project/planning.js +12 -0
  84. package/dist/application/project/planning.js.map +1 -0
  85. package/dist/application/update/planning.d.ts +40 -0
  86. package/dist/application/update/planning.js +293 -0
  87. package/dist/application/update/planning.js.map +1 -0
  88. package/dist/application/update/reporting.d.ts +42 -0
  89. package/dist/application/update/reporting.js +325 -0
  90. package/dist/application/update/reporting.js.map +1 -0
  91. package/dist/application/update/use-case.d.ts +8 -0
  92. package/dist/application/update/use-case.js +600 -0
  93. package/dist/application/update/use-case.js.map +1 -0
  94. package/dist/application/upgrade/use-case.d.ts +6 -0
  95. package/dist/application/upgrade/use-case.js +82 -0
  96. package/dist/application/upgrade/use-case.js.map +1 -0
  97. package/dist/args.d.ts +5 -65
  98. package/dist/args.js +4 -472
  99. package/dist/args.js.map +1 -1
  100. package/dist/artifact-lifecycle.d.ts +1 -21
  101. package/dist/artifact-lifecycle.js +1 -75
  102. package/dist/artifact-lifecycle.js.map +1 -1
  103. package/dist/catalogs.d.ts +1 -50
  104. package/dist/catalogs.js +1 -472
  105. package/dist/catalogs.js.map +1 -1
  106. package/dist/cli/args/contracts.d.ts +51 -0
  107. package/dist/cli/args/contracts.js +2 -0
  108. package/dist/cli/args/contracts.js.map +1 -0
  109. package/dist/cli/args/definitions.d.ts +6 -0
  110. package/dist/cli/args/definitions.js +187 -0
  111. package/dist/cli/args/definitions.js.map +1 -0
  112. package/dist/cli/args/help.d.ts +5 -0
  113. package/dist/cli/args/help.js +130 -0
  114. package/dist/cli/args/help.js.map +1 -0
  115. package/dist/cli/args/parser.d.ts +5 -0
  116. package/dist/cli/args/parser.js +145 -0
  117. package/dist/cli/args/parser.js.map +1 -0
  118. package/dist/cli/args/readers.d.ts +4 -0
  119. package/dist/cli/args/readers.js +19 -0
  120. package/dist/cli/args/readers.js.map +1 -0
  121. package/dist/cli/commands/diagnose.d.ts +4 -0
  122. package/dist/cli/commands/diagnose.js +12 -0
  123. package/dist/cli/commands/diagnose.js.map +1 -0
  124. package/dist/cli/commands/dispatch.d.ts +3 -0
  125. package/dist/cli/commands/dispatch.js +94 -0
  126. package/dist/cli/commands/dispatch.js.map +1 -0
  127. package/dist/cli/commands/help.d.ts +3 -0
  128. package/dist/cli/commands/help.js +32 -0
  129. package/dist/cli/commands/help.js.map +1 -0
  130. package/dist/cli/commands/helpers.d.ts +8 -0
  131. package/dist/cli/commands/helpers.js +97 -0
  132. package/dist/cli/commands/helpers.js.map +1 -0
  133. package/dist/cli/commands/initialize.d.ts +3 -0
  134. package/dist/cli/commands/initialize.js +7 -0
  135. package/dist/cli/commands/initialize.js.map +1 -0
  136. package/dist/cli/commands/migrate.d.ts +3 -0
  137. package/dist/cli/commands/migrate.js +8 -0
  138. package/dist/cli/commands/migrate.js.map +1 -0
  139. package/dist/cli/commands/plan.d.ts +3 -0
  140. package/dist/cli/commands/plan.js +34 -0
  141. package/dist/cli/commands/plan.js.map +1 -0
  142. package/dist/cli/commands/reference.d.ts +5 -0
  143. package/dist/cli/commands/reference.js +43 -0
  144. package/dist/cli/commands/reference.js.map +1 -0
  145. package/dist/cli/commands/update.d.ts +3 -0
  146. package/dist/cli/commands/update.js +9 -0
  147. package/dist/cli/commands/update.js.map +1 -0
  148. package/dist/cli/commands/upgrade.d.ts +3 -0
  149. package/dist/cli/commands/upgrade.js +7 -0
  150. package/dist/cli/commands/upgrade.js.map +1 -0
  151. package/dist/cli/context.d.ts +1 -0
  152. package/dist/cli/context.js +2 -0
  153. package/dist/cli/context.js.map +1 -0
  154. package/dist/cli/project-options.d.ts +3 -0
  155. package/dist/cli/project-options.js +41 -0
  156. package/dist/cli/project-options.js.map +1 -0
  157. package/dist/cli.d.ts +3 -3
  158. package/dist/cli.js +2 -2
  159. package/dist/cli.js.map +1 -1
  160. package/dist/commands.d.ts +4 -35
  161. package/dist/commands.js +3 -2969
  162. package/dist/commands.js.map +1 -1
  163. package/dist/container-validation.d.ts +1 -1
  164. package/dist/domain/governance/activation/approvals.d.ts +351 -0
  165. package/dist/domain/governance/activation/approvals.js +528 -0
  166. package/dist/domain/governance/activation/approvals.js.map +1 -0
  167. package/dist/domain/governance/activation/canonical-json.d.ts +4 -0
  168. package/dist/domain/governance/activation/canonical-json.js +39 -0
  169. package/dist/domain/governance/activation/canonical-json.js.map +1 -0
  170. package/dist/domain/governance/activation/capabilities.d.ts +7 -0
  171. package/dist/domain/governance/activation/capabilities.js +33 -0
  172. package/dist/domain/governance/activation/capabilities.js.map +1 -0
  173. package/dist/domain/governance/activation/evidence.d.ts +63 -0
  174. package/dist/domain/governance/activation/evidence.js +444 -0
  175. package/dist/domain/governance/activation/evidence.js.map +1 -0
  176. package/dist/domain/governance/activation/graph.d.ts +397 -0
  177. package/dist/domain/governance/activation/graph.js +441 -0
  178. package/dist/domain/governance/activation/graph.js.map +1 -0
  179. package/dist/domain/governance/activation/inputs.d.ts +25 -0
  180. package/dist/domain/governance/activation/inputs.js +63 -0
  181. package/dist/domain/governance/activation/inputs.js.map +1 -0
  182. package/dist/domain/governance/activation/operations.d.ts +19 -0
  183. package/dist/domain/governance/activation/operations.js +181 -0
  184. package/dist/domain/governance/activation/operations.js.map +1 -0
  185. package/dist/domain/governance/activation/readiness.d.ts +27 -0
  186. package/dist/domain/governance/activation/readiness.js +269 -0
  187. package/dist/domain/governance/activation/readiness.js.map +1 -0
  188. package/dist/domain/governance/activation/types.d.ts +399 -0
  189. package/dist/domain/governance/activation/types.js +109 -0
  190. package/dist/domain/governance/activation/types.js.map +1 -0
  191. package/dist/domain/governance/activation/validators.d.ts +22 -0
  192. package/dist/domain/governance/activation/validators.js +1347 -0
  193. package/dist/domain/governance/activation/validators.js.map +1 -0
  194. package/dist/domain/governance/assessment/catalog.d.ts +14 -0
  195. package/dist/domain/governance/assessment/catalog.js +105 -0
  196. package/dist/domain/governance/assessment/catalog.js.map +1 -0
  197. package/dist/domain/governance/assessment/errors.d.ts +10 -0
  198. package/dist/domain/governance/assessment/errors.js +22 -0
  199. package/dist/domain/governance/assessment/errors.js.map +1 -0
  200. package/dist/domain/governance/assessment/index.d.ts +8 -0
  201. package/dist/domain/governance/assessment/index.js +9 -0
  202. package/dist/domain/governance/assessment/index.js.map +1 -0
  203. package/dist/domain/governance/assessment/live-normalize.d.ts +19 -0
  204. package/dist/domain/governance/assessment/live-normalize.js +403 -0
  205. package/dist/domain/governance/assessment/live-normalize.js.map +1 -0
  206. package/dist/domain/governance/assessment/predicates.d.ts +36 -0
  207. package/dist/domain/governance/assessment/predicates.js +545 -0
  208. package/dist/domain/governance/assessment/predicates.js.map +1 -0
  209. package/dist/domain/governance/assessment/report.d.ts +23 -0
  210. package/dist/domain/governance/assessment/report.js +266 -0
  211. package/dist/domain/governance/assessment/report.js.map +1 -0
  212. package/dist/domain/governance/assessment/sanitize.d.ts +8 -0
  213. package/dist/domain/governance/assessment/sanitize.js +60 -0
  214. package/dist/domain/governance/assessment/sanitize.js.map +1 -0
  215. package/dist/domain/governance/assessment/types.d.ts +174 -0
  216. package/dist/domain/governance/assessment/types.js +13 -0
  217. package/dist/domain/governance/assessment/types.js.map +1 -0
  218. package/dist/domain/governance/assessment/yaml.d.ts +6 -0
  219. package/dist/domain/governance/assessment/yaml.js +30 -0
  220. package/dist/domain/governance/assessment/yaml.js.map +1 -0
  221. package/dist/domain/governance/policy/identity.d.ts +64 -0
  222. package/dist/domain/governance/policy/identity.js +112 -0
  223. package/dist/domain/governance/policy/identity.js.map +1 -0
  224. package/dist/domain/migration/dependencies.d.ts +7 -0
  225. package/dist/domain/migration/dependencies.js +221 -0
  226. package/dist/domain/migration/dependencies.js.map +1 -0
  227. package/dist/domain/migration/inventory.d.ts +3 -0
  228. package/dist/domain/migration/inventory.js +15 -0
  229. package/dist/domain/migration/inventory.js.map +1 -0
  230. package/dist/domain/project/artifact-lifecycle.d.ts +69 -0
  231. package/dist/domain/project/artifact-lifecycle.js +137 -0
  232. package/dist/domain/project/artifact-lifecycle.js.map +1 -0
  233. package/dist/domain/project/catalog.d.ts +48 -0
  234. package/dist/domain/project/catalog.js +507 -0
  235. package/dist/domain/project/catalog.js.map +1 -0
  236. package/dist/domain/project/contracts.d.ts +247 -0
  237. package/dist/domain/project/contracts.js +2 -0
  238. package/dist/domain/project/contracts.js.map +1 -0
  239. package/dist/domain/project/errors.d.ts +3 -0
  240. package/dist/domain/project/errors.js +7 -0
  241. package/dist/domain/project/errors.js.map +1 -0
  242. package/dist/domain/project/infrastructure-layout.d.ts +26 -0
  243. package/dist/domain/project/infrastructure-layout.js +124 -0
  244. package/dist/domain/project/infrastructure-layout.js.map +1 -0
  245. package/dist/domain/project/inputs.d.ts +21 -0
  246. package/dist/domain/project/inputs.js +41 -0
  247. package/dist/domain/project/inputs.js.map +1 -0
  248. package/dist/domain/project/manifest/artifacts.d.ts +8 -0
  249. package/dist/domain/project/manifest/artifacts.js +150 -0
  250. package/dist/domain/project/manifest/artifacts.js.map +1 -0
  251. package/dist/domain/project/manifest/context.d.ts +13 -0
  252. package/dist/domain/project/manifest/context.js +2 -0
  253. package/dist/domain/project/manifest/context.js.map +1 -0
  254. package/dist/domain/project/manifest/fields.d.ts +6 -0
  255. package/dist/domain/project/manifest/fields.js +36 -0
  256. package/dist/domain/project/manifest/fields.js.map +1 -0
  257. package/dist/domain/project/manifest/governance.d.ts +8 -0
  258. package/dist/domain/project/manifest/governance.js +161 -0
  259. package/dist/domain/project/manifest/governance.js.map +1 -0
  260. package/dist/domain/project/manifest/project-identity.d.ts +6 -0
  261. package/dist/domain/project/manifest/project-identity.js +273 -0
  262. package/dist/domain/project/manifest/project-identity.js.map +1 -0
  263. package/dist/domain/project/manifest/reader.d.ts +9 -0
  264. package/dist/domain/project/manifest/reader.js +141 -0
  265. package/dist/domain/project/manifest/reader.js.map +1 -0
  266. package/dist/domain/project/paths.d.ts +2 -0
  267. package/dist/domain/project/paths.js +29 -0
  268. package/dist/domain/project/paths.js.map +1 -0
  269. package/dist/domain/project/planning.d.ts +19 -0
  270. package/dist/domain/project/planning.js +329 -0
  271. package/dist/domain/project/planning.js.map +1 -0
  272. package/dist/domain/project/retired-workload.d.ts +4 -0
  273. package/dist/domain/project/retired-workload.js +18 -0
  274. package/dist/domain/project/retired-workload.js.map +1 -0
  275. package/dist/domain/project/supported-stack.d.ts +113 -0
  276. package/dist/domain/project/supported-stack.js +426 -0
  277. package/dist/domain/project/supported-stack.js.map +1 -0
  278. package/dist/domain/workstation/constraints.d.ts +6 -0
  279. package/dist/domain/workstation/constraints.js +8 -0
  280. package/dist/domain/workstation/constraints.js.map +1 -0
  281. package/dist/file-system.d.ts +10 -44
  282. package/dist/file-system.js +9 -1245
  283. package/dist/file-system.js.map +1 -1
  284. package/dist/framework-adapters.d.ts +1 -1
  285. package/dist/framework-validation.d.ts +1 -1
  286. package/dist/framework-validation.js +1 -1
  287. package/dist/framework-validation.js.map +1 -1
  288. package/dist/genai-templates.d.ts +2 -9
  289. package/dist/genai-templates.js +4 -5
  290. package/dist/genai-templates.js.map +1 -1
  291. package/dist/generators/common/artifacts.d.ts +6 -0
  292. package/dist/generators/common/artifacts.js +30 -0
  293. package/dist/generators/common/artifacts.js.map +1 -0
  294. package/dist/generators/common/base.d.ts +17 -0
  295. package/dist/generators/common/base.js +421 -0
  296. package/dist/generators/common/base.js.map +1 -0
  297. package/dist/generators/common/dotenv-validation.d.ts +2 -0
  298. package/dist/generators/common/dotenv-validation.js +80 -0
  299. package/dist/generators/common/dotenv-validation.js.map +1 -0
  300. package/dist/generators/common/environments.d.ts +3 -0
  301. package/dist/generators/common/environments.js +15 -0
  302. package/dist/generators/common/environments.js.map +1 -0
  303. package/dist/generators/common/frontend.d.ts +15 -0
  304. package/dist/generators/common/frontend.js +166 -0
  305. package/dist/generators/common/frontend.js.map +1 -0
  306. package/dist/generators/common/python-settings.d.ts +2 -0
  307. package/dist/generators/common/python-settings.js +70 -0
  308. package/dist/generators/common/python-settings.js.map +1 -0
  309. package/dist/generators/common/spec-workflow.d.ts +28 -0
  310. package/dist/generators/common/spec-workflow.js +567 -0
  311. package/dist/generators/common/spec-workflow.js.map +1 -0
  312. package/dist/generators/common/values.d.ts +13 -0
  313. package/dist/generators/common/values.js +16 -0
  314. package/dist/generators/common/values.js.map +1 -0
  315. package/dist/generators/containers/compose.d.ts +7 -0
  316. package/dist/generators/containers/compose.js +196 -0
  317. package/dist/generators/containers/compose.js.map +1 -0
  318. package/dist/generators/containers/context.d.ts +1 -0
  319. package/dist/generators/containers/context.js +43 -0
  320. package/dist/generators/containers/context.js.map +1 -0
  321. package/dist/generators/containers/images.d.ts +7 -0
  322. package/dist/generators/containers/images.js +119 -0
  323. package/dist/generators/containers/images.js.map +1 -0
  324. package/dist/generators/context.d.ts +42 -0
  325. package/dist/generators/context.js +38 -0
  326. package/dist/generators/context.js.map +1 -0
  327. package/dist/generators/genai/backend.d.ts +20 -0
  328. package/dist/generators/genai/backend.js +667 -0
  329. package/dist/generators/genai/backend.js.map +1 -0
  330. package/dist/generators/genai/configuration.d.ts +3 -0
  331. package/dist/generators/genai/configuration.js +43 -0
  332. package/dist/generators/genai/configuration.js.map +1 -0
  333. package/dist/generators/genai/database.d.ts +7 -0
  334. package/dist/generators/genai/database.js +71 -0
  335. package/dist/generators/genai/database.js.map +1 -0
  336. package/dist/generators/genai/functions.d.ts +15 -0
  337. package/dist/generators/genai/functions.js +156 -0
  338. package/dist/generators/genai/functions.js.map +1 -0
  339. package/dist/generators/genai/index.d.ts +8 -0
  340. package/dist/generators/genai/index.js +11 -0
  341. package/dist/generators/genai/index.js.map +1 -0
  342. package/dist/generators/genai/patterns.d.ts +9 -0
  343. package/dist/generators/genai/patterns.js +309 -0
  344. package/dist/generators/genai/patterns.js.map +1 -0
  345. package/dist/generators/infrastructure/azure.d.ts +18 -0
  346. package/dist/generators/infrastructure/azure.js +655 -0
  347. package/dist/generators/infrastructure/azure.js.map +1 -0
  348. package/dist/generators/infrastructure/names.d.ts +23 -0
  349. package/dist/generators/infrastructure/names.js +51 -0
  350. package/dist/generators/infrastructure/names.js.map +1 -0
  351. package/dist/generators/standard/configuration.d.ts +3 -0
  352. package/dist/generators/standard/configuration.js +26 -0
  353. package/dist/generators/standard/configuration.js.map +1 -0
  354. package/dist/generators/standard/go.d.ts +17 -0
  355. package/dist/generators/standard/go.js +338 -0
  356. package/dist/generators/standard/go.js.map +1 -0
  357. package/dist/generators/standard/index.d.ts +12 -0
  358. package/dist/generators/standard/index.js +12 -0
  359. package/dist/generators/standard/index.js.map +1 -0
  360. package/dist/generators/standard/node.d.ts +20 -0
  361. package/dist/generators/standard/node.js +314 -0
  362. package/dist/generators/standard/node.js.map +1 -0
  363. package/dist/generators/standard/python.d.ts +18 -0
  364. package/dist/generators/standard/python.js +191 -0
  365. package/dist/generators/standard/python.js.map +1 -0
  366. package/dist/go-template-assets.js +3 -3
  367. package/dist/go-template-assets.js.map +1 -1
  368. package/dist/governance-activation/activation-state.d.ts +1 -1
  369. package/dist/governance-activation/activation-state.js +46 -9
  370. package/dist/governance-activation/activation-state.js.map +1 -1
  371. package/dist/governance-activation/approvals.d.ts +1 -188
  372. package/dist/governance-activation/approvals.js +1 -507
  373. package/dist/governance-activation/approvals.js.map +1 -1
  374. package/dist/governance-activation/canonical-json.d.ts +1 -3
  375. package/dist/governance-activation/canonical-json.js +1 -35
  376. package/dist/governance-activation/canonical-json.js.map +1 -1
  377. package/dist/governance-activation/commands.d.ts +1 -1
  378. package/dist/governance-activation/commands.js +72 -106
  379. package/dist/governance-activation/commands.js.map +1 -1
  380. package/dist/governance-activation/compatibility.d.ts +11 -3
  381. package/dist/governance-activation/compatibility.js +31 -4
  382. package/dist/governance-activation/compatibility.js.map +1 -1
  383. package/dist/governance-activation/credentials.d.ts +3 -3
  384. package/dist/governance-activation/credentials.js +5 -5
  385. package/dist/governance-activation/credentials.js.map +1 -1
  386. package/dist/governance-activation/doctor.d.ts +1 -1
  387. package/dist/governance-activation/doctor.js +28 -96
  388. package/dist/governance-activation/doctor.js.map +1 -1
  389. package/dist/governance-activation/evidence.d.ts +1 -49
  390. package/dist/governance-activation/evidence.js +1 -245
  391. package/dist/governance-activation/evidence.js.map +1 -1
  392. package/dist/governance-activation/git-ignore.d.ts +9 -0
  393. package/dist/governance-activation/git-ignore.js +76 -0
  394. package/dist/governance-activation/git-ignore.js.map +1 -0
  395. package/dist/governance-activation/graph.d.ts +1 -397
  396. package/dist/governance-activation/graph.js +1 -440
  397. package/dist/governance-activation/graph.js.map +1 -1
  398. package/dist/governance-activation/identity.d.ts +1 -35
  399. package/dist/governance-activation/identity.js +1 -88
  400. package/dist/governance-activation/identity.js.map +1 -1
  401. package/dist/governance-activation/index.d.ts +3 -0
  402. package/dist/governance-activation/index.js +3 -0
  403. package/dist/governance-activation/index.js.map +1 -1
  404. package/dist/governance-activation/inputs.d.ts +8 -0
  405. package/dist/governance-activation/inputs.js +146 -0
  406. package/dist/governance-activation/inputs.js.map +1 -0
  407. package/dist/governance-activation/migration.d.ts +4 -3
  408. package/dist/governance-activation/migration.js +14 -6
  409. package/dist/governance-activation/migration.js.map +1 -1
  410. package/dist/governance-activation/phase-bootstrap-state.d.ts +3 -0
  411. package/dist/governance-activation/phase-bootstrap-state.js +128 -0
  412. package/dist/governance-activation/phase-bootstrap-state.js.map +1 -0
  413. package/dist/governance-activation/phase-discovery.d.ts +2 -0
  414. package/dist/governance-activation/phase-discovery.js +88 -0
  415. package/dist/governance-activation/phase-discovery.js.map +1 -0
  416. package/dist/governance-activation/phase-governance.d.ts +4 -0
  417. package/dist/governance-activation/phase-governance.js +141 -0
  418. package/dist/governance-activation/phase-governance.js.map +1 -0
  419. package/dist/governance-activation/phase-publication.d.ts +8 -0
  420. package/dist/governance-activation/phase-publication.js +318 -0
  421. package/dist/governance-activation/phase-publication.js.map +1 -0
  422. package/dist/governance-activation/read-only.d.ts +30 -0
  423. package/dist/governance-activation/read-only.js +93 -0
  424. package/dist/governance-activation/read-only.js.map +1 -0
  425. package/dist/governance-activation/readiness.d.ts +1 -25
  426. package/dist/governance-activation/readiness.js +1 -222
  427. package/dist/governance-activation/readiness.js.map +1 -1
  428. package/dist/governance-activation/reconciliation.d.ts +1 -1
  429. package/dist/governance-activation/reconciliation.js +3 -3
  430. package/dist/governance-activation/reconciliation.js.map +1 -1
  431. package/dist/governance-activation/release-integrity.d.ts +1 -1
  432. package/dist/governance-activation/release-integrity.js +2 -2
  433. package/dist/governance-activation/release-integrity.js.map +1 -1
  434. package/dist/governance-activation/seed-lifecycle.d.ts +9 -1
  435. package/dist/governance-activation/seed-lifecycle.js +197 -63
  436. package/dist/governance-activation/seed-lifecycle.js.map +1 -1
  437. package/dist/governance-activation/source-of-truth.d.ts +5 -3
  438. package/dist/governance-activation/source-of-truth.js +32 -52
  439. package/dist/governance-activation/source-of-truth.js.map +1 -1
  440. package/dist/governance-activation/spec-kit-seed.d.ts +9 -0
  441. package/dist/governance-activation/spec-kit-seed.js +43 -0
  442. package/dist/governance-activation/spec-kit-seed.js.map +1 -0
  443. package/dist/governance-activation/task-projection.d.ts +1 -1
  444. package/dist/governance-activation/transition-planning.d.ts +15 -0
  445. package/dist/governance-activation/transition-planning.js +250 -0
  446. package/dist/governance-activation/transition-planning.js.map +1 -0
  447. package/dist/governance-activation/transition-ports.d.ts +150 -0
  448. package/dist/governance-activation/transition-ports.js +2 -0
  449. package/dist/governance-activation/transition-ports.js.map +1 -0
  450. package/dist/governance-activation/transition-process.d.ts +7 -0
  451. package/dist/governance-activation/transition-process.js +21 -0
  452. package/dist/governance-activation/transition-process.js.map +1 -0
  453. package/dist/governance-activation/transition-records.d.ts +56 -0
  454. package/dist/governance-activation/transition-records.js +208 -0
  455. package/dist/governance-activation/transition-records.js.map +1 -0
  456. package/dist/governance-activation/transitions.d.ts +6 -165
  457. package/dist/governance-activation/transitions.js +142 -2055
  458. package/dist/governance-activation/transitions.js.map +1 -1
  459. package/dist/governance-activation/types.d.ts +1 -390
  460. package/dist/governance-activation/types.js +1 -107
  461. package/dist/governance-activation/types.js.map +1 -1
  462. package/dist/governance-activation/validators.d.ts +1 -17
  463. package/dist/governance-activation/validators.js +1 -1304
  464. package/dist/governance-activation/validators.js.map +1 -1
  465. package/dist/governance-assessment/catalog.d.ts +1 -2
  466. package/dist/governance-assessment/catalog.js +18 -85
  467. package/dist/governance-assessment/catalog.js.map +1 -1
  468. package/dist/governance-assessment/command.d.ts +1 -1
  469. package/dist/governance-assessment/command.js +8 -12
  470. package/dist/governance-assessment/command.js.map +1 -1
  471. package/dist/governance-assessment/engine.d.ts +6 -11
  472. package/dist/governance-assessment/engine.js +463 -114
  473. package/dist/governance-assessment/engine.js.map +1 -1
  474. package/dist/governance-assessment/live-normalize.d.ts +1 -19
  475. package/dist/governance-assessment/live-normalize.js +1 -402
  476. package/dist/governance-assessment/live-normalize.js.map +1 -1
  477. package/dist/governance-assessment/live-transport.d.ts +2 -6
  478. package/dist/governance-assessment/live-transport.js +10 -22
  479. package/dist/governance-assessment/live-transport.js.map +1 -1
  480. package/dist/governance-assessment/live.js +55 -5
  481. package/dist/governance-assessment/live.js.map +1 -1
  482. package/dist/governance-assessment/predicates.d.ts +1 -27
  483. package/dist/governance-assessment/predicates.js +1 -329
  484. package/dist/governance-assessment/predicates.js.map +1 -1
  485. package/dist/governance-assessment/project.d.ts +13 -17
  486. package/dist/governance-assessment/project.js +59 -149
  487. package/dist/governance-assessment/project.js.map +1 -1
  488. package/dist/governance-assessment/readers.d.ts +1 -5
  489. package/dist/governance-assessment/readers.js +5 -13
  490. package/dist/governance-assessment/readers.js.map +1 -1
  491. package/dist/governance-assessment/report.d.ts +1 -23
  492. package/dist/governance-assessment/report.js +1 -265
  493. package/dist/governance-assessment/report.js.map +1 -1
  494. package/dist/governance-assessment/sanitize.d.ts +1 -8
  495. package/dist/governance-assessment/sanitize.js +1 -56
  496. package/dist/governance-assessment/sanitize.js.map +1 -1
  497. package/dist/governance-assessment/types.d.ts +1 -173
  498. package/dist/governance-assessment/types.js +1 -12
  499. package/dist/governance-assessment/types.js.map +1 -1
  500. package/dist/governance-assessment/yaml.d.ts +1 -6
  501. package/dist/governance-assessment/yaml.js +1 -29
  502. package/dist/governance-assessment/yaml.js.map +1 -1
  503. package/dist/init-filesystem.d.ts +1 -1
  504. package/dist/init-filesystem.js +126 -174
  505. package/dist/init-filesystem.js.map +1 -1
  506. package/dist/interactive.d.ts +2 -2
  507. package/dist/interactive.js +42 -41
  508. package/dist/interactive.js.map +1 -1
  509. package/dist/migrate-plan.d.ts +1 -1
  510. package/dist/migrate-plan.js +14 -8
  511. package/dist/migrate-plan.js.map +1 -1
  512. package/dist/npm-template-assets.js +5 -4
  513. package/dist/npm-template-assets.js.map +1 -1
  514. package/dist/openspec-profile.d.ts +1 -1
  515. package/dist/opentofu-template-assets.js +3 -3
  516. package/dist/opentofu-template-assets.js.map +1 -1
  517. package/dist/package-identity.d.ts +3 -0
  518. package/dist/package-identity.js +13 -2
  519. package/dist/package-identity.js.map +1 -1
  520. package/dist/planner.d.ts +1 -19
  521. package/dist/planner.js +1 -565
  522. package/dist/planner.js.map +1 -1
  523. package/dist/process-runner.d.ts +1 -1
  524. package/dist/project-dependencies.d.ts +5 -3
  525. package/dist/project-dependencies.js +74 -63
  526. package/dist/project-dependencies.js.map +1 -1
  527. package/dist/published-verifier.js +61 -30
  528. package/dist/published-verifier.js.map +1 -1
  529. package/dist/python-template-assets.js +6 -9
  530. package/dist/python-template-assets.js.map +1 -1
  531. package/dist/reconcile.d.ts +1 -1
  532. package/dist/reconcile.js +2 -2
  533. package/dist/reconcile.js.map +1 -1
  534. package/dist/repository-governance.d.ts +5 -2
  535. package/dist/repository-governance.js +126 -112
  536. package/dist/repository-governance.js.map +1 -1
  537. package/dist/runtime.js +1 -1
  538. package/dist/runtime.js.map +1 -1
  539. package/dist/scan.d.ts +6 -2
  540. package/dist/scan.js +78 -30
  541. package/dist/scan.js.map +1 -1
  542. package/dist/self-upgrade.d.ts +1 -1
  543. package/dist/self-upgrade.js +20 -7
  544. package/dist/self-upgrade.js.map +1 -1
  545. package/dist/stable-release.js +7 -1
  546. package/dist/stable-release.js.map +1 -1
  547. package/dist/standard-templates.d.ts +2 -2
  548. package/dist/standard-templates.js +6 -841
  549. package/dist/standard-templates.js.map +1 -1
  550. package/dist/supported-stack.d.ts +3 -115
  551. package/dist/supported-stack.js +4 -438
  552. package/dist/supported-stack.js.map +1 -1
  553. package/dist/telemetry/index.d.ts +1 -1
  554. package/dist/templates.d.ts +10 -20
  555. package/dist/templates.js +52 -3261
  556. package/dist/templates.js.map +1 -1
  557. package/dist/types.d.ts +1 -263
  558. package/dist/types.js +1 -1
  559. package/dist/types.js.map +1 -1
  560. package/dist/version.js +2 -6
  561. package/dist/version.js.map +1 -1
  562. package/dist/workstation-catalog.d.ts +5 -2
  563. package/dist/workstation-catalog.js +33 -2
  564. package/dist/workstation-catalog.js.map +1 -1
  565. package/dist/workstation.d.ts +4 -3
  566. package/dist/workstation.js +86 -32
  567. package/dist/workstation.js.map +1 -1
  568. package/docs/assets/liftoff-terminal.svg +3 -3
  569. package/docs/azure-deployment.md +107 -10
  570. package/docs/cli-reference.md +49 -13
  571. package/docs/configuration-and-manifests.md +139 -34
  572. package/docs/existing-repositories.md +33 -6
  573. package/docs/getting-started.md +44 -31
  574. package/docs/prerequisites.md +34 -35
  575. package/docs/project-structure.md +39 -46
  576. package/docs/repository-governance.md +90 -24
  577. package/docs/safety-and-consent.md +65 -12
  578. package/docs/spec-workflows-and-agents.md +31 -28
  579. package/docs/supported-stack.md +11 -9
  580. package/docs/troubleshooting.md +107 -49
  581. package/docs/workloads.md +46 -62
  582. package/package.json +4 -7
  583. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/UPSTREAM_LICENSE.txt +0 -21
  584. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/catalog.json +0 -520
  585. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/packaged/gitignore +0 -27
  586. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/README.md +0 -25
  587. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/components.json +0 -22
  588. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/eslint.config.js +0 -23
  589. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/index.html +0 -13
  590. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package-lock.json +0 -6156
  591. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package.json +0 -60
  592. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/public/power-apps.svg +0 -55
  593. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/App.tsx +0 -17
  594. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/assets/react.svg +0 -1
  595. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/mode-toggle.tsx +0 -37
  596. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/badge.tsx +0 -46
  597. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/button.tsx +0 -60
  598. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/calendar.tsx +0 -211
  599. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/card.tsx +0 -92
  600. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/chart.tsx +0 -355
  601. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/checkbox.tsx +0 -32
  602. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/command.tsx +0 -184
  603. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/dialog.tsx +0 -141
  604. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/dropdown-menu.tsx +0 -257
  605. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/input.tsx +0 -21
  606. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/label.tsx +0 -22
  607. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/popover.tsx +0 -46
  608. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/progress.tsx +0 -29
  609. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/select.tsx +0 -185
  610. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/separator.tsx +0 -26
  611. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/skeleton.tsx +0 -13
  612. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/table.tsx +0 -114
  613. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/tabs.tsx +0 -66
  614. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/textarea.tsx +0 -18
  615. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/tooltip.tsx +0 -61
  616. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/hooks/use-theme.ts +0 -11
  617. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/index.css +0 -131
  618. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/lib/utils.ts +0 -6
  619. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/main.tsx +0 -10
  620. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/_layout.tsx +0 -31
  621. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/home.tsx +0 -59
  622. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/not-found.tsx +0 -14
  623. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/query-provider.tsx +0 -26
  624. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/sonner-provider.tsx +0 -23
  625. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/theme-provider.tsx +0 -64
  626. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/router.tsx +0 -24
  627. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.app.json +0 -34
  628. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.json +0 -13
  629. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.node.json +0 -26
  630. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/vite.config.ts +0 -19
  631. package/assets/power-apps-code-app/README.md +0 -27
  632. package/dist/code-apps-plugin.d.ts +0 -13
  633. package/dist/code-apps-plugin.js +0 -69
  634. package/dist/code-apps-plugin.js.map +0 -1
  635. package/dist/power-apps-assets.d.ts +0 -30
  636. package/dist/power-apps-assets.js +0 -131
  637. package/dist/power-apps-assets.js.map +0 -1
  638. package/dist/power-apps-templates.d.ts +0 -3
  639. package/dist/power-apps-templates.js +0 -177
  640. package/dist/power-apps-templates.js.map +0 -1
  641. package/dist/power-apps-validation.d.ts +0 -1
  642. package/dist/power-apps-validation.js +0 -79
  643. package/dist/power-apps-validation.js.map +0 -1
@@ -1,1941 +1,72 @@
1
- import { readdir, stat } from 'node:fs/promises';
2
- import path from 'node:path';
1
+ import { executeSeedOperations } from './seed-lifecycle.js';
2
+ import { executeGitOperations } from './phase-publication.js';
3
+ import { discoverPhase0 } from './phase-discovery.js';
4
+ import { executeActivationApproval, executeCredentialReady, executeRulesetPhase } from './phase-governance.js';
5
+ import { remoteImportRetention, executeBootstrapStateDisposal } from './phase-bootstrap-state.js';
6
+ import { phaseOrder, rollbackPlanFromCompletedOperations, phaseById, assertPlanOperationsAllowed, assertOperationAllowed, rollbackPlanForPhase } from '../domain/governance/activation/operations.js';
3
7
  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';
8
+ import { validateManifestActivationForExecution } from '../domain/governance/activation/validators.js';
9
+ import { withProjectMutationLock } from '../adapters/filesystem/project-lock.js';
10
+ import { initializeExecutionAnchor, saveTransitionPlan, blockedState, writeOutcomeTransaction, safeTimestamp, evidenceHeaderFor, evidencePathParts, nextStateForOutcome, evidenceWriteOperation, stateWriteOperation } from './transition-records.js';
11
+ import { canonicalJson, canonicalSha256, isRecord } from '../domain/governance/activation/canonical-json.js';
12
+ import { buildSavedTransitionPlan, previewApplyNext, comparePlanFreshness } from './transition-planning.js';
13
+ import { readActivationInputSnapshot, phaseInputDigest, remoteBindingDigest } from './inputs.js';
14
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
- ]
15
+ import { evidenceHeaderDigest, validateEvidenceFreshness } from '../domain/governance/activation/evidence.js';
16
+ export { governancePlanDirectoryPathParts, transitionPlanPathParts } from './transition-records.js';
17
+ export { buildSavedTransitionPlan, previewApplyNext } from './transition-planning.js';
18
+ export { rollbackPlanFromCompletedOperations, planDigestFor } from '../domain/governance/activation/operations.js';
19
+ const builtInExecutors = {
20
+ 'seed-valid': executeSeedOperations,
21
+ 'seed-verified': executeSeedOperations,
22
+ 'seed-archived': executeSeedOperations,
23
+ committed: executeGitOperations,
24
+ pushed: executeGitOperations,
25
+ 'phase-0-complete': discoverPhase0,
26
+ 'activation-approved': executeActivationApproval,
27
+ 'credential-ready': executeCredentialReady,
28
+ 'remote-ready': remoteImportRetention,
29
+ 'bootstrap-state-disposed': executeBootstrapStateDisposal,
30
+ 'rulesets-applied': executeRulesetPhase,
31
+ 'live-readback': executeRulesetPhase
217
32
  };
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
- export 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
- selectedPhase: nextReadyPhase,
1145
- nextReadyPhase,
1146
- approval: plan?.approval ?? null,
1147
- proposedMutations: {
1148
- local: plan?.mutationClasses.local ?? ['none'],
1149
- remote: plan?.mutationClasses.remote ?? ['none'],
1150
- operations: plan?.operations ?? []
1151
- },
1152
- savedPlan: null,
1153
- noWrites: true,
1154
- blockers: blockers.length > 0 ? blockers : ['No phase is ready for execution.']
1155
- };
1156
- }
1157
- if (plan.approval.evaluation.approvalRequired) {
1158
- return {
1159
- schemaVersion: 1,
1160
- command: 'governance apply-next',
1161
- projectRoot: input.inspection.projectRoot,
1162
- execute: input.execute,
1163
- applied: false,
1164
- authorized: false,
1165
- reason: 'approval-required',
1166
- message: plan.approval.evaluation.reasons.join('; '),
1167
- selectedPhase: nextReadyPhase,
1168
- nextReadyPhase,
1169
- approval: plan.approval,
1170
- proposedMutations: {
1171
- local: plan.mutationClasses.local,
1172
- remote: plan.mutationClasses.remote,
1173
- operations: plan.operations
1174
- },
1175
- savedPlan: null,
1176
- noWrites: true,
1177
- blockers: plan.approval.evaluation.reasons
1178
- };
1179
- }
1180
- return {
1181
- schemaVersion: 1,
1182
- command: 'governance apply-next',
1183
- projectRoot: input.inspection.projectRoot,
1184
- execute: input.execute,
1185
- applied: false,
1186
- authorized: true,
1187
- reason: input.execute ? 'execute-requested' : 'execute-required',
1188
- message: input.execute
1189
- ? 'Execution requested; the plan must be saved and revalidated before any mutation.'
1190
- : 'Preview only. Rerun with --execute to save this plan and execute at most one phase.',
1191
- selectedPhase: nextReadyPhase,
1192
- nextReadyPhase,
1193
- approval: plan.approval,
1194
- proposedMutations: {
1195
- local: plan.mutationClasses.local,
1196
- remote: plan.mutationClasses.remote,
1197
- operations: plan.operations
1198
- },
1199
- savedPlan: null,
1200
- noWrites: true,
1201
- blockers: []
1202
- };
1203
- }
1204
- async function saveTransitionPlan(projectRoot, plan) {
1205
- const validated = validateSavedTransitionPlan(plan);
1206
- assertNoSecrets(validated);
1207
- const pathParts = transitionPlanPathParts(validated);
1208
- const existing = await readProjectFile(projectRoot, pathParts);
1209
- if (existing !== undefined) {
1210
- throw new Error(`Refusing to overwrite existing governance transition plan ${pathParts.join('/')}.`);
1211
- }
1212
- const content = `${canonicalJson(validated)}\n`;
1213
- await applyProjectFileTransaction(projectRoot, [{
1214
- type: 'write',
1215
- pathParts,
1216
- content
1217
- }]);
1218
- return { pathParts, digest: canonicalSha256(validated) };
1219
- }
1220
- function comparePlanFreshness(saved, fresh) {
1221
- if (!fresh) {
1222
- return ['No phase remained ready after saving the transition plan.'];
1223
- }
1224
- const issues = [];
1225
- if (saved.phaseId !== fresh.phaseId) {
1226
- issues.push(`Ready phase changed from ${saved.phaseId} to ${fresh.phaseId}.`);
1227
- }
1228
- for (const field of ['stateHash', 'baselineDigest', 'inputDigest', 'transitionDigest']) {
1229
- if (saved[field] !== fresh[field]) {
1230
- issues.push(`${field} changed after plan save.`);
1231
- }
1232
- }
1233
- if (canonicalSha256(saved.operations) !== canonicalSha256(fresh.operations)) {
1234
- issues.push('Proposed operations changed after plan save.');
1235
- }
1236
- if (saved.approval.evaluation.approvalRequired || fresh.approval.evaluation.approvalRequired) {
1237
- issues.push('Approval was not valid immediately before execution.');
1238
- }
1239
- return issues;
1240
- }
1241
- function evidenceHeaderFor(input) {
1242
- const context = input.inspection.contexts[input.phase.id];
1243
- return validateEvidenceHeader({
1244
- schemaVersion: input.inspection.state.identity.evidenceHeaderSchemaVersion,
1245
- repositoryId: input.inspection.state.repository.id,
1246
- identity: input.inspection.state.identity,
1247
- phaseGraphHash: input.inspection.state.identity.phaseGraphHash,
1248
- phaseId: input.phase.id,
1249
- phaseContractDigest: phaseContractDigests(input.inspection.graph)[input.phase.id],
1250
- inputDigest: input.plan.inputDigest,
1251
- baselineSha: input.plan.baselineDigest,
1252
- transition: context.transition,
1253
- producedAt: input.now.toISOString(),
1254
- producer: engineProducer,
1255
- result: input.result
1256
- });
1257
- }
1258
- function appendUnique(values, value) {
1259
- if (value === null || values.includes(value)) {
1260
- return [...values];
1261
- }
1262
- return [...values, value];
1263
- }
1264
- function nextStateForOutcome(input) {
1265
- const base = cloneState(input.override ?? input.inspection.state);
1266
- if (input.inspection.sourceOfTruth.status === 'selected' &&
1267
- input.inspection.sourceOfTruth.recordActiveChangeOnNextMutation) {
1268
- const selected = stateWithSelectedActiveChange(base, input.inspection.sourceOfTruth.selected);
1269
- base.activeChange = selected.activeChange;
1270
- }
1271
- const phaseState = input.resultState === 'approved' ? 'approved' : input.resultState;
1272
- base.phases[input.phase.id] = {
1273
- state: phaseState,
1274
- updatedAt: input.now.toISOString(),
1275
- evidence: input.evidenceReference
1276
- ? [...base.phases[input.phase.id].evidence, input.evidenceReference]
1277
- : base.phases[input.phase.id].evidence,
1278
- approvals: appendUnique(base.phases[input.phase.id].approvals, input.plan.approval.envelopeId),
1279
- blockers: input.blocker ? [input.blocker] : []
1280
- };
1281
- base.updatedAt = input.now.toISOString();
1282
- return validateUserActivationState(base);
1283
- }
1284
- function blockedState(input) {
1285
- const base = cloneState(input.inspection.state);
1286
- base.phases[input.phase.id] = {
1287
- state: 'blocked',
1288
- updatedAt: input.now.toISOString(),
1289
- evidence: base.phases[input.phase.id].evidence,
1290
- approvals: appendUnique(base.phases[input.phase.id].approvals, input.plan.approval.envelopeId),
1291
- blockers: [input.blocker]
1292
- };
1293
- base.updatedAt = input.now.toISOString();
1294
- return validateUserActivationState(base);
1295
- }
1296
- async function assertLoadedStateHash(projectRoot, expectedHash) {
1297
- const bytes = await readProjectFile(projectRoot, activationStatePathParts());
1298
- const currentHash = bytes === undefined ? null : activationStateContentHash(bytes);
1299
- if (currentHash !== expectedHash) {
1300
- throw new Error(`Activation state changed after inspection: expected ${expectedHash ?? 'absent'}, found ${currentHash ?? 'absent'}.`);
1301
- }
1302
- }
1303
- async function writeOutcomeTransaction(input) {
1304
- await assertLoadedStateHash(input.projectRoot, input.plan.stateHash);
1305
- const stateContent = `${canonicalJson(validateUserActivationState(input.nextState))}\n`;
1306
- const mutations = [
1307
- ...(input.fileMutations ?? []).map((mutation) => ({
1308
- ...mutation,
1309
- pathParts: validateArtifactPathParts([...mutation.pathParts], 'Governance transition file mutation path')
1310
- })),
1311
- ...(input.evidenceRecord && input.evidencePathParts
1312
- ? [{
1313
- type: 'write',
1314
- pathParts: [...input.evidencePathParts],
1315
- content: `${canonicalJson(input.evidenceRecord)}\n`
1316
- }]
1317
- : []),
1318
- {
1319
- type: 'write',
1320
- pathParts: activationStatePathParts(),
1321
- content: stateContent
1322
- }
1323
- ];
1324
- if (input.evidencePathParts) {
1325
- const existing = await readProjectFile(input.projectRoot, [...input.evidencePathParts]);
1326
- if (existing !== undefined) {
1327
- throw new Error(`Refusing to overwrite existing governance evidence ${input.evidencePathParts.join('/')}.`);
1328
- }
1329
- }
1330
- await applyProjectFileTransaction(input.projectRoot, mutations);
1331
- return {
1332
- stateHash: activationStateContentHash(stateContent),
1333
- evidence: input.evidenceRecord && input.evidencePathParts
1334
- ? {
1335
- evidenceId: input.evidenceRecord.evidenceId,
1336
- pathParts: input.evidencePathParts,
1337
- headerDigest: evidenceHeaderDigest(input.evidenceRecord.header),
1338
- result: input.evidenceRecord.header.result
1339
- }
1340
- : null
1341
- };
1342
- }
1343
- async function executeGitOperations(input, operations) {
1344
- if (input.phase.id !== 'committed' && input.phase.id !== 'pushed') {
1345
- return null;
1346
- }
1347
- const completed = [];
1348
- for (const op of operations.filter((entry) => entry.adapter === 'git')) {
1349
- if (op.actionId === 'git.verify-existing-commit' || op.actionId === 'git.verify-existing-push') {
1350
- completed.push(op);
1351
- continue;
1352
- }
1353
- if (op.actionId === 'git.init') {
1354
- const result = await runGit(input.runner, input.inspection.projectRoot, ['init', '-b', String(op.inputs.branch)]);
1355
- if (!commandSucceeded(result)) {
1356
- return { status: 'blocked', blocker: commandFailure(result), completedOperations: completed };
1357
- }
1358
- completed.push(op);
1359
- }
1360
- else if (op.actionId === 'git.add-reviewed') {
1361
- const paths = Array.isArray(op.inputs.paths) ? op.inputs.paths.map(String) : [];
1362
- if (paths.length === 0) {
1363
- completed.push(op);
1364
- continue;
1365
- }
1366
- const result = await runGit(input.runner, input.inspection.projectRoot, ['add', '--', ...paths]);
1367
- if (!commandSucceeded(result)) {
1368
- return { status: 'blocked', blocker: commandFailure(result), completedOperations: completed };
1369
- }
1370
- completed.push(op);
1371
- }
1372
- else if (op.actionId === 'git.commit-reviewed') {
1373
- const message = String(op.inputs.message);
1374
- const result = await runGit(input.runner, input.inspection.projectRoot, ['commit', '-m', message]);
1375
- if (!commandSucceeded(result)) {
1376
- return { status: 'blocked', blocker: commandFailure(result), completedOperations: completed };
1377
- }
1378
- completed.push(op);
1379
- }
1380
- else if (op.actionId === 'git.push-approved-ref') {
1381
- const branch = String(op.inputs.branch);
1382
- const result = await runGit(input.runner, input.inspection.projectRoot, ['push', 'origin', `HEAD:refs/heads/${branch}`]);
1383
- if (!commandSucceeded(result)) {
1384
- return { status: 'blocked', blocker: commandFailure(result), completedOperations: completed };
1385
- }
1386
- completed.push(op);
1387
- }
1388
- }
1389
- return {
1390
- status: 'completed',
1391
- resultState: 'verified',
1392
- completedOperations: completed
1393
- };
1394
- }
1395
- async function executeSeedOperations(input) {
1396
- if (!input.phase.id.startsWith('seed-')) {
1397
- return null;
1398
- }
1399
- if (input.phase.id === 'seed-valid') {
1400
- const result = await validateGeneratedSeedForPhase(input.inspection.projectRoot, input.runner);
1401
- if (result.status === 'blocked') {
1402
- return { status: 'blocked', blocker: result.issues[0] ?? 'Seed validation failed.', completedOperations: [] };
1403
- }
1404
- return { status: 'completed', resultState: 'verified', completedOperations: input.plan.operations.filter((op) => op.actionId === 'openspec.seed.validate') };
1405
- }
1406
- if (input.phase.id === 'seed-verified') {
1407
- const result = await verifyGeneratedSeedBaselineForPhase(input.inspection.projectRoot, input.runner);
1408
- if (result.status === 'blocked') {
1409
- return { status: 'blocked', blocker: result.issues[0] ?? 'Seed baseline verification failed.', completedOperations: [] };
1410
- }
1411
- return {
1412
- status: 'completed',
1413
- resultState: 'verified',
1414
- evidencePayload: {
1415
- kind: 'seed-verified.v1',
1416
- checks: result.checks.map((check) => ({
1417
- id: check.id,
1418
- taskId: check.taskId,
1419
- status: check.status
1420
- }))
1421
- },
1422
- completedOperations: input.plan.operations.filter((op) => op.actionId === 'openspec.seed.baseline-verify')
1423
- };
1424
- }
1425
- const result = await archiveGeneratedSeedForPhase(input.inspection.projectRoot, input.runner);
1426
- if (result.status === 'blocked') {
1427
- return {
1428
- status: 'blocked',
1429
- blocker: result.issues[0] ?? 'Seed archive failed.',
1430
- completedOperations: result.archiveCompleted
1431
- ? input.plan.operations.filter((op) => op.actionId === 'openspec.seed.archive')
1432
- : [],
1433
- retryableWithoutStateMutation: result.retryableAfterRepair
1434
- };
1435
- }
1436
- return {
1437
- status: 'completed',
1438
- resultState: 'verified',
1439
- evidencePayload: {
1440
- kind: 'seed-archived.v1',
1441
- archiveSyncBehavior: result.archiveSyncBehavior ?? null
1442
- },
1443
- completedOperations: input.plan.operations.filter((op) => op.actionId === 'openspec.seed.archive')
1444
- };
1445
- }
1446
- async function discoverPhase0(input) {
1447
- if (input.phase.id !== 'phase-0-complete') {
1448
- return null;
1449
- }
1450
- const gh = await runCommand(input.runner, {
1451
- executable: 'gh',
1452
- args: ['repo', 'view', '--json', 'id,nameWithOwner,defaultBranchRef,isPrivate']
1453
- }, input.inspection.projectRoot);
1454
- if (!commandSucceeded(gh)) {
1455
- return {
1456
- status: 'blocked',
1457
- blocker: `Phase 0 GitHub read-only discovery failed: ${commandFailure(gh)}`,
1458
- completedOperations: []
1459
- };
1460
- }
1461
- let repo;
1462
- try {
1463
- repo = JSON.parse(gh.stdout);
1464
- }
1465
- catch (error) {
1466
- return {
1467
- status: 'blocked',
1468
- blocker: `Phase 0 GitHub discovery returned invalid JSON: ${errorMessage(error)}`,
1469
- completedOperations: []
1470
- };
1471
- }
1472
- if (!repo.id || !repo.nameWithOwner || !repo.defaultBranchRef?.name) {
1473
- return {
1474
- status: 'blocked',
1475
- blocker: 'Phase 0 GitHub discovery did not return repository id, nameWithOwner, and default branch.',
1476
- completedOperations: []
1477
- };
1478
- }
1479
- const az = await runCommand(input.runner, {
1480
- executable: 'az',
1481
- args: ['account', 'show', '--output', 'json']
1482
- }, input.inspection.projectRoot);
1483
- const azureAccount = commandSucceeded(az) ? safeJson(az.stdout) : null;
1484
- const git = await inspectGitRepository(input.inspection.projectRoot, input.runner);
1485
- const baselineDigest = canonicalSha256({
1486
- gitHead: git.head,
1487
- repositoryId: repo.id,
1488
- repositoryName: repo.nameWithOwner,
1489
- defaultBranch: repo.defaultBranchRef.name
1490
- });
1491
- const facts = {
1492
- repositoryId: repo.id,
1493
- repositoryName: repo.nameWithOwner,
1494
- defaultBranch: repo.defaultBranchRef.name,
1495
- baselineDigest,
1496
- privateStagingDast: false,
1497
- credentialRequired: false,
1498
- statePath: 'none',
1499
- approvedFacts: [
1500
- { id: 'repository.id', value: repo.id },
1501
- { id: 'repository.nameWithOwner', value: repo.nameWithOwner },
1502
- { id: 'repository.defaultBranch', value: repo.defaultBranchRef.name },
1503
- { id: 'repository.isPrivate', value: repo.isPrivate ?? null },
1504
- { id: 'azure.accountReadable', value: azureAccount !== null }
1505
- ]
1506
- };
1507
- const nextState = cloneState(input.inspection.state);
1508
- nextState.repository = {
1509
- id: facts.repositoryId,
1510
- name: facts.repositoryName,
1511
- defaultBranch: facts.defaultBranch
1512
- };
1513
- nextState.applicability = {
1514
- statePath: facts.statePath,
1515
- privateStagingDast: facts.privateStagingDast,
1516
- credentialRequired: facts.credentialRequired
1517
- };
1518
- return {
1519
- status: 'completed',
1520
- resultState: 'verified',
1521
- evidencePayload: {
1522
- kind: 'phase-0-discovery.v1',
1523
- facts: facts.approvedFacts,
1524
- activationCreateChangePlan: {
1525
- deterministic: true,
1526
- graphHash: input.inspection.graphHash
1527
- }
1528
- },
1529
- stateOverride: nextState,
1530
- completedOperations: input.plan.operations.filter((op) => op.actionId === 'github.phase0.discover' || op.actionId === 'azure.phase0.discover')
1531
- };
1532
- }
1533
- function safeJson(value) {
1534
- try {
1535
- return JSON.parse(value);
1536
- }
1537
- catch {
1538
- return null;
1539
- }
1540
- }
1541
- async function executeActivationApproval(input) {
1542
- if (input.phase.id !== 'activation-approved') {
1543
- return null;
1544
- }
1545
- const state = cloneState(input.inspection.state);
1546
- const fileMutations = [];
1547
- if (input.inspection.sourceOfTruth.status === 'none') {
1548
- const facts = buildApprovedPhase0FactsFromState(input.inspection.manifest, input.inspection.state, input.inspection.evidence);
1549
- if (!facts || input.inspection.sourceOfTruth.createPlan.status !== 'ready') {
1550
- return {
1551
- status: 'blocked',
1552
- blocker: input.inspection.sourceOfTruth.createPlan.reason,
1553
- completedOperations: []
1554
- };
1555
- }
1556
- const writePlan = renderGovernanceChangeWritePlan(facts);
1557
- for (const file of writePlan.files) {
1558
- const existing = await readProjectFile(input.inspection.projectRoot, [...file.pathParts]);
1559
- if (existing !== undefined) {
1560
- return {
1561
- status: 'blocked',
1562
- blocker: `Refusing to overwrite existing governance artifact ${file.pathParts.join('/')}.`,
1563
- completedOperations: []
1564
- };
1565
- }
1566
- fileMutations.push({
1567
- type: 'write',
1568
- pathParts: [...file.pathParts],
1569
- content: file.content
1570
- });
1571
- }
1572
- state.activeChange = {
1573
- id: writePlan.changeId,
1574
- kind: writePlan.workflowKind === 'openspec' ? 'openspec' : 'spec-kit'
1575
- };
1576
- }
1577
- else if (input.inspection.sourceOfTruth.status === 'selected') {
1578
- state.activeChange = {
1579
- id: input.inspection.sourceOfTruth.selected.changeId,
1580
- kind: input.inspection.sourceOfTruth.selected.workflowKind === 'openspec' ? 'openspec' : 'spec-kit'
1581
- };
1582
- }
1583
- else {
1584
- return {
1585
- status: 'blocked',
1586
- blocker: 'A compatible governance source of truth is required before activation approval can be recorded.',
1587
- completedOperations: []
1588
- };
1589
- }
1590
- return {
1591
- status: 'completed',
1592
- resultState: 'approved',
1593
- stateOverride: state,
1594
- fileMutations,
1595
- completedOperations: input.plan.operations.filter((op) => op.actionId.endsWith('governance.create-change'))
1596
- };
1597
- }
1598
- async function executeCredentialReady(input) {
1599
- if (input.phase.id !== 'credential-ready') {
1600
- return null;
1601
- }
1602
- const policy = await readProjectFile(input.inspection.projectRoot, [...credentialPolicyPathParts]);
1603
- if (policy === undefined) {
1604
- return {
1605
- status: 'blocked',
1606
- blocker: 'Credential enrollment requires a secure masked input channel or an existing selected-repository App; automated noninteractive apply cannot collect a PAT.',
1607
- completedOperations: []
1608
- };
1609
- }
1610
- const scan = detectCredentialLeaks([{
1611
- source: 'imported-evidence',
1612
- label: credentialPolicyPathParts.join('/'),
1613
- text: policy.toString('utf8')
1614
- }]);
1615
- if (scan.status === 'compromised') {
1616
- return {
1617
- status: 'blocked',
1618
- blocker: scan.guidance.join(' '),
1619
- completedOperations: []
1620
- };
1621
- }
1622
- return {
1623
- status: 'completed',
1624
- resultState: 'verified',
1625
- evidencePayload: {
1626
- kind: 'credential-ready.v1',
1627
- policyPathParts: credentialPolicyPathParts,
1628
- payloadFree: true,
1629
- policyDigest: canonicalSha256(policy.toString('utf8'))
1630
- },
1631
- completedOperations: input.plan.operations.filter((op) => op.actionId === 'github.credential.verify-policy')
1632
- };
1633
- }
1634
- function remoteImportRetention(input) {
1635
- if (input.phase.id !== 'remote-ready') {
1636
- return null;
1637
- }
1638
- if (input.inspection.state.applicability.statePath !== 'bootstrap-local') {
1639
- return {
1640
- status: 'completed',
1641
- resultState: 'verified',
1642
- evidencePayload: { kind: 'remote-ready.v1', retention: 'not-applicable' },
1643
- completedOperations: input.plan.operations.filter((op) => op.actionId === 'azure.remote-ready.verify')
1644
- };
1645
- }
1646
- const importRecord = latestRecordWithPayload(input.inspection, 'remote-import-verified');
1647
- if (!importRecord || !isRecord(importRecord.payload) || importRecord.payload.kind !== remoteImportPayloadKind) {
1648
- return {
1649
- status: 'blocked',
1650
- blocker: 'Remote import retention requires immutable remote-import evidence with state and key identifiers.',
1651
- completedOperations: []
1652
- };
1653
- }
1654
- const encryptedStatePathParts = pathPartLists(importRecord.payload.encryptedStatePathParts, 'encryptedStatePathParts');
1655
- const encryptionKeyPathParts = pathPartLists(importRecord.payload.encryptionKeyPathParts, 'encryptionKeyPathParts');
1656
- const retainedAt = input.now.toISOString();
1657
- const disposeAfter = new Date(input.now.getTime() + 30 * dayMs).toISOString();
1658
- const state = cloneState(input.inspection.state);
1659
- state.bootstrapState = {
1660
- status: 'retained',
1661
- remoteImportEvidenceId: importRecord.evidenceId,
1662
- remoteImportEvidenceDigest: evidenceHeaderDigest(importRecord.header),
1663
- retainedAt,
1664
- disposeAfter,
1665
- encryptedStatePathParts,
1666
- encryptionKeyPathParts
1667
- };
1668
- return {
1669
- status: 'completed',
1670
- resultState: 'retained',
1671
- stateOverride: state,
1672
- evidencePayload: {
1673
- kind: 'remote-ready.v1',
1674
- retention: state.bootstrapState
1675
- },
1676
- completedOperations: input.plan.operations.filter((op) => op.actionId === 'azure.remote-ready.verify')
1677
- };
1678
- }
1679
- function pathPartLists(value, label) {
1680
- if (!Array.isArray(value)) {
1681
- throw new Error(`${label} must be an array of path-part arrays.`);
1682
- }
1683
- return value.map((entry, index) => validateArtifactPathParts(entry, `${label}[${index}]`));
1684
- }
1685
- async function executeBootstrapStateDisposal(input) {
1686
- if (input.phase.id !== 'bootstrap-state-disposed') {
1687
- return null;
1688
- }
1689
- const retention = input.inspection.state.bootstrapState;
1690
- if (!retention || input.inspection.state.applicability.statePath !== 'bootstrap-local') {
1691
- return {
1692
- status: 'completed',
1693
- resultState: 'inapplicable',
1694
- evidencePayload: { kind: bootstrapStateDisposedPayloadKind, reason: 'no retained bootstrap state' },
1695
- completedOperations: []
1696
- };
1697
- }
1698
- if (retention.status === 'disposed') {
1699
- return {
1700
- status: 'completed',
1701
- resultState: 'disposed',
1702
- evidencePayload: { kind: bootstrapStateDisposedPayloadKind, reason: 'already disposed', deletionEvidenceId: retention.deletionEvidenceId ?? null },
1703
- completedOperations: []
1704
- };
1705
- }
1706
- if (Date.parse(retention.disposeAfter) > input.now.getTime()) {
1707
- return {
1708
- status: 'blocked',
1709
- blocker: `Retained bootstrap state is not disposable until ${retention.disposeAfter}.`,
1710
- completedOperations: []
1711
- };
1712
- }
1713
- const importRecord = input.inspection.evidence.find((record) => record.evidenceId === retention.remoteImportEvidenceId);
1714
- if (!importRecord || evidenceHeaderDigest(importRecord.header) !== retention.remoteImportEvidenceDigest) {
1715
- return {
1716
- status: 'blocked',
1717
- blocker: 'Destructive disposal requires the immutable remote-import evidence referenced by retained bootstrap state.',
1718
- completedOperations: []
1719
- };
1720
- }
1721
- if (!isRecord(importRecord.payload) || importRecord.payload.kind !== remoteImportPayloadKind) {
1722
- return {
1723
- status: 'blocked',
1724
- blocker: 'Destructive disposal requires verified remote backend and no-change evidence.',
1725
- completedOperations: []
1726
- };
1727
- }
1728
- const remoteBackendDigest = importRecord.payload.remoteBackendDigest;
1729
- const noChangePlanDigest = importRecord.payload.noChangePlanDigest;
1730
- if (typeof remoteBackendDigest !== 'string' ||
1731
- !/^[a-f0-9]{64}$/u.test(remoteBackendDigest) ||
1732
- typeof noChangePlanDigest !== 'string' ||
1733
- !/^[a-f0-9]{64}$/u.test(noChangePlanDigest)) {
1734
- return {
1735
- status: 'blocked',
1736
- blocker: 'Destructive disposal requires payload-free remote backend and no-change plan digests.',
1737
- completedOperations: []
1738
- };
1739
- }
1740
- const allPaths = [...retention.encryptedStatePathParts, ...retention.encryptionKeyPathParts]
1741
- .map((parts) => validateArtifactPathParts([...parts], 'Bootstrap disposal path'));
1742
- const incompleteCleanup = [];
1743
- for (const parts of allPaths) {
1744
- try {
1745
- const target = await resolveProjectPath(input.inspection.projectRoot, parts);
1746
- const details = await stat(target);
1747
- if (!details.isFile()) {
1748
- incompleteCleanup.push(`${parts.join('/')} is not a regular file.`);
1749
- }
1750
- }
1751
- catch (error) {
1752
- incompleteCleanup.push(`${parts.join('/')} was already absent before disposal: ${errorMessage(error)}`);
1753
- }
1754
- }
1755
- const state = cloneState(input.inspection.state);
1756
- state.bootstrapState = {
1757
- ...retention,
1758
- status: 'disposed',
1759
- disposedAt: input.now.toISOString(),
1760
- deletionEvidenceId: `${input.phase.id}-${safeTimestamp(input.now.toISOString())}`,
1761
- incompleteCleanup
1762
- };
1763
- return {
1764
- status: 'completed',
1765
- resultState: 'disposed',
1766
- stateOverride: state,
1767
- evidencePayload: {
1768
- kind: bootstrapStateDisposedPayloadKind,
1769
- deletedPathParts: allPaths,
1770
- remoteBackendDigest,
1771
- noChangePlanDigest,
1772
- incompleteCleanup,
1773
- payloadFree: true
1774
- },
1775
- fileMutations: allPaths.map((parts) => ({ type: 'delete', pathParts: parts })),
1776
- completedOperations: input.plan.operations.filter((op) => op.actionId === 'local.bootstrap-state.dispose'),
1777
- cleanupWarnings: incompleteCleanup
1778
- };
1779
- }
1780
- function assertGreenRedProof(input) {
1781
- const record = latestRecordWithPayload(input.inspection, 'green-red-proof');
1782
- if (!record || !isRecord(record.payload) || record.payload.kind !== greenRedPayloadKind) {
1783
- throw new Error('Ruleset enforcement requires current green-red-proof evidence with typed proof payload.');
1784
- }
1785
- const green = isRecord(record.payload.green) ? record.payload.green : null;
1786
- const red = isRecord(record.payload.deliberateRed) ? record.payload.deliberateRed : null;
1787
- const greenConclusion = green?.conclusion;
1788
- const redConclusion = red?.conclusion;
1789
- const badConclusions = [greenConclusion, redConclusion].filter((value) => value === 'skipped' || value === 'cancelled' || value === 'neutral');
1790
- if (badConclusions.length > 0) {
1791
- throw new Error('Skipped, cancelled, and neutral checks cannot satisfy green/red proof.');
1792
- }
1793
- if (!red || greenConclusion !== 'success' || redConclusion !== 'failure' || red.deliberate !== true) {
1794
- throw new Error('Ruleset enforcement requires green success and deliberate red failure proof.');
1795
- }
1796
- return record;
1797
- }
1798
- async function executeRulesetPhase(input) {
1799
- if (input.phase.id !== 'rulesets-applied' && input.phase.id !== 'live-readback') {
1800
- return null;
1801
- }
1802
- try {
1803
- assertGreenRedProof(input);
1804
- }
1805
- catch (error) {
1806
- return { status: 'blocked', blocker: errorMessage(error), completedOperations: [] };
1807
- }
1808
- const sourceDigest = rulesetSourceDigestFromEvidence(input.inspection);
1809
- if (!sourceDigest) {
1810
- return {
1811
- status: 'blocked',
1812
- blocker: 'Ruleset enforcement requires a saved ruleset source digest from workflow-source-ready evidence.',
1813
- completedOperations: []
1814
- };
1815
- }
1816
- const adapter = input.adapters.githubRulesets;
1817
- if (!adapter) {
1818
- return {
1819
- status: 'blocked',
1820
- blocker: 'No injected GitHub ruleset adapter is configured; refusing live ruleset calls in this execution context.',
1821
- completedOperations: []
1822
- };
1823
- }
1824
- const envelopeId = input.plan.approval.envelopeId;
1825
- if (input.phase.id === 'rulesets-applied' && !envelopeId) {
1826
- return {
1827
- status: 'blocked',
1828
- blocker: 'Ruleset enforcement requires a persisted enforcement approval envelope.',
1829
- completedOperations: []
1830
- };
1831
- }
1832
- const completed = [];
1833
- const write = input.phase.id === 'rulesets-applied'
1834
- ? await adapter.applyRuleset({
1835
- repository: input.inspection.state.repository.name,
1836
- sourceDigest,
1837
- approvalEnvelopeId: envelopeId ?? 'ungated-readback'
1838
- })
1839
- : await adapter.readRuleset({
1840
- repository: input.inspection.state.repository.name,
1841
- sourceDigest
1842
- });
1843
- completed.push(...input.plan.operations.filter((op) => input.phase.id === 'rulesets-applied'
1844
- ? op.actionId === 'github.ruleset.apply' || op.actionId === 'github.ruleset.readback'
1845
- : op.actionId === 'github.ruleset.readback'));
1846
- if (write.sourceDigest !== sourceDigest || write.readbackDigest !== sourceDigest) {
1847
- return {
1848
- status: 'blocked',
1849
- blocker: 'Post-write live ruleset readback did not match the saved source digest.',
1850
- completedOperations: completed
1851
- };
1852
- }
1853
- const header = evidenceHeaderFor({
1854
- inspection: input.inspection,
1855
- phase: input.phase,
1856
- plan: input.plan,
1857
- result: 'verified',
1858
- now: input.now
1859
- });
1860
- const liveReadback = {
1861
- schemaVersion: input.inspection.state.identity.evidenceHeaderSchemaVersion,
1862
- repositoryId: header.repositoryId,
1863
- identity: header.identity,
1864
- phaseGraphHash: header.phaseGraphHash,
1865
- phaseId: header.phaseId,
1866
- baselineSha: header.baselineSha,
1867
- inputDigest: header.inputDigest,
1868
- transition: header.transition,
1869
- observedAt: input.now.toISOString(),
1870
- provider: 'github',
1871
- resourceType: 'ruleset',
1872
- resourceId: write.resourceId,
1873
- sourceDigest,
1874
- readbackDigest: write.readbackDigest,
1875
- matches: true
1876
- };
1877
- return {
1878
- status: 'completed',
1879
- resultState: 'verified',
1880
- liveReadback: [liveReadback],
1881
- evidencePayload: {
1882
- kind: `${input.phase.id}.v1`,
1883
- sourceDigest,
1884
- resourceId: write.resourceId,
1885
- readbackDigest: write.readbackDigest
1886
- },
1887
- completedOperations: completed
1888
- };
1889
- }
1890
33
  async function executeBuiltInPhase(input) {
1891
34
  const custom = input.adapters.phases?.[input.phase.id];
1892
- if (custom) {
35
+ if (custom)
1893
36
  return await custom.execute(input);
1894
- }
1895
- for (const attempt of [
1896
- executeSeedOperations,
1897
- async (execution) => executeGitOperations(execution, execution.plan.operations),
1898
- discoverPhase0,
1899
- executeActivationApproval,
1900
- executeCredentialReady,
1901
- async (execution) => remoteImportRetention(execution),
1902
- executeBootstrapStateDisposal,
1903
- executeRulesetPhase
1904
- ]) {
1905
- const outcome = await attempt(input);
1906
- if (outcome) {
1907
- return outcome;
1908
- }
1909
- }
1910
- return {
37
+ return await builtInExecutors[input.phase.id]?.(input) ?? {
1911
38
  status: 'blocked',
1912
39
  blocker: `No production adapter is configured for ${input.phase.id}; refusing success-shaped fallback.`,
1913
40
  completedOperations: []
1914
41
  };
1915
42
  }
1916
43
  function sourceOfTruthAllowsPhase(inspection, phaseId) {
1917
- if (phaseOrder(phaseId) <= phaseOrder('phase-0-complete')) {
44
+ if (phaseOrder(phaseId) <= phaseOrder('phase-0-complete'))
1918
45
  return null;
1919
- }
1920
46
  const source = inspection.sourceOfTruth;
1921
- if (phaseId === 'activation-approved' && (source.status === 'none' || source.status === 'selected')) {
47
+ if (phaseId === 'activation-approved' && (source.status === 'none' || source.status === 'selected'))
1922
48
  return null;
1923
- }
1924
- if (source.status === 'selected' && source.reconciliation.status === 'not-required') {
49
+ if (source.status === 'selected' && source.reconciliation.status === 'not-required')
1925
50
  return null;
1926
- }
1927
- if (source.status === 'seed-blocked') {
1928
- return source.blockers.join('; ');
1929
- }
1930
- if (source.status === 'ambiguous' || source.status === 'incompatible') {
51
+ if (source.status === 'seed-blocked' || source.status === 'ambiguous' || source.status === 'incompatible')
1931
52
  return source.blockers.join('; ');
1932
- }
1933
- if (source.status === 'none') {
53
+ if (source.status === 'none')
1934
54
  return source.createPlan.reason;
1935
- }
1936
55
  return 'Active governance source of truth is not ready.';
1937
56
  }
1938
57
  export async function executeApplyNext(input) {
58
+ validateManifestActivationForExecution(input.inspection.manifest);
59
+ return withProjectMutationLock(input.inspection.projectRoot, async (lease) => {
60
+ await lease.assertHeld();
61
+ if (!input.inspection.loadedState && input.inspection.state.repository.id === 'unbound' &&
62
+ input.inspection.readiness.nextReadyPhase?.startsWith('seed-')) {
63
+ await initializeExecutionAnchor(input.inspection, input.now ?? new Date());
64
+ input = { ...input, inspection: await input.reinspect() };
65
+ }
66
+ return executeApplyNextLocked(input, lease);
67
+ });
68
+ }
69
+ async function executeApplyNextLocked(input, lease) {
1939
70
  const runner = input.runner ?? new NodeCommandRunner();
1940
71
  const adapters = input.adapters ?? {};
1941
72
  const now = input.now ?? new Date();
@@ -1959,15 +90,8 @@ export async function executeApplyNext(input) {
1959
90
  if (initialPlan.approval.evaluation.approvalRequired) {
1960
91
  const preview = await previewApplyNext({ inspection: input.inspection, runner, now, execute: true });
1961
92
  return {
1962
- ...preview,
1963
- applied: false,
1964
- executedPhase: null,
1965
- noWrites: false,
1966
- executedOperations: [],
1967
- evidence: null,
1968
- stateHash: null,
1969
- rollbackPlan: initialPlan.rollbackPlan,
1970
- cleanupWarnings: []
93
+ ...preview, applied: false, executedPhase: null, noWrites: false,
94
+ executedOperations: [], evidence: null, stateHash: null, rollbackPlan: initialPlan.rollbackPlan, cleanupWarnings: []
1971
95
  };
1972
96
  }
1973
97
  const saved = await saveTransitionPlan(input.inspection.projectRoot, initialPlan);
@@ -1976,151 +100,134 @@ export async function executeApplyNext(input) {
1976
100
  const freshnessIssues = comparePlanFreshness(initialPlan, freshPlan);
1977
101
  if (freshnessIssues.length > 0) {
1978
102
  return {
1979
- schemaVersion: 1,
1980
- command: 'governance apply-next',
1981
- projectRoot: input.inspection.projectRoot,
1982
- execute: true,
1983
- applied: false,
1984
- authorized: false,
1985
- reason: 'stale-after-plan-save',
1986
- message: freshnessIssues.join(' '),
1987
- selectedPhase: initialPlan.phaseId,
1988
- executedPhase: null,
1989
- nextReadyPhase: freshInspection.readiness.nextReadyPhase,
103
+ schemaVersion: 1, command: 'governance apply-next', projectRoot: input.inspection.projectRoot,
104
+ execute: true, applied: false, authorized: false, reason: 'stale-after-plan-save', message: freshnessIssues.join(' '),
105
+ selectedPhase: initialPlan.phaseId, executedPhase: null, nextReadyPhase: freshInspection.readiness.nextReadyPhase,
1990
106
  approval: initialPlan.approval,
1991
- proposedMutations: {
1992
- local: initialPlan.mutationClasses.local,
1993
- remote: initialPlan.mutationClasses.remote,
1994
- operations: initialPlan.operations
1995
- },
1996
- savedPlan: saved,
1997
- noWrites: false,
1998
- blockers: freshnessIssues,
1999
- executedOperations: [],
2000
- evidence: null,
2001
- stateHash: null,
2002
- rollbackPlan: initialPlan.rollbackPlan,
2003
- cleanupWarnings: []
107
+ proposedMutations: { local: initialPlan.mutationClasses.local, remote: initialPlan.mutationClasses.remote, operations: initialPlan.operations },
108
+ savedPlan: saved, noWrites: false, blockers: freshnessIssues, executedOperations: [],
109
+ evidence: null, stateHash: null, rollbackPlan: initialPlan.rollbackPlan, cleanupWarnings: []
2004
110
  };
2005
111
  }
2006
112
  const sourceBlocker = sourceOfTruthAllowsPhase(freshInspection, initialPlan.phaseId);
2007
113
  if (sourceBlocker) {
2008
114
  const nextState = blockedState({ inspection: freshInspection, phase, plan: initialPlan, blocker: sourceBlocker, now });
2009
- const write = await writeOutcomeTransaction({
2010
- projectRoot: freshInspection.projectRoot,
2011
- plan: initialPlan,
2012
- nextState
2013
- });
115
+ const write = await writeOutcomeTransaction({ projectRoot: freshInspection.projectRoot, plan: initialPlan, nextState });
2014
116
  return executionBlockedResult(freshInspection, initialPlan, saved, sourceBlocker, [], write.stateHash);
2015
117
  }
118
+ await lease.assertHeld();
2016
119
  const outcome = await executeBuiltInPhase({
2017
- inspection: freshInspection,
2018
- plan: initialPlan,
2019
- phase,
2020
- runner,
2021
- adapters,
2022
- now
120
+ inspection: freshInspection, plan: initialPlan, phase, runner, adapters, now,
121
+ clock: () => input.now ?? new Date(), lease
2023
122
  });
2024
123
  const completedOperations = outcome.completedOperations ?? [];
124
+ for (const completed of completedOperations) {
125
+ assertOperationAllowed(phase, completed);
126
+ if (!initialPlan.operations.some((planned) => canonicalSha256(planned) === canonicalSha256(completed))) {
127
+ throw new Error(`Completed operation ${completed.actionId} was not in the reviewed plan.`);
128
+ }
129
+ }
130
+ for (const mutation of outcome.fileMutations ?? []) {
131
+ const allowed = initialPlan.operations.some((operation) => {
132
+ const prefix = operation.destination.pathParts;
133
+ return !operation.remote && prefix && prefix.every((part, index) => mutation.pathParts[index] === part) &&
134
+ ['write-seed-tasks', 'write-openspec-governance', 'write-workflows', 'write-ruleset-source', 'delete-local-state'].includes(operation.mutationClass) &&
135
+ (mutation.type !== 'delete' || operation.destructive && prefix.length === mutation.pathParts.length);
136
+ });
137
+ if (!allowed)
138
+ throw new Error(`Outcome file mutation ${mutation.pathParts.join('/')} is outside the reviewed plan destinations.`);
139
+ }
140
+ const postSnapshot = await readActivationInputSnapshot(freshInspection.projectRoot, freshInspection.manifest, runner);
141
+ if (postSnapshot.baselineSha !== initialPlan.baselineDigest || phaseInputDigest(phase.id, postSnapshot) !== initialPlan.inputDigest) {
142
+ return executionBlockedResult(freshInspection, initialPlan, saved, 'Relevant project inputs changed during execution; no outcome was persisted.', completedOperations, freshInspection.loadedState?.contentHash ?? '', outcome.cleanupWarnings ?? [], false);
143
+ }
2025
144
  if (outcome.status === 'blocked') {
2026
145
  const blocker = outcome.blocker ?? `Phase ${phase.id} blocked.`;
2027
146
  if (outcome.retryableWithoutStateMutation) {
2028
- const stateHash = freshInspection.loadedState?.contentHash ??
2029
- activationStateContentHash(canonicalJson(freshInspection.state));
147
+ const stateHash = freshInspection.loadedState?.contentHash ?? activationStateContentHash(canonicalJson(freshInspection.state));
2030
148
  return executionBlockedResult(freshInspection, initialPlan, saved, blocker, completedOperations, stateHash, outcome.cleanupWarnings ?? [], false);
2031
149
  }
2032
150
  const nextState = blockedState({ inspection: freshInspection, phase, plan: initialPlan, blocker, now });
2033
- const write = await writeOutcomeTransaction({
2034
- projectRoot: freshInspection.projectRoot,
2035
- plan: initialPlan,
2036
- nextState
2037
- });
151
+ const write = await writeOutcomeTransaction({ projectRoot: freshInspection.projectRoot, plan: initialPlan, nextState });
2038
152
  return executionBlockedResult(freshInspection, initialPlan, saved, blocker, completedOperations, write.stateHash, outcome.cleanupWarnings ?? []);
2039
153
  }
2040
154
  const resultState = outcome.resultState ?? 'verified';
2041
155
  if (!phase.terminalStates.includes(resultState)) {
2042
156
  const blocker = `Phase adapter returned ${resultState}, which is not an allowed terminal state for ${phase.id}.`;
2043
157
  const nextState = blockedState({ inspection: freshInspection, phase, plan: initialPlan, blocker, now });
2044
- const write = await writeOutcomeTransaction({
2045
- projectRoot: freshInspection.projectRoot,
2046
- plan: initialPlan,
2047
- nextState
2048
- });
158
+ const write = await writeOutcomeTransaction({ projectRoot: freshInspection.projectRoot, plan: initialPlan, nextState });
2049
159
  return executionBlockedResult(freshInspection, initialPlan, saved, blocker, completedOperations, write.stateHash, outcome.cleanupWarnings ?? []);
2050
160
  }
161
+ if (outcome.stateOverride && outcome.stateOverride.repository.id !== freshInspection.state.repository.id) {
162
+ throw new Error('A phase outcome must not replace the immutable local execution anchor.');
163
+ }
164
+ if (outcome.stateOverride) {
165
+ if (canonicalSha256(outcome.stateOverride.phases) !== canonicalSha256(freshInspection.state.phases)) {
166
+ throw new Error('A phase adapter cannot rewrite authoritative phase history or unrelated phase states.');
167
+ }
168
+ if (phase.id !== 'phase-0-complete' &&
169
+ canonicalSha256(outcome.stateOverride.remoteBinding ?? null) !== canonicalSha256(freshInspection.state.remoteBinding ?? null)) {
170
+ throw new Error('Only verified Phase 0 discovery may establish a remote repository binding.');
171
+ }
172
+ }
173
+ if (phase.approvalGate.required) {
174
+ const finalInspection = await input.reinspect();
175
+ const finalPlan = await buildSavedTransitionPlan({ inspection: finalInspection, runner, now: input.now ?? new Date() });
176
+ if (!finalPlan || finalPlan.phaseId !== phase.id || finalPlan.approval.evaluation.approvalRequired ||
177
+ finalPlan.approval.envelopeHash !== initialPlan.approval.envelopeHash) {
178
+ return executionBlockedResult(freshInspection, initialPlan, saved, 'Approval changed or expired before outcome persistence; no successful outcome was recorded.', completedOperations, freshInspection.loadedState?.contentHash ?? '', outcome.cleanupWarnings ?? [], false);
179
+ }
180
+ }
181
+ const outcomeInspection = { ...freshInspection, state: outcome.stateOverride ?? freshInspection.state };
182
+ const boundPayload = isRecord(outcome.evidencePayload)
183
+ ? { ...outcome.evidencePayload, planDigest: initialPlan.planDigest, savedPlanDigest: canonicalSha256(initialPlan) }
184
+ : outcome.evidencePayload;
185
+ const outcomeNow = input.now ?? new Date();
2051
186
  let evidenceRecord;
2052
187
  let evidenceParts;
2053
188
  let evidenceReference;
2054
189
  if (resultState !== 'approved') {
2055
190
  const evidenceId = `${phase.id}-${safeTimestamp(now.toISOString())}`;
2056
191
  const header = evidenceHeaderFor({
2057
- inspection: freshInspection,
2058
- phase,
2059
- plan: initialPlan,
2060
- result: resultState,
2061
- now
192
+ inspection: outcomeInspection, phase, plan: initialPlan, result: resultState, now: outcomeNow,
193
+ payload: boundPayload, liveReadback: outcome.liveReadback
2062
194
  });
2063
195
  evidenceRecord = {
2064
- evidenceId,
2065
- header,
196
+ evidenceId, header,
2066
197
  ...(outcome.liveReadback ? { liveReadback: outcome.liveReadback } : {}),
2067
- ...(outcome.evidencePayload !== undefined ? { payload: outcome.evidencePayload } : {})
198
+ ...(boundPayload !== undefined ? { payload: boundPayload } : {})
2068
199
  };
2069
200
  evidenceParts = evidencePathParts(evidenceId);
2070
- evidenceReference = {
2071
- phaseId: phase.id,
2072
- evidenceId,
2073
- headerDigest: evidenceHeaderDigest(header),
2074
- result: header.result
2075
- };
201
+ evidenceReference = { phaseId: phase.id, evidenceId, headerDigest: evidenceHeaderDigest(header), result: header.result };
202
+ const validation = validateEvidenceFreshness(evidenceRecord, {
203
+ ...freshInspection.contexts[phase.id],
204
+ remoteBindingDigest: remoteBindingDigest(outcomeInspection.state.remoteBinding),
205
+ evidenceReferences: [evidenceReference], reviewedPlans: [initialPlan],
206
+ ...(phase.id === 'seed-archived' && postSnapshot.workflowSpecDigest ? { workflowSpecDigest: postSnapshot.workflowSpecDigest } : {}),
207
+ now: outcomeNow
208
+ });
209
+ if (!validation.valid) {
210
+ return executionBlockedResult(freshInspection, initialPlan, saved, `Completed outcome rejected: ${validation.issues.map((issue) => issue.message).join(' ')}`, completedOperations, freshInspection.loadedState?.contentHash ?? '', outcome.cleanupWarnings ?? [], false);
211
+ }
2076
212
  }
2077
213
  const nextState = nextStateForOutcome({
2078
- inspection: freshInspection,
2079
- phase,
2080
- plan: initialPlan,
2081
- resultState,
2082
- evidenceReference,
2083
- override: outcome.stateOverride,
2084
- now
214
+ inspection: freshInspection, phase, plan: initialPlan, resultState,
215
+ evidenceReference, override: outcome.stateOverride, now
2085
216
  });
2086
217
  const write = await writeOutcomeTransaction({
2087
- projectRoot: freshInspection.projectRoot,
2088
- plan: initialPlan,
2089
- nextState,
2090
- evidenceRecord,
2091
- evidencePathParts: evidenceParts,
2092
- fileMutations: outcome.fileMutations
218
+ projectRoot: freshInspection.projectRoot, plan: initialPlan, nextState, evidenceRecord,
219
+ evidencePathParts: evidenceParts, fileMutations: outcome.fileMutations, filePreconditions: outcome.filePreconditions
2093
220
  });
2094
221
  const rollbackPlan = rollbackPlanForPhase(phase, completedOperations);
2095
222
  return {
2096
- schemaVersion: 1,
2097
- command: 'governance apply-next',
2098
- projectRoot: freshInspection.projectRoot,
2099
- execute: true,
2100
- applied: true,
2101
- authorized: true,
2102
- reason: 'phase-executed',
2103
- message: `Executed one phase: ${phase.id}.`,
2104
- selectedPhase: phase.id,
2105
- executedPhase: phase.id,
2106
- nextReadyPhase: phase.id,
2107
- approval: initialPlan.approval,
2108
- proposedMutations: {
2109
- local: initialPlan.mutationClasses.local,
2110
- remote: initialPlan.mutationClasses.remote,
2111
- operations: initialPlan.operations
2112
- },
2113
- savedPlan: saved,
2114
- noWrites: false,
2115
- blockers: [],
2116
- executedOperations: [
2117
- ...completedOperations,
2118
- ...(evidenceParts ? [evidenceWriteOperation(phase, evidenceParts)] : []),
2119
- stateWriteOperation(phase)
2120
- ],
2121
- evidence: write.evidence,
2122
- stateHash: write.stateHash,
2123
- rollbackPlan,
223
+ schemaVersion: 1, command: 'governance apply-next', projectRoot: freshInspection.projectRoot,
224
+ execute: true, applied: true, authorized: true, reason: 'phase-executed',
225
+ message: `Executed one phase: ${phase.id}.`, selectedPhase: phase.id, executedPhase: phase.id,
226
+ nextReadyPhase: phase.id, approval: initialPlan.approval,
227
+ proposedMutations: { local: initialPlan.mutationClasses.local, remote: initialPlan.mutationClasses.remote, operations: initialPlan.operations },
228
+ savedPlan: saved, noWrites: false, blockers: [],
229
+ executedOperations: [...completedOperations, ...(evidenceParts ? [evidenceWriteOperation(phase, evidenceParts)] : []), stateWriteOperation(phase)],
230
+ evidence: write.evidence, stateHash: write.stateHash, rollbackPlan,
2124
231
  cleanupWarnings: [...rollbackPlan.cleanupWarnings, ...(outcome.cleanupWarnings ?? [])]
2125
232
  };
2126
233
  }
@@ -2128,33 +235,13 @@ function executionBlockedResult(inspection, plan, saved, blocker, completedOpera
2128
235
  const phase = phaseById(inspection.graph, plan.phaseId);
2129
236
  const rollbackPlan = rollbackPlanForPhase(phase, completedOperations);
2130
237
  return {
2131
- schemaVersion: 1,
2132
- command: 'governance apply-next',
2133
- projectRoot: inspection.projectRoot,
2134
- execute: true,
2135
- applied: false,
2136
- authorized: false,
2137
- reason: 'blocked',
2138
- message: blocker,
2139
- selectedPhase: plan.phaseId,
2140
- executedPhase: null,
2141
- nextReadyPhase: plan.phaseId,
2142
- approval: plan.approval,
2143
- proposedMutations: {
2144
- local: plan.mutationClasses.local,
2145
- remote: plan.mutationClasses.remote,
2146
- operations: plan.operations
2147
- },
2148
- savedPlan: saved,
2149
- noWrites: false,
2150
- blockers: [blocker],
2151
- executedOperations: [
2152
- ...completedOperations,
2153
- ...(stateWritten ? [stateWriteOperation(phase)] : [])
2154
- ],
2155
- evidence: null,
2156
- stateHash: stateHashValue,
2157
- rollbackPlan,
238
+ schemaVersion: 1, command: 'governance apply-next', projectRoot: inspection.projectRoot,
239
+ execute: true, applied: false, authorized: false, reason: 'blocked', message: blocker,
240
+ selectedPhase: plan.phaseId, executedPhase: null, nextReadyPhase: plan.phaseId, approval: plan.approval,
241
+ proposedMutations: { local: plan.mutationClasses.local, remote: plan.mutationClasses.remote, operations: plan.operations },
242
+ savedPlan: saved, noWrites: false, blockers: [blocker],
243
+ executedOperations: [...completedOperations, ...(stateWritten ? [stateWriteOperation(phase)] : [])],
244
+ evidence: null, stateHash: stateHashValue, rollbackPlan,
2158
245
  cleanupWarnings: [...rollbackPlan.cleanupWarnings, ...cleanupWarnings]
2159
246
  };
2160
247
  }