@b00dea/cockpit 0.0.0-bootstrap.0 → 0.1.0

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 (363) hide show
  1. package/CHANGELOG.md +375 -0
  2. package/LICENSE +21 -0
  3. package/README.md +21 -1
  4. package/bin/cockpit.mjs +33 -0
  5. package/cockpit.schema.json +2574 -0
  6. package/dist/autonomy.d.ts +44 -0
  7. package/dist/autonomy.js +156 -0
  8. package/dist/bounded-read.d.ts +14 -0
  9. package/dist/bounded-read.js +42 -0
  10. package/dist/browser/commands.d.ts +62 -0
  11. package/dist/browser/commands.js +349 -0
  12. package/dist/browser/package-paths.d.ts +1 -0
  13. package/dist/browser/package-paths.js +6 -0
  14. package/dist/browser/proxy.d.ts +22 -0
  15. package/dist/browser/proxy.js +37 -0
  16. package/dist/browser/runtime.d.ts +117 -0
  17. package/dist/browser/runtime.js +183 -0
  18. package/dist/browser/surfaces.d.ts +141 -0
  19. package/dist/browser/surfaces.js +556 -0
  20. package/dist/browser/ui-gate.d.ts +15 -0
  21. package/dist/browser/ui-gate.js +18 -0
  22. package/dist/cli-guard.d.ts +5 -0
  23. package/dist/cli-guard.js +71 -0
  24. package/dist/cli-main.d.ts +139 -0
  25. package/dist/cli-main.js +1405 -0
  26. package/dist/cli.d.ts +3 -0
  27. package/dist/cli.js +16 -0
  28. package/dist/command-table.d.ts +113 -0
  29. package/dist/command-table.js +512 -0
  30. package/dist/config/load.d.ts +171 -0
  31. package/dist/config/load.js +345 -0
  32. package/dist/config/sibling-toml.d.ts +48 -0
  33. package/dist/config/sibling-toml.js +160 -0
  34. package/dist/config/surface-tools-toml.d.ts +9 -0
  35. package/dist/config/surface-tools-toml.js +37 -0
  36. package/dist/config/verified-write.d.ts +43 -0
  37. package/dist/config/verified-write.js +89 -0
  38. package/dist/diagnostics.d.ts +26 -0
  39. package/dist/diagnostics.js +335 -0
  40. package/dist/emit.d.ts +22 -0
  41. package/dist/emit.js +43 -0
  42. package/dist/entrypoint-kinds.d.ts +3 -0
  43. package/dist/entrypoint-kinds.js +11 -0
  44. package/dist/environment/application/destructive.d.ts +6 -0
  45. package/dist/environment/application/destructive.js +425 -0
  46. package/dist/environment/application/doctor.d.ts +89 -0
  47. package/dist/environment/application/doctor.js +235 -0
  48. package/dist/environment/application/guard.d.ts +43 -0
  49. package/dist/environment/application/guard.js +622 -0
  50. package/dist/environment/application/health.d.ts +134 -0
  51. package/dist/environment/application/health.js +379 -0
  52. package/dist/environment/application/lifecycle-context.d.ts +228 -0
  53. package/dist/environment/application/lifecycle-context.js +154 -0
  54. package/dist/environment/application/lifecycle-state.d.ts +25 -0
  55. package/dist/environment/application/lifecycle-state.js +201 -0
  56. package/dist/environment/application/lifecycle.d.ts +17 -0
  57. package/dist/environment/application/lifecycle.js +729 -0
  58. package/dist/environment/application/logs.d.ts +45 -0
  59. package/dist/environment/application/logs.js +127 -0
  60. package/dist/environment/application/setup.d.ts +129 -0
  61. package/dist/environment/application/setup.js +741 -0
  62. package/dist/environment/application/shared.d.ts +11 -0
  63. package/dist/environment/application/shared.js +61 -0
  64. package/dist/environment/claims.d.ts +389 -0
  65. package/dist/environment/claims.js +1709 -0
  66. package/dist/environment/codex-hook-trust.d.ts +11 -0
  67. package/dist/environment/codex-hook-trust.js +185 -0
  68. package/dist/environment/compose-environment.d.ts +1 -0
  69. package/dist/environment/compose-environment.js +6 -0
  70. package/dist/environment/compose-ownership.d.ts +21 -0
  71. package/dist/environment/compose-ownership.js +117 -0
  72. package/dist/environment/compose-topology.d.ts +117 -0
  73. package/dist/environment/compose-topology.js +413 -0
  74. package/dist/environment/contract.d.ts +489 -0
  75. package/dist/environment/contract.js +761 -0
  76. package/dist/environment/declaration.d.ts +14 -0
  77. package/dist/environment/declaration.js +39 -0
  78. package/dist/environment/destructive-composition.d.ts +29 -0
  79. package/dist/environment/destructive-composition.js +333 -0
  80. package/dist/environment/docker-daemon.d.ts +30 -0
  81. package/dist/environment/docker-daemon.js +78 -0
  82. package/dist/environment/docker-endpoint.d.ts +10 -0
  83. package/dist/environment/docker-endpoint.js +19 -0
  84. package/dist/environment/env-schema-observation.d.ts +46 -0
  85. package/dist/environment/env-schema-observation.js +241 -0
  86. package/dist/environment/evidence.d.ts +136 -0
  87. package/dist/environment/evidence.js +437 -0
  88. package/dist/environment/executable.d.ts +26 -0
  89. package/dist/environment/executable.js +167 -0
  90. package/dist/environment/generated-application/authorities.d.ts +5 -0
  91. package/dist/environment/generated-application/authorities.js +61 -0
  92. package/dist/environment/generated-application/bootstrap-skill.d.ts +16 -0
  93. package/dist/environment/generated-application/bootstrap-skill.js +328 -0
  94. package/dist/environment/generated-application/check.d.ts +13 -0
  95. package/dist/environment/generated-application/check.js +189 -0
  96. package/dist/environment/generated-application/custom-surface.d.ts +26 -0
  97. package/dist/environment/generated-application/custom-surface.js +289 -0
  98. package/dist/environment/generated-application/prepare.d.ts +29 -0
  99. package/dist/environment/generated-application/prepare.js +276 -0
  100. package/dist/environment/generated-application/publication.d.ts +33 -0
  101. package/dist/environment/generated-application/publication.js +547 -0
  102. package/dist/environment/generated-application/shared.d.ts +484 -0
  103. package/dist/environment/generated-application/shared.js +349 -0
  104. package/dist/environment/generated-application/surface-observation.d.ts +4 -0
  105. package/dist/environment/generated-application/surface-observation.js +69 -0
  106. package/dist/environment/generated-application/surface-outputs.d.ts +71 -0
  107. package/dist/environment/generated-application/surface-outputs.js +389 -0
  108. package/dist/environment/generated-application/surfaces.d.ts +3 -0
  109. package/dist/environment/generated-application/surfaces.js +699 -0
  110. package/dist/environment/generated-application/sync.d.ts +6 -0
  111. package/dist/environment/generated-application/sync.js +109 -0
  112. package/dist/environment/git-observation.d.ts +183 -0
  113. package/dist/environment/git-observation.js +544 -0
  114. package/dist/environment/health-composition.d.ts +11 -0
  115. package/dist/environment/health-composition.js +628 -0
  116. package/dist/environment/host-observation.d.ts +56 -0
  117. package/dist/environment/host-observation.js +52 -0
  118. package/dist/environment/identity.d.ts +48 -0
  119. package/dist/environment/identity.js +85 -0
  120. package/dist/environment/init-application.d.ts +308 -0
  121. package/dist/environment/init-application.js +697 -0
  122. package/dist/environment/install-policy-observation.d.ts +71 -0
  123. package/dist/environment/install-policy-observation.js +284 -0
  124. package/dist/environment/install-policy.d.ts +48 -0
  125. package/dist/environment/install-policy.js +96 -0
  126. package/dist/environment/instance.d.ts +33 -0
  127. package/dist/environment/instance.js +122 -0
  128. package/dist/environment/instruction-pointer.d.ts +19 -0
  129. package/dist/environment/instruction-pointer.js +144 -0
  130. package/dist/environment/lifecycle-composition.d.ts +29 -0
  131. package/dist/environment/lifecycle-composition.js +675 -0
  132. package/dist/environment/lifecycle-stage.d.ts +115 -0
  133. package/dist/environment/lifecycle-stage.js +398 -0
  134. package/dist/environment/lifecycle.d.ts +75 -0
  135. package/dist/environment/lifecycle.js +354 -0
  136. package/dist/environment/logs.d.ts +132 -0
  137. package/dist/environment/logs.js +519 -0
  138. package/dist/environment/observations.d.ts +46 -0
  139. package/dist/environment/observations.js +344 -0
  140. package/dist/environment/operator-application.d.ts +50 -0
  141. package/dist/environment/operator-application.js +155 -0
  142. package/dist/environment/ports.d.ts +182 -0
  143. package/dist/environment/ports.js +1205 -0
  144. package/dist/environment/posture-observation.d.ts +68 -0
  145. package/dist/environment/posture-observation.js +253 -0
  146. package/dist/environment/read-composition.d.ts +27 -0
  147. package/dist/environment/read-composition.js +352 -0
  148. package/dist/environment/readiness-composition.d.ts +38 -0
  149. package/dist/environment/readiness-composition.js +130 -0
  150. package/dist/environment/reclaim.d.ts +333 -0
  151. package/dist/environment/reclaim.js +1835 -0
  152. package/dist/environment/repository-identity.d.ts +13 -0
  153. package/dist/environment/repository-identity.js +108 -0
  154. package/dist/environment/runtime-measurement-record.d.ts +38 -0
  155. package/dist/environment/runtime-measurement-record.js +111 -0
  156. package/dist/environment/runtime-measurement.d.ts +27 -0
  157. package/dist/environment/runtime-measurement.js +224 -0
  158. package/dist/environment/runtime-probe-residue.d.ts +43 -0
  159. package/dist/environment/runtime-probe-residue.js +148 -0
  160. package/dist/environment/runtime-probe.d.ts +72 -0
  161. package/dist/environment/runtime-probe.js +725 -0
  162. package/dist/environment/runtime.d.ts +505 -0
  163. package/dist/environment/runtime.js +2335 -0
  164. package/dist/environment/seed.d.ts +16 -0
  165. package/dist/environment/seed.js +43 -0
  166. package/dist/environment/selftest-application.d.ts +86 -0
  167. package/dist/environment/selftest-application.js +713 -0
  168. package/dist/environment/sensitive.d.ts +3 -0
  169. package/dist/environment/sensitive.js +48 -0
  170. package/dist/environment/setup-observation.d.ts +62 -0
  171. package/dist/environment/setup-observation.js +1473 -0
  172. package/dist/environment/setup.d.ts +43 -0
  173. package/dist/environment/setup.js +176 -0
  174. package/dist/environment/sibling-instructions.d.ts +33 -0
  175. package/dist/environment/sibling-instructions.js +224 -0
  176. package/dist/environment/sibling-registration.d.ts +45 -0
  177. package/dist/environment/sibling-registration.js +538 -0
  178. package/dist/environment/state.d.ts +371 -0
  179. package/dist/environment/state.js +2013 -0
  180. package/dist/environment/templates.d.ts +140 -0
  181. package/dist/environment/templates.js +685 -0
  182. package/dist/environment/tool-registration.d.ts +9 -0
  183. package/dist/environment/tool-registration.js +96 -0
  184. package/dist/environment/upgrade-application.d.ts +19 -0
  185. package/dist/environment/upgrade-application.js +175 -0
  186. package/dist/environment-exec/execute.d.ts +102 -0
  187. package/dist/environment-exec/execute.js +1175 -0
  188. package/dist/environment-exec/libexec-program.d.ts +6 -0
  189. package/dist/environment-exec/libexec-program.js +22 -0
  190. package/dist/environment-exec/verified-digest.d.ts +4 -0
  191. package/dist/environment-exec/verified-digest.js +16 -0
  192. package/dist/exact-keys.d.ts +7 -0
  193. package/dist/exact-keys.js +15 -0
  194. package/dist/fix-catalogue.d.ts +32 -0
  195. package/dist/fix-catalogue.js +60 -0
  196. package/dist/freshness.d.ts +29 -0
  197. package/dist/freshness.js +75 -0
  198. package/dist/generated-environment/adopt.d.ts +16 -0
  199. package/dist/generated-environment/adopt.js +275 -0
  200. package/dist/generated-environment/command.d.ts +47 -0
  201. package/dist/generated-environment/command.js +75 -0
  202. package/dist/generated-environment/transaction.d.ts +186 -0
  203. package/dist/generated-environment/transaction.js +1770 -0
  204. package/dist/grading/evaluate.d.ts +71 -0
  205. package/dist/grading/evaluate.js +403 -0
  206. package/dist/grading/grade.d.ts +33 -0
  207. package/dist/grading/grade.js +57 -0
  208. package/dist/guards/evaluator.d.ts +113 -0
  209. package/dist/guards/evaluator.js +1226 -0
  210. package/dist/guards/harness-adapter.d.ts +17 -0
  211. package/dist/guards/harness-adapter.js +57 -0
  212. package/dist/guards/host.d.ts +8 -0
  213. package/dist/guards/host.js +10 -0
  214. package/dist/guards/invocation-parser.d.ts +66 -0
  215. package/dist/guards/invocation-parser.js +595 -0
  216. package/dist/guards/local-identity.d.ts +40 -0
  217. package/dist/guards/local-identity.js +123 -0
  218. package/dist/guards/policy-pin.d.ts +13 -0
  219. package/dist/guards/policy-pin.js +175 -0
  220. package/dist/guards/protected-locations.d.ts +62 -0
  221. package/dist/guards/protected-locations.js +461 -0
  222. package/dist/guards/read-only.d.ts +37 -0
  223. package/dist/guards/read-only.js +315 -0
  224. package/dist/init/command.d.ts +40 -0
  225. package/dist/init/command.js +310 -0
  226. package/dist/init/derive.d.ts +51 -0
  227. package/dist/init/derive.js +715 -0
  228. package/dist/init/draft.d.ts +138 -0
  229. package/dist/init/draft.js +1205 -0
  230. package/dist/init/render.d.ts +35 -0
  231. package/dist/init/render.js +82 -0
  232. package/dist/init/scaffold.d.ts +53 -0
  233. package/dist/init/scaffold.js +121 -0
  234. package/dist/invocation.d.ts +13 -0
  235. package/dist/invocation.js +20 -0
  236. package/dist/package-metadata.d.ts +9 -0
  237. package/dist/package-metadata.js +19 -0
  238. package/dist/readiness.d.ts +53 -0
  239. package/dist/readiness.js +88 -0
  240. package/dist/references/named.d.ts +24 -0
  241. package/dist/references/named.js +27 -0
  242. package/dist/refusal.d.ts +15 -0
  243. package/dist/refusal.js +7 -0
  244. package/dist/registry-derivation.d.ts +31 -0
  245. package/dist/registry-derivation.js +32 -0
  246. package/dist/registry-resolve/command.d.ts +47 -0
  247. package/dist/registry-resolve/command.js +226 -0
  248. package/dist/registry-resolve/registry.d.ts +68 -0
  249. package/dist/registry-resolve/registry.js +292 -0
  250. package/dist/registry-resolve/render-shell.d.ts +18 -0
  251. package/dist/registry-resolve/render-shell.js +36 -0
  252. package/dist/repair.d.ts +29 -0
  253. package/dist/repair.js +8 -0
  254. package/dist/schema-json.d.ts +1 -0
  255. package/dist/schema-json.js +177 -0
  256. package/dist/schema.d.ts +1322 -0
  257. package/dist/schema.js +934 -0
  258. package/dist/setup/command.d.ts +4 -0
  259. package/dist/setup/command.js +4 -0
  260. package/dist/sha256.d.ts +1 -0
  261. package/dist/sha256.js +4 -0
  262. package/dist/surfaces/adoption.d.ts +41 -0
  263. package/dist/surfaces/adoption.js +217 -0
  264. package/dist/surfaces/consumer-instructions.d.ts +20 -0
  265. package/dist/surfaces/consumer-instructions.js +43 -0
  266. package/dist/surfaces/contract.d.ts +42 -0
  267. package/dist/surfaces/contract.js +169 -0
  268. package/dist/surfaces/core-declarations.d.ts +24 -0
  269. package/dist/surfaces/core-declarations.js +79 -0
  270. package/dist/surfaces/development-markers.d.ts +1 -0
  271. package/dist/surfaces/development-markers.js +18 -0
  272. package/dist/surfaces/generator-adapter.d.ts +71 -0
  273. package/dist/surfaces/generator-adapter.js +809 -0
  274. package/dist/surfaces/harness-registry.d.ts +180 -0
  275. package/dist/surfaces/harness-registry.js +117 -0
  276. package/dist/surfaces/mcp.d.ts +95 -0
  277. package/dist/surfaces/mcp.js +186 -0
  278. package/dist/surfaces/merge.d.ts +30 -0
  279. package/dist/surfaces/merge.js +105 -0
  280. package/dist/surfaces/ownership.d.ts +190 -0
  281. package/dist/surfaces/ownership.js +795 -0
  282. package/dist/surfaces/projection.d.ts +136 -0
  283. package/dist/surfaces/projection.js +681 -0
  284. package/dist/surfaces/render.d.ts +118 -0
  285. package/dist/surfaces/render.js +633 -0
  286. package/dist/surfaces/restrictions.d.ts +130 -0
  287. package/dist/surfaces/restrictions.js +447 -0
  288. package/dist/surfaces/selftest.d.ts +162 -0
  289. package/dist/surfaces/selftest.js +393 -0
  290. package/dist/surfaces/skill-contract.d.ts +85 -0
  291. package/dist/surfaces/skill-contract.js +449 -0
  292. package/dist/surfaces/sync.d.ts +57 -0
  293. package/dist/surfaces/sync.js +164 -0
  294. package/dist/team-metadata.d.ts +41 -0
  295. package/dist/team-metadata.js +512 -0
  296. package/dist/testing/gates.d.ts +91 -0
  297. package/dist/testing/gates.js +182 -0
  298. package/dist/testing/hooks.d.ts +59 -0
  299. package/dist/testing/hooks.js +139 -0
  300. package/dist/testing/verdict.d.ts +29 -0
  301. package/dist/testing/verdict.js +62 -0
  302. package/dist/work-orders/application.d.ts +99 -0
  303. package/dist/work-orders/application.js +383 -0
  304. package/dist/work-orders/composition.d.ts +124 -0
  305. package/dist/work-orders/composition.js +1010 -0
  306. package/dist/work-orders/contract.d.ts +61 -0
  307. package/dist/work-orders/contract.js +122 -0
  308. package/dist/work-orders/store.d.ts +158 -0
  309. package/dist/work-orders/store.js +197 -0
  310. package/docs/README.md +65 -0
  311. package/docs/architecture.md +136 -0
  312. package/docs/capability-matrix.md +50 -0
  313. package/docs/cli-reference.md +599 -0
  314. package/docs/command-list.md +51 -0
  315. package/docs/compose-patterns.md +99 -0
  316. package/docs/concepts.md +171 -0
  317. package/docs/configuration-essentials.md +172 -0
  318. package/docs/configuration-reference.md +621 -0
  319. package/docs/environment.md +760 -0
  320. package/docs/extending.md +43 -0
  321. package/docs/faq.md +111 -0
  322. package/docs/getting-started.md +608 -0
  323. package/docs/harness-surfaces.md +433 -0
  324. package/docs/joining-a-team.md +76 -0
  325. package/docs/release-policy.md +49 -0
  326. package/docs/safety-and-autonomy.md +419 -0
  327. package/docs/troubleshooting.md +361 -0
  328. package/docs/tutorial.md +170 -0
  329. package/docs/work-and-browser.md +254 -0
  330. package/libexec/browser-operation.cjs +186 -0
  331. package/libexec/fence-marker.cjs +101 -0
  332. package/libexec/fence-marker.d.cts +24 -0
  333. package/libexec/fenced-group-runner.cjs +77 -0
  334. package/libexec/fenced-supervisor.cjs +137 -0
  335. package/libexec/generated-filesystem.cjs +731 -0
  336. package/libexec/install-filesystem.cjs +225 -0
  337. package/libexec/rulesync-generator.NOTICE.md +1112 -0
  338. package/libexec/rulesync-generator.cjs +743 -0
  339. package/libexec/surface-generator-worker.cjs +266 -0
  340. package/libexec/surface-output-reader.cjs +99 -0
  341. package/libexec/surface-scratch-cleanup.cjs +14 -0
  342. package/libexec/validate-harness-evidence.d.mts +183 -0
  343. package/libexec/validate-harness-evidence.mjs +1312 -0
  344. package/package.json +76 -1
  345. package/surfaces/README.md +33 -0
  346. package/surfaces/agents/evaluator.agent.md +76 -0
  347. package/surfaces/guards/bash-guard.sh +48 -0
  348. package/surfaces/guards/claude.adapter.json +25 -0
  349. package/surfaces/guards/codex.adapter.json +24 -0
  350. package/surfaces/guards/opencode.adapter.json +25 -0
  351. package/surfaces/references/browser.md +63 -0
  352. package/surfaces/references/test-data-and-personas.md +84 -0
  353. package/surfaces/references/testing-tiers.md +11 -0
  354. package/surfaces/skills/cockpit-browser.skill.md +54 -0
  355. package/surfaces/skills/cockpit-explain.skill.md +32 -0
  356. package/surfaces/skills/cockpit-fix.skill.md +37 -0
  357. package/surfaces/skills/cockpit-setup.skill.md +39 -0
  358. package/surfaces/skills/cockpit-work.skill.md +34 -0
  359. package/surfaces/skills/collection.json +5 -0
  360. package/surfaces/skills/procedures/explain.md +61 -0
  361. package/surfaces/skills/procedures/fix.md +81 -0
  362. package/surfaces/skills/procedures/setup.md +379 -0
  363. package/surfaces/skills/procedures/work.md +105 -0
@@ -0,0 +1,697 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
3
+ import { readinessReport } from '../readiness.js';
4
+ import { cockpitCommand, initCommand, formatShellCommand } from '../fix-catalogue.js';
5
+ import { isAnswerProvenance, plannedGreenfieldQuestions, } from '../init/draft.js';
6
+ import { SURFACE_TARGETS } from '../surfaces/contract.js';
7
+ import { ENVIRONMENT_CODES, snapshotJson, errorCause } from './contract.js';
8
+ import { bootstrapHolderId } from './identity.js';
9
+ import { authenticateStatePublication, readState, refreshBootstrapObservation, refreshBootstrapPublication, resetBootstrapAnswer, restartBootstrapState, selectBootstrapStateRoot, transactState, } from './state.js';
10
+ import { refusalOutcome as refused } from '../refusal.js';
11
+ const DRAFT_BYTE_LIMIT = 262_144;
12
+ const SHA256 = /^[a-f0-9]{64}$/;
13
+ function sha256(value) {
14
+ return createHash('sha256').update(value).digest('hex');
15
+ }
16
+ // The document owner reports the open question as a blocking gap because a standalone draft with
17
+ // it is invalid. Here the run stops at that question and exits 0, so an error read as a failure.
18
+ function questionNotices(diagnostics) {
19
+ return diagnostics.map(row => row.code === 'DOCUMENT_GAP' && row.severity === 'error' ? { ...row, severity: 'info' } : row);
20
+ }
21
+ function diagnostic(code, message, target) {
22
+ return { severity: 'error', code, message, ...(target === undefined ? {} : { target }) };
23
+ }
24
+ export function unsupportedInitPlatformOutcome(platform) {
25
+ return refused(ENVIRONMENT_CODES.STATE_ROOT_UNAVAILABLE, `init cannot select a state root on ${platform} — bootstrap state ownership is implemented only for macOS and Linux; fix: run ${initCommand()} on a supported macOS or Linux host`);
26
+ }
27
+ function ownerRefusal(outcome, stage, projection, greenfield) {
28
+ return {
29
+ kind: 'refusal', status: 2, artifact: null, destination: null,
30
+ diagnostics: outcome.diagnostics,
31
+ result: result(stage, projection, outcome, greenfield),
32
+ };
33
+ }
34
+ function result(stage, projection, ownerOutcome, greenfield) {
35
+ return { stage, root: projection.root, target: projection.target,
36
+ previewSha256: projection.previewSha256, files: [...projection.files],
37
+ ...(ownerOutcome === undefined ? {} : { ownerOutcome }),
38
+ ...(greenfield?.question === undefined ? {} : { question: greenfield.question }),
39
+ ...(greenfield?.estate === undefined ? {} : { estate: greenfield.estate }),
40
+ ...(greenfield?.interview === undefined ? {} : { interview: greenfield.interview }),
41
+ ...(greenfield?.cockpitRoot === undefined ? {} : { cockpitRoot: greenfield.cockpitRoot }),
42
+ ...(greenfield?.resetAnswer === undefined ? {} : { resetAnswer: greenfield.resetAnswer }),
43
+ ...(greenfield?.unconfirmed === undefined ? {} : { unconfirmed: greenfield.unconfirmed }) };
44
+ }
45
+ // The repository's directory name becomes the first sibling's slug and the cockpit's name stem.
46
+ const SIBLING_DIRECTORY = /^[a-z][a-z0-9-]{0,63}$/;
47
+ function cockpitRootQuestion(root) {
48
+ const dedicated = resolve(dirname(root), `${basename(root)}-cockpit`);
49
+ const unnamed = SIBLING_DIRECTORY.test(basename(root)) ? undefined
50
+ : `the directory name ${JSON.stringify(basename(root))} is not a valid sibling slug (lowercase letters, digits and hyphens, starting with a letter); rename the directory, or create a dedicated cockpit folder yourself and add this repository with cockpit sibling add --from`;
51
+ return {
52
+ field: 'cockpit.root',
53
+ why: `${root} is a product repository, and the cockpit root is where init writes cockpit.toml, AGENTS.md, guards and harness settings, so answer cockpit.root = "dedicated" or "in-repo"`,
54
+ recommended: 'in-repo',
55
+ options: [
56
+ { answer: 'dedicated', root: dedicated, creates: [dedicated], siblingsRoot: '..',
57
+ firstSibling: root,
58
+ tradeOff: `the product repository stays free of cockpit files, and the cockpit grows to more repositories with cockpit sibling add, each created or found beside ${basename(root)}, without moving`,
59
+ ...(unnamed === undefined ? {} : { unavailable: unnamed }) },
60
+ { answer: 'in-repo', root, creates: [], siblingsRoot: '.', firstSibling: root,
61
+ tradeOff: 'the product repository carries the cockpit files and the cockpit cannot neutrally own several repositories; it can be moved out to a dedicated folder later',
62
+ ...(unnamed === undefined ? {} : { unavailable: unnamed }) },
63
+ ],
64
+ };
65
+ }
66
+ /** Where the estate's repositories are observed: the cockpit root, or the product repository a dedicated cockpit sits beside. */
67
+ function estateInput(input) {
68
+ if (input.cockpitRoot?.kind === 'dedicated')
69
+ return { root: dirname(input.cockpitRoot.firstSibling),
70
+ repositories: [basename(input.cockpitRoot.firstSibling)] };
71
+ // Choosing in-repo must not lose the product: its root manifests are observed as the sibling at ".".
72
+ if (input.cockpitRoot?.kind === 'in-repo')
73
+ return { root: input.root, rootIsRepository: true };
74
+ return { root: input.root };
75
+ }
76
+ function documentEstate(input) {
77
+ return input.cockpitRoot === undefined ? {} : { estate: estateInput(input) };
78
+ }
79
+ async function interviewPlan(input, adapters, classification, answered) {
80
+ const planned = plannedGreenfieldQuestions({ root: input.root, classification: classification.classification, gaps: classification.gaps });
81
+ const observedRoot = estateInput(input).root;
82
+ const inference = await adapters.infer?.({ root: observedRoot, configurationRoot: input.root,
83
+ repositories: classification.runnable.map(row => ({ slug: row.slug, path: relative(observedRoot, resolve(observedRoot, row.path)) || '.' })) });
84
+ return {
85
+ answered: planned.slice(0, answered).map(row => row.field),
86
+ outstanding: planned.slice(answered).map(({ field, why }) => ({ field, why,
87
+ inferred: [...field === 'cockpit.name' ? nameProposal(input.root) : [],
88
+ ...(inference?.facts ?? []).filter(fact => fact.fields.includes(field))
89
+ .map(({ target, value, provenance }) => ({ target, value, provenance }))] })),
90
+ inferenceSkipped: inference?.skipped ?? [],
91
+ ...(input.cockpitRoot === undefined ? {} : { cockpitRoot: input.cockpitRoot.kind }),
92
+ };
93
+ }
94
+ // The cockpit root's directory name is the proposed cockpit name when it is a valid one.
95
+ function nameProposal(root) {
96
+ const name = basename(root);
97
+ return /^[a-z0-9][a-z0-9-]*$/.test(name) ? [{ target: 'cockpit.name', value: name,
98
+ provenance: { kind: 'inferred', source: root, reason: 'the cockpit root directory name' } }] : [];
99
+ }
100
+ function commandDigest(input) {
101
+ return sha256(JSON.stringify({ root: input.root, target: input.target, tools: input.tools, registry: input.registry ?? null,
102
+ ...(input.cockpitRoot === undefined ? {} : { cockpitRoot: input.cockpitRoot.kind }),
103
+ ...(input.cockpitRoot?.kind === 'dedicated' ? { firstSibling: input.cockpitRoot.firstSibling } : {}) }));
104
+ }
105
+ function evidenceMatches(record, projection, commandSha256) {
106
+ return record.value.checkoutRoot === projection.root
107
+ && record.value.target === projection.target
108
+ && record.value.commandSha256 === commandSha256
109
+ && record.value.previewSha256 === projection.previewSha256
110
+ && JSON.stringify(record.value.sources) === JSON.stringify(projection.sources);
111
+ }
112
+ function recordedProjection(record) {
113
+ return {
114
+ root: record.value.checkoutRoot,
115
+ target: record.value.target,
116
+ sources: [...record.value.sources],
117
+ files: [...record.value.published],
118
+ previewSha256: record.value.previewSha256,
119
+ };
120
+ }
121
+ async function persist(namespace, address, input, projection, adapters, stage, current, fields) {
122
+ const publication = await transactState(namespace, address, { ownerId: bootstrapHolderId({
123
+ checkoutId: namespace.checkoutId, sessionId: input.sessionId,
124
+ }) }, (observed) => ({
125
+ format: 'cockpit-state/v1', kind: 'bootstrap', checkoutId: namespace.checkoutId, target: input.target,
126
+ revision: observed === null ? 0 : observed.revision + 1,
127
+ updatedAt: adapters.now(),
128
+ value: {
129
+ stage, checkoutRoot: input.root, target: input.target,
130
+ sources: [...projection.sources], commandSha256: commandDigest(input), previewSha256: projection.previewSha256,
131
+ published: [...(fields?.published ?? current?.value.published ?? [])],
132
+ destination: fields?.destination ?? current?.value.destination ?? null,
133
+ artifactBytes: fields?.artifactBytes ?? current?.value.artifactBytes ?? null,
134
+ artifactSha256: fields?.artifactSha256 ?? current?.value.artifactSha256 ?? null,
135
+ ...(fields?.greenfield === undefined && current?.value.greenfield === undefined
136
+ ? {} : { greenfield: fields?.greenfield ?? current.value.greenfield }),
137
+ },
138
+ }));
139
+ return publication;
140
+ }
141
+ function greenfieldResult(classification, sourceObservationSha256, answers) {
142
+ return { ...classification, sourceObservationSha256, answers: [...answers] };
143
+ }
144
+ function stateFailure(outcome) {
145
+ if (outcome.kind === 'refusal')
146
+ return refused(outcome.code, outcome.message, outcome.target);
147
+ return refused('INIT_STATE_DURABILITY_INDETERMINATE', 'bootstrap state durability could not be confirmed');
148
+ }
149
+ export async function runInitApplication(input, adapters) {
150
+ return withReadiness(await runInitStages(input, adapters), input, adapters);
151
+ }
152
+ async function publicationGone(root, record, adapters) {
153
+ for (const path of [...record.value.published.map(row => row.path), 'cockpit.toml']) {
154
+ if (!await pathAbsent(root, path, adapters))
155
+ return false;
156
+ }
157
+ return true;
158
+ }
159
+ // A file is absent when it or any directory above it is; the observation refuses a path whose
160
+ // parent is missing, and anything it cannot prove absent counts as present.
161
+ async function pathAbsent(root, path, adapters) {
162
+ for (let current = path; current !== '.' && current !== '/'; current = dirname(current)) {
163
+ const observed = await adapters.observeDestination({ path: join(root, current), byteLimit: DRAFT_BYTE_LIMIT });
164
+ if (observed.kind !== 'refusal')
165
+ return observed.kind === 'absent';
166
+ }
167
+ return false;
168
+ }
169
+ // Every stop init makes, the preview included, says which prerequisites are still missing, so the
170
+ // setup agent can walk the person through them before the first `up` instead of meeting them one
171
+ // refusal at a time; a cockpit was left uncommitted, outside Git or with no installed cockpit while
172
+ // init reported success.
173
+ async function withReadiness(outcome, input, adapters) {
174
+ const result = outcome.result;
175
+ if (!adapters.inspectReadiness || result === null)
176
+ return outcome;
177
+ const configured = result.stage === 'draft-observed' || result.stage === 'environment-verified';
178
+ const estateRoot = estateInput(input).root;
179
+ const inspected = await adapters.inspectReadiness({
180
+ root: result.root,
181
+ ...(configured ? { configPath: join(result.root, 'cockpit.toml') } : {}),
182
+ repositories: configured ? [] : (result.estate?.runnable ?? [])
183
+ .map(({ slug, path }) => ({ slug, root: resolve(estateRoot, path) })),
184
+ // A dedicated cockpit folder is created as a Git repository by init itself.
185
+ checkRoot: input.cockpitRoot?.kind !== 'dedicated',
186
+ tools: input.tools,
187
+ ...(result.estate === undefined ? {} : { classification: result.estate.classification }),
188
+ });
189
+ const diagnostics = [...outcome.diagnostics, ...inspected.findings];
190
+ return { ...outcome, diagnostics,
191
+ result: { ...result, readiness: readinessReport(inspected.state, diagnostics, input.tools) } };
192
+ }
193
+ async function runInitStages(input, adapters) {
194
+ if (!isAbsolute(input.root) || resolve(input.root) !== input.root) {
195
+ return refused('INIT_ROOT_INVALID', 'init requires one canonical absolute checkout root', input.root);
196
+ }
197
+ if (input.tools[0] !== input.target || new Set(input.tools).size !== input.tools.length) {
198
+ return refused('CLI_USAGE_INVALID', 'init requires distinct tools whose first is the target', '--target');
199
+ }
200
+ if (input.resetAnswer !== undefined && (input.greenfieldAnswer !== undefined || input.accepted)) {
201
+ return refused('INIT_ANSWER_RESET_CONFLICT', 'resetting a recorded answer is a standalone operation');
202
+ }
203
+ if (input.greenfieldAnswer !== undefined && !input.accepted) {
204
+ return refused('GREENFIELD_ANSWER_UNEXPECTED', 'configuration answers require an accepted bootstrap preview');
205
+ }
206
+ const firstSibling = input.cockpitRoot?.kind === 'dedicated' ? input.cockpitRoot.firstSibling : undefined;
207
+ if (firstSibling !== undefined && (input.registry !== undefined || !isAbsolute(firstSibling) || resolve(firstSibling) !== firstSibling
208
+ || dirname(firstSibling) !== dirname(input.root) || firstSibling === input.root)) {
209
+ return refused('INIT_ROOT_INVALID', 'a dedicated cockpit root must sit beside the product repository it is created for, without a registry', input.root);
210
+ }
211
+ for (const target of SURFACE_TARGETS) {
212
+ if (target === input.target)
213
+ continue;
214
+ const other = await selectBootstrapStateRoot({ platform: input.platform, home: input.home,
215
+ xdgStateHome: input.xdgStateHome, checkoutRoot: input.root, target, uid: input.uid });
216
+ if (other.kind !== 'complete')
217
+ return stateFailure(other);
218
+ const priorAddress = other.value.paths.bootstrap(target);
219
+ const prior = await readState(priorAddress);
220
+ if (prior.kind === 'complete') {
221
+ return refused('INIT_TARGET_CHANGED', `bootstrap state is already bound to ${target}`, input.root, `to resume the init started for ${target}, run ${initCommand({ root: input.root, target })} with the options you started it with (a root that was removed and recreated starts again from the preview); to use --target ${input.tools.join(',')} instead, ask the person who owns this machine before you delete ${formatShellCommand([priorAddress.path])}, which holds only that init's progress, then run ${initCommand({ root: input.root, target: input.tools.join(',') })}`);
222
+ }
223
+ if (prior.kind === 'refusal')
224
+ return stateFailure(prior);
225
+ }
226
+ const selected = await selectBootstrapStateRoot({ platform: input.platform, home: input.home,
227
+ xdgStateHome: input.xdgStateHome, checkoutRoot: input.root, target: input.target, uid: input.uid });
228
+ if (selected.kind !== 'complete')
229
+ return stateFailure(selected);
230
+ const namespace = selected.value;
231
+ const address = namespace.paths.bootstrap(input.target);
232
+ const read = await readState(address);
233
+ if (read.kind === 'refusal')
234
+ return stateFailure(read);
235
+ let current = read.kind === 'complete' ? read.value : null;
236
+ // State is keyed by the root's path, so it outlives the directory. Resuming into a recreated one
237
+ // reached its sync, which could not authenticate files that were no longer there.
238
+ let restarted;
239
+ let discarded;
240
+ if (current !== null && current.value.stage !== 'surface-previewed' && await publicationGone(input.root, current, adapters)) {
241
+ // A reset would rewrite answers that the restart is about to discard.
242
+ if (input.resetAnswer !== undefined)
243
+ return refused('INIT_ANSWER_RESET_UNAVAILABLE', `none of the files init published into ${input.root}, nor cockpit.toml, exist any more, so its recorded answers no longer apply`, address.path, `run ${initCommand({ root: input.root, target: input.tools.join(','), args: '--json' })} without --reset-answer: init starts again from the preview`);
244
+ restarted = { severity: 'info', code: 'INIT_STATE_RESTARTED', target: address.path,
245
+ message: `none of the files init published into ${input.root}, nor cockpit.toml, exist any more (the directory was removed or recreated, or they were deleted), so init starts again from the preview; the answers recorded there are discarded once the fresh preview is accepted` };
246
+ discarded = current;
247
+ current = null;
248
+ }
249
+ const outcome = await initStagesFrom(input, adapters, namespace, address, current, discarded);
250
+ return restarted === undefined ? outcome
251
+ : { ...outcome, diagnostics: [restarted, ...outcome.diagnostics] };
252
+ }
253
+ async function initStagesFrom(input, adapters, namespace, address, recorded, discarded) {
254
+ let current = recorded;
255
+ let rootQuestion;
256
+ let boundInput = input;
257
+ if (current !== null && input.cockpitRoot === undefined && input.registry === undefined) {
258
+ // The command driver starts each invocation before it has rediscovered placement. The
259
+ // accepted command digest restores the placement without reclassifying a genesis root; a
260
+ // dedicated root is named after its product repository, so the remedies that rerun init
261
+ // against the cockpit root resume it too.
262
+ const placements = [{ kind: 'in-repo' },
263
+ ...basename(input.root).endsWith('-cockpit') ? [{ kind: 'dedicated',
264
+ firstSibling: resolve(dirname(input.root), basename(input.root).slice(0, -'-cockpit'.length)) }] : []];
265
+ const accepted = current.value.commandSha256;
266
+ boundInput = placements.map(cockpitRoot => ({ ...input, cockpitRoot })).find(candidate => commandDigest(candidate) === accepted) ?? input;
267
+ }
268
+ if (current === null && input.cockpitRoot === undefined && input.registry === undefined && adapters.observeRoot) {
269
+ let observed;
270
+ try {
271
+ observed = await adapters.observeRoot({ root: input.root });
272
+ }
273
+ catch (error) {
274
+ return refused('INIT_ROOT_OBSERVATION_FAILED', `whether ${input.root} is a product repository could not be observed (${errorCause(error)}); fix the named file and rerun ${initCommand({ root: input.root, target: input.tools.join(',') })}`, input.root);
275
+ }
276
+ if (observed === 'product-repository')
277
+ rootQuestion = cockpitRootQuestion(input.root);
278
+ }
279
+ // A single product repository defaults to the cockpit living in it, so --accept-preview and
280
+ // --accept-defaults take the recommended placement; a directory name that cannot be a sibling
281
+ // slug leaves no default and refuses.
282
+ if (rootQuestion && input.accepted) {
283
+ const unavailable = rootQuestion.options.find(option => option.answer === rootQuestion.recommended)?.unavailable;
284
+ if (unavailable !== undefined)
285
+ return refused('INIT_COCKPIT_ROOT_REQUIRED', `${input.root} is a product repository and cannot default to cockpit.root = "in-repo": ${unavailable}`, 'cockpit.root');
286
+ input = { ...input, cockpitRoot: { kind: 'in-repo' } };
287
+ boundInput = input;
288
+ rootQuestion = undefined;
289
+ }
290
+ let prepared;
291
+ let resumed;
292
+ if (current?.value.stage === 'surface-published' || current?.value.stage === 'draft-observed'
293
+ || current?.value.stage === 'environment-verified') {
294
+ resumed = await adapters.resumeBootstrap({ root: input.root, target: input.target, tools: input.tools, address,
295
+ expectedRevision: current.revision, expectedCommandSha256: current.value.commandSha256 });
296
+ if (resumed.kind === 'refusal')
297
+ return ownerRefusal(resumed, current.value.stage, recordedProjection(current));
298
+ }
299
+ if (resumed?.kind !== 'complete' && resumed?.kind !== 'refresh') {
300
+ prepared = await adapters.prepareBootstrap({ root: input.root, target: input.target, tools: input.tools });
301
+ if (prepared.kind !== 'complete') {
302
+ return ownerRefusal(prepared, current?.value.stage ?? 'surface-previewed', current === null ? {
303
+ root: input.root, target: input.target, sources: [], files: [], previewSha256: sha256('unavailable'),
304
+ } : recordedProjection(current));
305
+ }
306
+ }
307
+ let projection;
308
+ if (resumed?.kind === 'complete' || resumed?.kind === 'refresh')
309
+ projection = resumed.result;
310
+ else if (prepared?.kind === 'complete')
311
+ projection = prepared.result;
312
+ else
313
+ return refused('INIT_BOOTSTRAP_IDENTITY_CHANGED', 'selected bootstrap projection is unavailable');
314
+ if (projection.root !== input.root || projection.target !== input.target || !SHA256.test(projection.previewSha256)) {
315
+ return refused('INIT_BOOTSTRAP_IDENTITY_CHANGED', 'selected bootstrap preview does not match the requested root and target');
316
+ }
317
+ const expectedCommand = commandDigest(boundInput);
318
+ const stateMatches = current === null || resumed?.kind === 'refresh'
319
+ ? current === null || current.value.stage === 'surface-published' && current.value.checkoutRoot === projection.root
320
+ && current.value.target === projection.target && current.value.commandSha256 === expectedCommand
321
+ : evidenceMatches(current, projection, expectedCommand);
322
+ if (!stateMatches) {
323
+ return refused('INIT_STATE_STALE', 'bootstrap state does not match the current root, target, command, sources, or preview', address.path);
324
+ }
325
+ if (resumed?.kind === 'refresh' && !input.accepted)
326
+ return { kind: 'pending', status: 0, artifact: null, destination: null,
327
+ diagnostics: [...resumed.diagnostics], result: result('surface-published', projection) };
328
+ let appliedReset;
329
+ if (input.resetAnswer !== undefined) {
330
+ const reset = await resetBootstrapAnswer(namespace, address, {
331
+ ownerId: bootstrapHolderId({ checkoutId: namespace.checkoutId, sessionId: input.sessionId }),
332
+ field: input.resetAnswer.field, updatedAt: adapters.now(),
333
+ ...(input.resetAnswer.acceptedPreviewSha256 === undefined ? {}
334
+ : { acceptedPreviewSha256: input.resetAnswer.acceptedPreviewSha256 }),
335
+ });
336
+ if (reset.kind === 'refusal') {
337
+ const code = reset.code.replace('BOOTSTRAP_', 'INIT_');
338
+ const fix = code === 'INIT_ANSWER_RESET_UNAVAILABLE' ? resetUnavailableFix(input, current) : undefined;
339
+ return refused(code, reset.message, reset.target, fix);
340
+ }
341
+ const publicPreview = { ...reset.preview };
342
+ if (reset.kind === 'pending')
343
+ return { kind: 'pending', status: 0, artifact: null, destination: null,
344
+ diagnostics: [], result: result(current.value.stage, projection, undefined, { resetAnswer: publicPreview }) };
345
+ current = reset.record;
346
+ appliedReset = publicPreview;
347
+ }
348
+ if (rootQuestion)
349
+ return { kind: 'pending', status: 0, artifact: null, destination: null,
350
+ diagnostics: [...(resumed?.diagnostics ?? prepared.diagnostics)],
351
+ result: result(current?.value.stage ?? 'surface-previewed', projection, undefined, { cockpitRoot: rootQuestion }) };
352
+ if (!input.accepted && (current?.value.greenfield === undefined || current.value.stage !== 'surface-published')) {
353
+ const diagnostics = [...(resumed?.diagnostics ?? prepared.diagnostics)];
354
+ let interview;
355
+ let estate;
356
+ const stage = current?.value.stage ?? 'surface-previewed';
357
+ const recorded = current?.value.greenfield?.answers ?? [];
358
+ // Questions are only asked after acceptance, but listing them now lets an unattended caller
359
+ // prepare every answer before it consents. The observation is read-only. Once cockpit.toml is
360
+ // prepared the interview is over, and it still lists the recorded answers: without them a
361
+ // resumed run reported every field of the same answers file as never asked.
362
+ if (boundInput.registry === undefined && adapters.classifyEstate
363
+ && (stage === 'surface-previewed' || stage === 'surface-published' || current?.value.greenfield !== undefined)) {
364
+ const classified = await adapters.classifyEstate(estateInput(boundInput));
365
+ if (classified.kind === 'complete') {
366
+ interview = await interviewPlan(boundInput, adapters, classified.result, recorded.length);
367
+ if (recorded.length > 0)
368
+ estate = greenfieldResult(classified.result, classified.result.sourceObservationSha256, recorded);
369
+ }
370
+ else
371
+ diagnostics.push({ severity: 'warning', code: 'INIT_INTERVIEW_PREVIEW_UNAVAILABLE',
372
+ message: `the interview could not be previewed (${classified.diagnostics.map(row => row.code).join(', ')}); each question is still reported when accepting reaches it`,
373
+ target: input.root });
374
+ }
375
+ return { kind: 'pending', status: 0, artifact: null, destination: null, diagnostics,
376
+ result: result(stage, projection, undefined, interview === undefined ? undefined : { interview, ...(estate === undefined ? {} : { estate }) }) };
377
+ }
378
+ if (input.accepted && (!SHA256.test(input.acceptedPreviewSha256 ?? '') || input.acceptedPreviewSha256 !== projection.previewSha256)) {
379
+ return refused('INIT_PREVIEW_CHANGED', 'selected bootstrap preview changed after acceptance', input.root);
380
+ }
381
+ if (resumed?.kind === 'refresh') {
382
+ const prior = current;
383
+ const applied = await adapters.applyBootstrap(resumed.authority);
384
+ if (applied.kind !== 'complete')
385
+ return ownerRefusal(applied, prior.value.stage, projection);
386
+ if (applied.result.root !== projection.root || applied.result.target !== projection.target
387
+ || applied.result.previewSha256 !== projection.previewSha256
388
+ || JSON.stringify(applied.result.files) !== JSON.stringify(projection.files)) {
389
+ return refused('INIT_BOOTSTRAP_IDENTITY_CHANGED', 'refreshed bootstrap result does not match the accepted preview');
390
+ }
391
+ const refreshed = await refreshBootstrapPublication(namespace, address, { ownerId: bootstrapHolderId({
392
+ checkoutId: namespace.checkoutId, sessionId: input.sessionId,
393
+ }), expectedRevision: prior.revision, expectedSources: prior.value.sources,
394
+ expectedCommandSha256: prior.value.commandSha256, expectedPreviewSha256: prior.value.previewSha256,
395
+ expectedPublished: prior.value.published,
396
+ sources: [...projection.sources], previewSha256: projection.previewSha256, published: [...projection.files],
397
+ updatedAt: adapters.now() });
398
+ if (refreshed.kind === 'refusal' && 'reason' in refreshed)
399
+ return refused('INIT_STATE_STALE', `${refreshed.message}; rerun ${initCommand({ root: input.root, target: input.tools.join(',') })} to review the current preview`, address.path);
400
+ if (refreshed.kind !== 'complete')
401
+ return stateFailure(refreshed);
402
+ current = refreshed.value;
403
+ }
404
+ if (current === null) {
405
+ const previewed = discarded === undefined
406
+ ? await persist(namespace, address, boundInput, projection, adapters, 'surface-previewed', null)
407
+ : await restartBootstrapState(namespace, address, { ownerId: bootstrapHolderId({ checkoutId: namespace.checkoutId,
408
+ sessionId: input.sessionId }), expectedRevision: discarded.revision, updatedAt: adapters.now(),
409
+ value: { commandSha256: commandDigest(boundInput), previewSha256: projection.previewSha256, sources: projection.sources } });
410
+ if (previewed.kind === 'stale')
411
+ return refused('INIT_STATE_STALE', `bootstrap state changed while init was restarting it; rerun ${initCommand({ root: input.root, target: input.tools.join(',') })} to review the current preview`, address.path);
412
+ if (previewed.kind !== 'complete')
413
+ return stateFailure(previewed);
414
+ current = previewed.value;
415
+ }
416
+ if (current.value.stage === 'surface-previewed') {
417
+ if (prepared?.kind !== 'complete') {
418
+ return refused('INIT_BOOTSTRAP_IDENTITY_CHANGED', 'selected bootstrap publication authority is unavailable');
419
+ }
420
+ const applied = await adapters.applyBootstrap(prepared.authority);
421
+ if (applied.kind !== 'complete') {
422
+ return ownerRefusal(applied, current.value.stage, projection);
423
+ }
424
+ if (applied.result.root !== projection.root || applied.result.target !== projection.target
425
+ || applied.result.previewSha256 !== projection.previewSha256
426
+ || JSON.stringify(applied.result.files) !== JSON.stringify(projection.files)) {
427
+ return refused('INIT_BOOTSTRAP_IDENTITY_CHANGED', 'published bootstrap result does not match the accepted preview');
428
+ }
429
+ const published = await persist(namespace, address, boundInput, projection, adapters, 'surface-published', current, { published: projection.files });
430
+ if (published.kind !== 'complete')
431
+ return stateFailure(published);
432
+ current = published.value;
433
+ }
434
+ const destination = resolve(input.root, 'cockpit.toml');
435
+ if (current.value.stage === 'surface-published') {
436
+ let classification;
437
+ let sourceObservationSha256;
438
+ if (boundInput.registry === undefined) {
439
+ if (!adapters.classifyEstate)
440
+ return { kind: 'pending-dependency', status: 2, artifact: null, destination: null,
441
+ diagnostics: [diagnostic('GREENFIELD_DISCOVERY_UNAVAILABLE', 'greenfield init requires the public estate observation owner')],
442
+ result: result('surface-published', projection) };
443
+ const classified = await adapters.classifyEstate(estateInput(boundInput));
444
+ if (classified.kind !== 'complete')
445
+ return ownerRefusal(classified, current.value.stage, projection);
446
+ classification = classified.result;
447
+ sourceObservationSha256 = classified.result.sourceObservationSha256;
448
+ if (!SHA256.test(sourceObservationSha256))
449
+ return refused('GREENFIELD_OBSERVATION_INVALID', 'estate classification omitted its stable source observation digest', input.root);
450
+ }
451
+ else {
452
+ sourceObservationSha256 = sha256(JSON.stringify({ root: boundInput.root, registry: resolve(boundInput.registry),
453
+ commandSha256: expectedCommand }));
454
+ }
455
+ let state = current.value.greenfield;
456
+ const mode = boundInput.registry === undefined ? 'estate' : 'registry';
457
+ let staleSourceObservationSha256;
458
+ if (state && ((state.mode ?? 'estate') !== mode || state.sourceObservationSha256 !== sourceObservationSha256
459
+ || state.classification !== classification?.classification)) {
460
+ if (mode !== 'estate' || (state.mode ?? 'estate') !== 'estate') {
461
+ return refused('GREENFIELD_OBSERVATION_CHANGED', 'configuration source changed after the interview was persisted', address.path);
462
+ }
463
+ if (state.sourceObservationSha256 === sourceObservationSha256) {
464
+ return refused('GREENFIELD_OBSERVATION_CHANGED', `configuration classification changed while its source observation digest stayed the same; rerun ${initCommand({ root: input.root, target: input.tools.join(',') })} to re-observe the current interview, and report the invariant failure if it persists`, address.path);
465
+ }
466
+ if (input.greenfieldAnswer !== undefined) {
467
+ return refused('GREENFIELD_ANSWER_INVALID', `configuration source changed, so the answer for the earlier pending question was not applied; rerun ${initCommand({ root: input.root, target: input.tools.join(',') })} to review the fresh observation before answering again`, address.path);
468
+ }
469
+ staleSourceObservationSha256 = state.sourceObservationSha256;
470
+ state = undefined;
471
+ }
472
+ const priorAnswers = state?.answers ?? [];
473
+ const proposed = input.greenfieldAnswer;
474
+ if (proposed && (!isAnswerProvenance(proposed.provenance) || proposed.bytes.trim() !== proposed.bytes
475
+ || proposed.bytes.length < 1 || proposed.bytes.includes('\0') || Buffer.byteLength(proposed.bytes) > DRAFT_BYTE_LIMIT
476
+ || proposed.questionId !== state?.questions[state.answers.length]?.id)) {
477
+ return refused('GREENFIELD_ANSWER_INVALID', 'configuration answer does not match the next persisted question and provenance', address.path);
478
+ }
479
+ const answers = proposed ? [...priorAnswers, { ...proposed, sha256: sha256(proposed.bytes) }] : [...priorAnswers];
480
+ const documented = await adapters.document({ root: boundInput.root, registry: boundInput.registry, output: destination, ...documentEstate(boundInput),
481
+ configuration: { sourceObservationSha256, ...(classification ? { classification: classification.classification } : {}), answers, tools: input.tools } });
482
+ if (documented.artifact === null) {
483
+ if (proposed && documented.diagnostics.some(row => row.code.startsWith('DOCUMENT_CONFIGURATION_ANSWER')
484
+ || row.code === 'DOCUMENT_DEFAULT_CONFIRMATION_INVALID')) {
485
+ // The owner's diagnostics name the field and the schema reason; dropping them left the operator with no way to correct the answer.
486
+ return { kind: 'refusal', status: 2, artifact: null, destination: null, result: null, diagnostics: [
487
+ diagnostic('GREENFIELD_ANSWER_INVALID', 'configuration owner rejected the proposed answer', address.path),
488
+ ...documented.diagnostics
489
+ ] };
490
+ }
491
+ return ownerRefusal(documented, current.value.stage, projection);
492
+ }
493
+ const draftRemedy = `shorten the answers; for a saved answer, from the cockpit root, preview its reset with ${initCommand({ root: input.root, target: input.tools.join(','), args: '--reset-answer <field> --json' })}, then repeat with --accept-reset <previewSha256> before answering again`;
494
+ if (Buffer.byteLength(documented.artifact, 'utf8') > DRAFT_BYTE_LIMIT) {
495
+ return refused('INIT_DRAFT_INVALID', `cockpit.toml exceeds the draft byte limit and cannot be offered for confirmation; ${draftRemedy}`, destination);
496
+ }
497
+ const question = documented.question;
498
+ if (question) {
499
+ const surfacedQuestion = question.field === 'configuration.defaults'
500
+ ? { ...question, document: documented.artifact.split('\n') } : question;
501
+ const expected = state?.questions[state.answers.length];
502
+ if (!proposed && expected && JSON.stringify(expected) !== JSON.stringify(question)) {
503
+ return refused('GREENFIELD_OBSERVATION_CHANGED', 'configuration question changed after it was persisted', address.path);
504
+ }
505
+ const interview = classification ? await interviewPlan(boundInput, adapters, classification, answers.length) : undefined;
506
+ const estate = classification ? greenfieldResult(classification, sourceObservationSha256, answers) : undefined;
507
+ const pendingResult = result('surface-published', projection, undefined, { ...(staleSourceObservationSha256 !== undefined && !input.accepted ? {} : { question: surfacedQuestion }),
508
+ ...(estate ? { estate } : {}), ...(interview ? { interview } : {}),
509
+ ...(!input.accepted && appliedReset ? { resetAnswer: appliedReset } : {}) });
510
+ if (snapshotJson(pendingResult) === undefined) {
511
+ return refused('INIT_DRAFT_INVALID', `configuration review cannot be shown exactly within the bounded human output; ${draftRemedy}`, destination);
512
+ }
513
+ if (!input.accepted)
514
+ return { kind: 'pending', status: 0, artifact: null, destination: null,
515
+ diagnostics: questionNotices(documented.diagnostics), result: pendingResult };
516
+ const questions = [...(state?.questions ?? []),
517
+ ...(state === undefined || proposed && !state.questions.some(row => row.id === question.id) ? [question] : [])];
518
+ const nextState = { ...(mode === 'registry' ? { mode } : {}), stage: 'question-pending', sourceObservationSha256,
519
+ ...(classification ? { classification: classification.classification, notice: classification.notice } : {}), questions, answers };
520
+ const recorded = staleSourceObservationSha256 === undefined
521
+ ? await persist(namespace, address, boundInput, projection, adapters, 'surface-published', current, { published: current.value.published, greenfield: nextState })
522
+ : await refreshBootstrapObservation(namespace, address, {
523
+ ownerId: bootstrapHolderId({ checkoutId: namespace.checkoutId, sessionId: input.sessionId }),
524
+ expectedRevision: current.revision,
525
+ expectedSourceObservationSha256: staleSourceObservationSha256,
526
+ greenfield: nextState, updatedAt: adapters.now(),
527
+ });
528
+ if (recorded.kind === 'refusal' && 'reason' in recorded) {
529
+ return refused('GREENFIELD_OBSERVATION_CHANGED', `${recorded.message}; rerun ${initCommand({ root: input.root, target: input.tools.join(',') })} to review the fresh observation before answering again`, address.path);
530
+ }
531
+ if (recorded.kind !== 'complete')
532
+ return stateFailure(recorded);
533
+ return { kind: 'pending', status: 0, artifact: null, destination: null,
534
+ diagnostics: questionNotices(documented.diagnostics), result: pendingResult };
535
+ }
536
+ if (documented.kind !== 'complete')
537
+ return ownerRefusal(documented, current.value.stage, projection);
538
+ if (documented.destination !== destination) {
539
+ return refused('INIT_DRAFT_INVALID', 'document owner returned an unbound cockpit.toml artifact', destination);
540
+ }
541
+ const resolvedState = { ...(mode === 'registry' ? { mode } : {}), stage: answers.length === 0 ? 'resolved' : 'answers-recorded',
542
+ sourceObservationSha256, ...(classification ? { classification: classification.classification,
543
+ notice: classification.notice } : {}),
544
+ questions: [...(state?.questions ?? [])], answers };
545
+ if (!input.accepted && staleSourceObservationSha256 !== undefined) {
546
+ const interview = classification ? await interviewPlan(boundInput, adapters, classification, 0) : undefined;
547
+ const estate = classification ? greenfieldResult(classification, sourceObservationSha256, []) : undefined;
548
+ return { kind: 'pending', status: 0, artifact: null, destination: null, diagnostics: documented.diagnostics,
549
+ result: result('surface-published', projection, undefined, { ...(estate ? { estate } : {}), ...(interview ? { interview } : {}) }) };
550
+ }
551
+ if (staleSourceObservationSha256 !== undefined) {
552
+ const refreshed = await refreshBootstrapObservation(namespace, address, {
553
+ ownerId: bootstrapHolderId({ checkoutId: namespace.checkoutId, sessionId: input.sessionId }),
554
+ expectedRevision: current.revision,
555
+ expectedSourceObservationSha256: staleSourceObservationSha256,
556
+ greenfield: resolvedState, updatedAt: adapters.now(),
557
+ });
558
+ if (refreshed.kind === 'refusal' && 'reason' in refreshed) {
559
+ return refused('GREENFIELD_OBSERVATION_CHANGED', `${refreshed.message}; rerun ${initCommand({ root: input.root, target: input.tools.join(',') })} to review the fresh observation before answering again`, address.path);
560
+ }
561
+ if (refreshed.kind !== 'complete')
562
+ return stateFailure(refreshed);
563
+ current = refreshed.value;
564
+ }
565
+ const artifactSha256 = sha256(documented.artifact);
566
+ const persisted = await persist(namespace, address, boundInput, projection, adapters, 'draft-prepared', current, {
567
+ published: current.value.published, destination: 'cockpit.toml', artifactBytes: documented.artifact, artifactSha256,
568
+ greenfield: resolvedState,
569
+ });
570
+ if (persisted.kind !== 'complete')
571
+ return stateFailure(persisted);
572
+ current = persisted.value;
573
+ return { kind: 'artifact', status: 0, artifact: documented.artifact, destination,
574
+ diagnostics: [...documented.diagnostics, ...await adapters.composeFindings?.({ root: dirname(destination), draft: documented.artifact }) ?? []],
575
+ result: result('draft-prepared', projection, undefined, unconfirmedOf(documented)) };
576
+ }
577
+ const persistedConfiguration = current.value.greenfield;
578
+ if (!persistedConfiguration)
579
+ return refused('INIT_DRAFT_SOURCE_CHANGED', 'persisted cockpit.toml has no source-bound configuration answers', destination);
580
+ const currentDocument = await adapters.document({ root: boundInput.root, registry: boundInput.registry, output: destination, ...documentEstate(boundInput),
581
+ configuration: { sourceObservationSha256: persistedConfiguration.sourceObservationSha256,
582
+ ...(persistedConfiguration.classification ? { classification: persistedConfiguration.classification } : {}),
583
+ answers: persistedConfiguration.answers, tools: input.tools } });
584
+ if (currentDocument.kind !== 'complete') {
585
+ // The answers are bound to the estate init observed; a product edit after the draft breaks
586
+ // that binding, and replaying the same answers cannot rebind it, so a bare rerun refuses again.
587
+ const observed = current.value.stage !== 'draft-prepared';
588
+ const fix = `restore the product files changed since init drafted cockpit.toml, then rerun ${initCommand({ root: input.root, target: input.tools.join(','),
589
+ args: `--accept-preview ${projection.previewSha256}` })}${observed
590
+ ? `; to keep the change instead, finish without init: run ${cockpitCommand('sync', { config: destination })}, then ${cockpitCommand('check', { config: destination })}` : ''}`;
591
+ return ownerRefusal({ ...currentDocument, diagnostics: currentDocument.diagnostics.map(row => row.code === 'DOCUMENT_ESTATE_OBSERVATION_CHANGED' ? { ...row, fix,
592
+ message: 'the product files init observed (package manifests, Dockerfile, Compose files) changed after it drafted cockpit.toml, so the recorded answers no longer describe this repository and init will not verify the draft against it' }
593
+ : row) }, current.value.stage, projection);
594
+ }
595
+ if (currentDocument.destination !== destination || currentDocument.artifact !== current.value.artifactBytes
596
+ || sha256(currentDocument.artifact) !== current.value.artifactSha256) {
597
+ return refused('INIT_DRAFT_SOURCE_CHANGED', 'current configuration sources no longer reproduce the persisted cockpit.toml', destination);
598
+ }
599
+ let draftPublication;
600
+ if (current.value.stage === 'draft-prepared') {
601
+ const observed = await adapters.observeDestination({ path: destination, byteLimit: DRAFT_BYTE_LIMIT });
602
+ if (observed.kind === 'refusal')
603
+ return { kind: 'refusal', status: 2, artifact: null, destination: null,
604
+ diagnostics: observed.diagnostics, result: result(current.value.stage, projection) };
605
+ if (observed.kind === 'absent') {
606
+ return { kind: 'artifact', status: 0, artifact: current.value.artifactBytes, destination,
607
+ diagnostics: [...await adapters.composeFindings?.({ root: dirname(destination), draft: current.value.artifactBytes }) ?? []], result: result('draft-prepared', projection, undefined, unconfirmedOf(currentDocument)) };
608
+ }
609
+ if (observed.sha256 !== current.value.artifactSha256) {
610
+ return refused('INIT_DRAFT_CHANGED', 'existing cockpit.toml bytes differ from the persisted draft', destination);
611
+ }
612
+ const observedState = await persist(namespace, address, boundInput, projection, adapters, 'draft-observed', current);
613
+ if (observedState.kind !== 'complete')
614
+ return stateFailure(observedState);
615
+ current = observedState.value;
616
+ draftPublication = observedState;
617
+ }
618
+ if (resumed === undefined) {
619
+ resumed = await adapters.resumeBootstrap({ root: input.root, target: input.target, tools: input.tools, address });
620
+ if (resumed.kind === 'refusal')
621
+ return ownerRefusal(resumed, current.value.stage, projection);
622
+ if (resumed.kind === 'complete') {
623
+ projection = resumed.result;
624
+ if (!evidenceMatches(current, projection, expectedCommand)
625
+ || projection.previewSha256 !== input.acceptedPreviewSha256) {
626
+ return refused('INIT_STATE_STALE', 'canonical bootstrap ownership does not match the accepted durable state', address.path);
627
+ }
628
+ }
629
+ }
630
+ if (current.value.stage === 'environment-verified' && resumed.kind !== 'complete') {
631
+ return refused('INIT_CANONICAL_OWNERSHIP_MISSING', 'verified bootstrap state has no matching canonical ownership', address.path);
632
+ }
633
+ if (resumed.kind === 'pending' && draftPublication === undefined) {
634
+ const refreshed = await persist(namespace, address, boundInput, projection, adapters, 'draft-observed', current);
635
+ if (refreshed.kind !== 'complete')
636
+ return stateFailure(refreshed);
637
+ current = refreshed.value;
638
+ draftPublication = refreshed;
639
+ }
640
+ if (draftPublication !== undefined) {
641
+ const authenticated = authenticateStatePublication({ publication: draftPublication, address });
642
+ if (!authenticated)
643
+ return refused('INIT_STATE_UNAUTHENTICATED', 'bootstrap state publication could not be authenticated', address.path);
644
+ }
645
+ // From here cockpit.toml is on disk and the recorded answers can no longer be reset, so every stop
646
+ // says how to finish: an agent that met one tried --reset-answer, then ran sync by hand.
647
+ const verificationStop = (outcome) => {
648
+ const stopped = ownerRefusal(outcome, 'draft-observed', projection, unconfirmedOf(currentDocument));
649
+ return { ...stopped, diagnostics: [...stopped.diagnostics, { severity: 'info', code: 'INIT_VERIFICATION_INCOMPLETE', target: destination,
650
+ message: 'cockpit.toml is written, so its recorded answers can no longer be reset; init stopped while verifying it',
651
+ fix: `correct what the errors above name, changing a cockpit.toml value in cockpit.toml itself, then rerun ${initCommand({ root: input.root,
652
+ target: input.tools.join(','), args: `--accept-preview ${projection.previewSha256}` })} to finish verifying it` }] };
653
+ };
654
+ const validation = await adapters.validateConfig(destination);
655
+ if (validation.status !== 0)
656
+ return verificationStop(validation);
657
+ const synchronized = await adapters.synchronize(resumed.kind === 'complete'
658
+ ? { configPath: destination }
659
+ : { configPath: destination, bootstrapState: { publication: draftPublication, address } });
660
+ if (synchronized.status !== 0)
661
+ return verificationStop(synchronized);
662
+ const checked = await adapters.check({ configPath: destination });
663
+ if (checked.status !== 0)
664
+ return verificationStop(checked);
665
+ const preflight = await adapters.preflight({ configPath: destination });
666
+ // A surfaces-only cockpit has no runtime to preflight. Its refusal is the verified state, and is
667
+ // reported as information so the operator still reads that no runtime is declared.
668
+ const runtimeAbsent = preflight.status !== 0 && preflight.diagnostics.length > 0
669
+ && preflight.diagnostics.every(row => row.code === 'ENVIRONMENT_NOT_DECLARED');
670
+ if (preflight.status !== 0 && !runtimeAbsent)
671
+ return verificationStop(preflight);
672
+ const verified = await persist(namespace, address, boundInput, projection, adapters, 'environment-verified', current);
673
+ if (verified.kind !== 'complete')
674
+ return stateFailure(verified);
675
+ return { kind: 'complete', status: 0, artifact: null, destination: null,
676
+ diagnostics: runtimeAbsent ? preflight.diagnostics.map(row => ({ ...row, severity: 'info' })) : preflight.diagnostics,
677
+ result: result('environment-verified', projection, undefined, unconfirmedOf(currentDocument)) };
678
+ }
679
+ // A reset rewrites the interview, which only exists until cockpit.toml is written from it.
680
+ function resetUnavailableFix(input, current) {
681
+ const resume = (args) => initCommand({ root: input.root, target: input.tools.join(','), args });
682
+ switch (current?.value.stage) {
683
+ case 'draft-prepared':
684
+ return `cockpit.toml is already drafted from the recorded answers: rerun ${resume(`--accept-preview ${current.value.previewSha256}`)} to write it, then change the value in cockpit.toml itself and rerun the same command`;
685
+ case 'draft-observed':
686
+ return `cockpit.toml is already written from the recorded answers: change the value in cockpit.toml itself, then rerun ${resume(`--accept-preview ${current.value.previewSha256}`)} to finish verifying it`;
687
+ case 'environment-verified':
688
+ return `init has finished: change the value in cockpit.toml itself, then run ${cockpitCommand('sync', { config: join(input.root, 'cockpit.toml') })}`;
689
+ default:
690
+ return `no answer is recorded yet: run ${resume('--json')} to preview, then answer the interview`;
691
+ }
692
+ }
693
+ /** The review items a finished interview leaves: answers the setup agent chose or no one attributed. */
694
+ function unconfirmedOf(document) {
695
+ const rows = document.kind === 'complete' ? document.result.unconfirmed : undefined;
696
+ return rows === undefined || rows.length === 0 ? {} : { unconfirmed: rows };
697
+ }