@latysh-development/monolith 0.0.0-stage → 0.1.0-alpha.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 (345) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +179 -2
  3. package/dist/brief/cli.d.ts +30 -0
  4. package/dist/brief/cli.d.ts.map +1 -0
  5. package/dist/brief/cli.js +85 -0
  6. package/dist/brief/collect.d.ts +17 -0
  7. package/dist/brief/collect.d.ts.map +1 -0
  8. package/dist/brief/collect.js +342 -0
  9. package/dist/brief/config.d.ts +40 -0
  10. package/dist/brief/config.d.ts.map +1 -0
  11. package/dist/brief/config.js +85 -0
  12. package/dist/brief/constants.d.ts +12 -0
  13. package/dist/brief/constants.d.ts.map +1 -0
  14. package/dist/brief/constants.js +11 -0
  15. package/dist/brief/docs.d.ts +53 -0
  16. package/dist/brief/docs.d.ts.map +1 -0
  17. package/dist/brief/docs.js +229 -0
  18. package/dist/brief/freshness.d.ts +104 -0
  19. package/dist/brief/freshness.d.ts.map +1 -0
  20. package/dist/brief/freshness.js +263 -0
  21. package/dist/brief/git-signals.d.ts +17 -0
  22. package/dist/brief/git-signals.d.ts.map +1 -0
  23. package/dist/brief/git-signals.js +101 -0
  24. package/dist/brief/index.d.ts +11 -0
  25. package/dist/brief/index.d.ts.map +1 -0
  26. package/dist/brief/index.js +5 -0
  27. package/dist/brief/reconcile.d.ts +75 -0
  28. package/dist/brief/reconcile.d.ts.map +1 -0
  29. package/dist/brief/reconcile.js +259 -0
  30. package/dist/brief/render.d.ts +67 -0
  31. package/dist/brief/render.d.ts.map +1 -0
  32. package/dist/brief/render.js +458 -0
  33. package/dist/brief/sanitize.d.ts +38 -0
  34. package/dist/brief/sanitize.d.ts.map +1 -0
  35. package/dist/brief/sanitize.js +48 -0
  36. package/dist/brief/selectors.d.ts +36 -0
  37. package/dist/brief/selectors.d.ts.map +1 -0
  38. package/dist/brief/selectors.js +75 -0
  39. package/dist/brief/story.d.ts +18 -0
  40. package/dist/brief/story.d.ts.map +1 -0
  41. package/dist/brief/story.js +59 -0
  42. package/dist/brief/types.d.ts +126 -0
  43. package/dist/brief/types.d.ts.map +1 -0
  44. package/dist/brief/types.js +11 -0
  45. package/dist/cli/index.d.ts +3 -0
  46. package/dist/cli/index.d.ts.map +1 -0
  47. package/dist/cli/index.js +199 -0
  48. package/dist/cli/init-args.d.ts +16 -0
  49. package/dist/cli/init-args.d.ts.map +1 -0
  50. package/dist/cli/init-args.js +24 -0
  51. package/dist/cli/ui-args.d.ts +23 -0
  52. package/dist/cli/ui-args.d.ts.map +1 -0
  53. package/dist/cli/ui-args.js +66 -0
  54. package/dist/cli/ui.d.ts +40 -0
  55. package/dist/cli/ui.d.ts.map +1 -0
  56. package/dist/cli/ui.js +180 -0
  57. package/dist/coordinatorium/checkpoint.d.ts +44 -0
  58. package/dist/coordinatorium/checkpoint.d.ts.map +1 -0
  59. package/dist/coordinatorium/checkpoint.js +124 -0
  60. package/dist/coordinatorium/cli.d.ts +39 -0
  61. package/dist/coordinatorium/cli.d.ts.map +1 -0
  62. package/dist/coordinatorium/cli.js +203 -0
  63. package/dist/coordinatorium/errors.d.ts +112 -0
  64. package/dist/coordinatorium/errors.d.ts.map +1 -0
  65. package/dist/coordinatorium/errors.js +145 -0
  66. package/dist/coordinatorium/index.d.ts +11 -0
  67. package/dist/coordinatorium/index.d.ts.map +1 -0
  68. package/dist/coordinatorium/index.js +5 -0
  69. package/dist/coordinatorium/lock.d.ts +187 -0
  70. package/dist/coordinatorium/lock.d.ts.map +1 -0
  71. package/dist/coordinatorium/lock.js +659 -0
  72. package/dist/coordinatorium/mission.d.ts +32 -0
  73. package/dist/coordinatorium/mission.d.ts.map +1 -0
  74. package/dist/coordinatorium/mission.js +87 -0
  75. package/dist/coordinatorium/state-keeper.d.ts +115 -0
  76. package/dist/coordinatorium/state-keeper.d.ts.map +1 -0
  77. package/dist/coordinatorium/state-keeper.js +396 -0
  78. package/dist/coordinatorium/types.d.ts +69 -0
  79. package/dist/coordinatorium/types.d.ts.map +1 -0
  80. package/dist/coordinatorium/types.js +27 -0
  81. package/dist/discovery/compose.d.ts +28 -0
  82. package/dist/discovery/compose.d.ts.map +1 -0
  83. package/dist/discovery/compose.js +159 -0
  84. package/dist/discovery/contracts.d.ts +211 -0
  85. package/dist/discovery/contracts.d.ts.map +1 -0
  86. package/dist/discovery/contracts.js +555 -0
  87. package/dist/discovery/docs.d.ts +278 -0
  88. package/dist/discovery/docs.d.ts.map +1 -0
  89. package/dist/discovery/docs.js +1182 -0
  90. package/dist/discovery/facts.d.ts +49 -0
  91. package/dist/discovery/facts.d.ts.map +1 -0
  92. package/dist/discovery/facts.js +541 -0
  93. package/dist/discovery/index.d.ts +8 -0
  94. package/dist/discovery/index.d.ts.map +1 -0
  95. package/dist/discovery/index.js +9 -0
  96. package/dist/discovery/orchestrate.d.ts +35 -0
  97. package/dist/discovery/orchestrate.d.ts.map +1 -0
  98. package/dist/discovery/orchestrate.js +32 -0
  99. package/dist/discovery/render.d.ts +36 -0
  100. package/dist/discovery/render.d.ts.map +1 -0
  101. package/dist/discovery/render.js +278 -0
  102. package/dist/discovery/traverse.d.ts +197 -0
  103. package/dist/discovery/traverse.d.ts.map +1 -0
  104. package/dist/discovery/traverse.js +510 -0
  105. package/dist/durable/assess.d.ts +105 -0
  106. package/dist/durable/assess.d.ts.map +1 -0
  107. package/dist/durable/assess.js +239 -0
  108. package/dist/durable/canonical.d.ts +25 -0
  109. package/dist/durable/canonical.d.ts.map +1 -0
  110. package/dist/durable/canonical.js +78 -0
  111. package/dist/durable/cli.d.ts +37 -0
  112. package/dist/durable/cli.d.ts.map +1 -0
  113. package/dist/durable/cli.js +141 -0
  114. package/dist/durable/errors.d.ts +192 -0
  115. package/dist/durable/errors.d.ts.map +1 -0
  116. package/dist/durable/errors.js +323 -0
  117. package/dist/durable/fs-store.d.ts +254 -0
  118. package/dist/durable/fs-store.d.ts.map +1 -0
  119. package/dist/durable/fs-store.js +1493 -0
  120. package/dist/durable/index.d.ts +20 -0
  121. package/dist/durable/index.d.ts.map +1 -0
  122. package/dist/durable/index.js +13 -0
  123. package/dist/durable/log.d.ts +33 -0
  124. package/dist/durable/log.d.ts.map +1 -0
  125. package/dist/durable/log.js +157 -0
  126. package/dist/durable/memory-store.d.ts +51 -0
  127. package/dist/durable/memory-store.d.ts.map +1 -0
  128. package/dist/durable/memory-store.js +302 -0
  129. package/dist/durable/projection.d.ts +35 -0
  130. package/dist/durable/projection.d.ts.map +1 -0
  131. package/dist/durable/projection.js +328 -0
  132. package/dist/durable/prune-check.d.ts +34 -0
  133. package/dist/durable/prune-check.d.ts.map +1 -0
  134. package/dist/durable/prune-check.js +72 -0
  135. package/dist/durable/register.d.ts +100 -0
  136. package/dist/durable/register.d.ts.map +1 -0
  137. package/dist/durable/register.js +532 -0
  138. package/dist/durable/retention.d.ts +49 -0
  139. package/dist/durable/retention.d.ts.map +1 -0
  140. package/dist/durable/retention.js +59 -0
  141. package/dist/durable/runtime.d.ts +499 -0
  142. package/dist/durable/runtime.d.ts.map +1 -0
  143. package/dist/durable/runtime.js +1288 -0
  144. package/dist/durable/seams.d.ts +44 -0
  145. package/dist/durable/seams.d.ts.map +1 -0
  146. package/dist/durable/seams.js +21 -0
  147. package/dist/durable/testing.d.ts +11 -0
  148. package/dist/durable/testing.d.ts.map +1 -0
  149. package/dist/durable/testing.js +25 -0
  150. package/dist/durable/types.d.ts +442 -0
  151. package/dist/durable/types.d.ts.map +1 -0
  152. package/dist/durable/types.js +75 -0
  153. package/dist/graph/builder.d.ts +11 -0
  154. package/dist/graph/builder.d.ts.map +1 -0
  155. package/dist/graph/builder.js +153 -0
  156. package/dist/graph/identity.d.ts +37 -0
  157. package/dist/graph/identity.d.ts.map +1 -0
  158. package/dist/graph/identity.js +46 -0
  159. package/dist/graph/index.d.ts +7 -0
  160. package/dist/graph/index.d.ts.map +1 -0
  161. package/dist/graph/index.js +3 -0
  162. package/dist/graph/resolver.d.ts +41 -0
  163. package/dist/graph/resolver.d.ts.map +1 -0
  164. package/dist/graph/resolver.js +36 -0
  165. package/dist/graph/types.d.ts +42 -0
  166. package/dist/graph/types.d.ts.map +1 -0
  167. package/dist/graph/types.js +1 -0
  168. package/dist/http/index.d.ts +5 -0
  169. package/dist/http/index.d.ts.map +1 -0
  170. package/dist/http/index.js +2 -0
  171. package/dist/http/server.d.ts +76 -0
  172. package/dist/http/server.d.ts.map +1 -0
  173. package/dist/http/server.js +195 -0
  174. package/dist/http/ui-assets.d.ts +22 -0
  175. package/dist/http/ui-assets.d.ts.map +1 -0
  176. package/dist/http/ui-assets.js +100 -0
  177. package/dist/ideas/cli.d.ts +33 -0
  178. package/dist/ideas/cli.d.ts.map +1 -0
  179. package/dist/ideas/cli.js +158 -0
  180. package/dist/ideas/format.d.ts +38 -0
  181. package/dist/ideas/format.d.ts.map +1 -0
  182. package/dist/ideas/format.js +194 -0
  183. package/dist/ideas/index.d.ts +6 -0
  184. package/dist/ideas/index.d.ts.map +1 -0
  185. package/dist/ideas/index.js +3 -0
  186. package/dist/ideas/store.d.ts +55 -0
  187. package/dist/ideas/store.d.ts.map +1 -0
  188. package/dist/ideas/store.js +262 -0
  189. package/dist/ideas/types.d.ts +54 -0
  190. package/dist/ideas/types.d.ts.map +1 -0
  191. package/dist/ideas/types.js +26 -0
  192. package/dist/index.d.ts +32 -0
  193. package/dist/index.d.ts.map +1 -0
  194. package/dist/index.js +28 -0
  195. package/dist/init/index.d.ts +12 -0
  196. package/dist/init/index.d.ts.map +1 -0
  197. package/dist/init/index.js +90 -0
  198. package/dist/init/prompts.d.ts +3 -0
  199. package/dist/init/prompts.d.ts.map +1 -0
  200. package/dist/init/prompts.js +47 -0
  201. package/dist/init/scaffold.d.ts +29 -0
  202. package/dist/init/scaffold.d.ts.map +1 -0
  203. package/dist/init/scaffold.js +208 -0
  204. package/dist/init/types.d.ts +14 -0
  205. package/dist/init/types.d.ts.map +1 -0
  206. package/dist/init/types.js +1 -0
  207. package/dist/managed/index.d.ts +73 -0
  208. package/dist/managed/index.d.ts.map +1 -0
  209. package/dist/managed/index.js +222 -0
  210. package/dist/readmodel/dashboard.d.ts +40 -0
  211. package/dist/readmodel/dashboard.d.ts.map +1 -0
  212. package/dist/readmodel/dashboard.js +236 -0
  213. package/dist/readmodel/index.d.ts +3 -0
  214. package/dist/readmodel/index.d.ts.map +1 -0
  215. package/dist/readmodel/index.js +1 -0
  216. package/dist/readmodel/types.d.ts +104 -0
  217. package/dist/readmodel/types.d.ts.map +1 -0
  218. package/dist/readmodel/types.js +15 -0
  219. package/dist/root/index.d.ts +186 -0
  220. package/dist/root/index.d.ts.map +1 -0
  221. package/dist/root/index.js +430 -0
  222. package/dist/root/safe-write.d.ts +268 -0
  223. package/dist/root/safe-write.d.ts.map +1 -0
  224. package/dist/root/safe-write.js +966 -0
  225. package/dist/root/write-artifacts.d.ts +20 -0
  226. package/dist/root/write-artifacts.d.ts.map +1 -0
  227. package/dist/root/write-artifacts.js +42 -0
  228. package/dist/scriptorium/compile.d.ts +4 -0
  229. package/dist/scriptorium/compile.d.ts.map +1 -0
  230. package/dist/scriptorium/compile.js +185 -0
  231. package/dist/scriptorium/frontmatter.d.ts +13 -0
  232. package/dist/scriptorium/frontmatter.d.ts.map +1 -0
  233. package/dist/scriptorium/frontmatter.js +90 -0
  234. package/dist/scriptorium/index.d.ts +3 -0
  235. package/dist/scriptorium/index.d.ts.map +1 -0
  236. package/dist/scriptorium/index.js +1 -0
  237. package/dist/scriptorium/markdown.d.ts +31 -0
  238. package/dist/scriptorium/markdown.d.ts.map +1 -0
  239. package/dist/scriptorium/markdown.js +88 -0
  240. package/dist/scriptorium/types.d.ts +78 -0
  241. package/dist/scriptorium/types.d.ts.map +1 -0
  242. package/dist/scriptorium/types.js +1 -0
  243. package/dist/types/enforcement.d.ts +11 -0
  244. package/dist/types/enforcement.d.ts.map +1 -0
  245. package/dist/types/enforcement.js +1 -0
  246. package/dist/types/integrity.d.ts +33 -0
  247. package/dist/types/integrity.d.ts.map +1 -0
  248. package/dist/types/integrity.js +1 -0
  249. package/dist/types/project-definition.d.ts +30 -0
  250. package/dist/types/project-definition.d.ts.map +1 -0
  251. package/dist/types/project-definition.js +1 -0
  252. package/dist/ui/assets/cosmic-glass-bg-Dwx47zPu.webp +0 -0
  253. package/dist/ui/assets/index-C4NhMaq7.js +28 -0
  254. package/dist/ui/assets/index-DMZ7ZvZY.css +1 -0
  255. package/dist/ui/index.html +13 -0
  256. package/dist/update/cli.d.ts +8 -0
  257. package/dist/update/cli.d.ts.map +1 -0
  258. package/dist/update/cli.js +115 -0
  259. package/dist/update/index.d.ts +78 -0
  260. package/dist/update/index.d.ts.map +1 -0
  261. package/dist/update/index.js +285 -0
  262. package/dist/validate/index.d.ts +6 -0
  263. package/dist/validate/index.d.ts.map +1 -0
  264. package/dist/validate/index.js +214 -0
  265. package/dist/version.d.ts +2 -0
  266. package/dist/version.d.ts.map +1 -0
  267. package/dist/version.js +1 -0
  268. package/package.json +67 -4
  269. package/templates/.gitkeep +0 -0
  270. package/templates/default-instance/instruction/.cursorrules +59 -0
  271. package/templates/default-instance/instruction/.windsurfrules +59 -0
  272. package/templates/default-instance/instruction/CLAUDE.md +57 -0
  273. package/templates/default-instance/managed/agent.md +20 -0
  274. package/templates/default-instance/managed/bootstrap.md +7 -0
  275. package/templates/default-instance/managed/index-idea.md +5 -0
  276. package/templates/default-instance/managed/routing-idea.md +14 -0
  277. package/templates/default-instance/monolith/AGENT.md +1 -0
  278. package/templates/default-instance/monolith/context/boot/HANDOFF.md +49 -0
  279. package/templates/default-instance/monolith/context/boot/WORK_SESSION.md +30 -0
  280. package/templates/default-instance/monolith/context/execution/current-state.md +30 -0
  281. package/templates/default-instance/monolith/context/maps/dependency-map.md +38 -0
  282. package/templates/default-instance/monolith/context/maps/project-map.md +85 -0
  283. package/templates/default-instance/monolith/context/maps/roadmap-index.md +41 -0
  284. package/templates/default-instance/monolith/context/maps/routing-map.md +151 -0
  285. package/templates/default-instance/monolith/context/maps/semantic-map.md +75 -0
  286. package/templates/default-instance/monolith/context/models/model-registry.md +37 -0
  287. package/templates/default-instance/monolith/context/models/privacy-classification.md +35 -0
  288. package/templates/default-instance/monolith/context/templates/plan-template.md +56 -0
  289. package/templates/default-instance/monolith/governance/README.md +29 -0
  290. package/templates/default-instance/monolith/governance/RULE_SCHEMA.md +58 -0
  291. package/templates/default-instance/monolith/learning/INDEX.md +48 -0
  292. package/templates/default-instance/monolith/learning/injection-map.md +49 -0
  293. package/templates/default-instance/monolith/learning/taxonomy.md +30 -0
  294. package/templates/default-instance/monolith/memory/errors/README.md +26 -0
  295. package/templates/default-instance/monolith/memory/errors/SCHEMA.md +47 -0
  296. package/templates/default-instance/monolith/memory/errors/TAXONOMY.md +27 -0
  297. package/templates/default-instance/monolith/memory/long-term/architecture-principles.md +31 -0
  298. package/templates/default-instance/monolith/memory/long-term/decisions.md +36 -0
  299. package/templates/default-instance/monolith/memory/long-term/execution-history.md +20 -0
  300. package/templates/default-instance/monolith/memory/long-term/tech-debt.md +25 -0
  301. package/templates/default-instance/monolith/memory/working/active.md +41 -0
  302. package/templates/default-instance/monolith/memory/working/governance-result.md +0 -0
  303. package/templates/default-instance/monolith/memory/working/integrity-result.md +0 -0
  304. package/templates/default-instance/monolith/memory/working/story-index.md +28 -0
  305. package/templates/default-instance/monolith/protocols/boot/boot-sequence.md +59 -0
  306. package/templates/default-instance/monolith/protocols/cognition/cognitive-modes.md +90 -0
  307. package/templates/default-instance/monolith/protocols/cognition/dry-run.md +56 -0
  308. package/templates/default-instance/monolith/protocols/cognition/execution-trace-format.md +52 -0
  309. package/templates/default-instance/monolith/protocols/cognition/semantic-planning.md +51 -0
  310. package/templates/default-instance/monolith/protocols/execution/hybrid-execution.md +37 -0
  311. package/templates/default-instance/monolith/protocols/execution/local-llm-governance.md +42 -0
  312. package/templates/default-instance/monolith/protocols/governance/command-policy.md +51 -0
  313. package/templates/default-instance/monolith/protocols/governance/core-invariants.md +40 -0
  314. package/templates/default-instance/monolith/protocols/governance/memory-governance.md +54 -0
  315. package/templates/default-instance/monolith/protocols/governance/metadata-schema.md +62 -0
  316. package/templates/default-instance/monolith/protocols/governance/skills.md +53 -0
  317. package/templates/default-instance/monolith/protocols/governance/work-session.md +46 -0
  318. package/templates/default-instance/monolith/skills/INDEX.md +32 -0
  319. package/templates/default-instance/monolith/skills/current-status/SKILL.md +47 -0
  320. package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +86 -0
  321. package/templates/default-instance/monolith/skills/global-status/SKILL.md +56 -0
  322. package/templates/default-instance/monolith/skills/idea-capture/SKILL.md +157 -0
  323. package/templates/default-instance/monolith/skills/self-improvement/SKILL.md +77 -0
  324. package/templates/default-instance/monolith/state.json +11 -0
  325. package/templates/default-instance/monolith/tests/INDEX.md +57 -0
  326. package/templates/default-instance/monolith/tests/scenarios/architecture-review.md +32 -0
  327. package/templates/default-instance/monolith/tests/scenarios/debug-failure.md +32 -0
  328. package/templates/default-instance/monolith/tests/scenarios/dry-run-activation.md +33 -0
  329. package/templates/default-instance/monolith/tests/scenarios/execution-trace-verification.md +31 -0
  330. package/templates/default-instance/monolith/tests/scenarios/finalize-story.md +35 -0
  331. package/templates/default-instance/monolith/tests/scenarios/implement-feature.md +36 -0
  332. package/templates/default-instance/monolith/tests/scenarios/local-llm-delegation.md +31 -0
  333. package/templates/default-instance/monolith/tests/scenarios/planning-request.md +32 -0
  334. package/templates/default-instance/monolith/tests/scenarios/sensitive-context-task.md +30 -0
  335. package/templates/default-instance/monolith/tests/scenarios/validator-verification.md +33 -0
  336. package/templates/default-instance/monolith/tests/validation/expected-behavior.md +70 -0
  337. package/templates/default-instance/monolith/tests/validators/INDEX.md +17 -0
  338. package/templates/default-instance/monolith/tests/validators/delegation-semantics.md +22 -0
  339. package/templates/default-instance/monolith/tests/validators/mode-consistency.md +25 -0
  340. package/templates/default-instance/monolith/tests/validators/privacy-routing.md +23 -0
  341. package/templates/default-instance/monolith/tests/validators/protocol-references.md +27 -0
  342. package/templates/default-instance/monolith/tests/validators/routing-integrity.md +30 -0
  343. package/templates/default-instance/projectroot/AGENTS.md +111 -0
  344. package/templates/default-instance/projectroot/monolith.config.json +32 -0
  345. package/templates/default-instance/projectroot/project.definition.mjs.template +41 -0
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Tactical Coordinatorium — mission lifecycle.
3
+ *
4
+ * A mission is a unit of development work tracked across AI sessions.
5
+ * This module owns mission semantics (ID generation, valid transitions);
6
+ * state-keeper.ts owns persistence and never knows what a mission is.
7
+ */
8
+ import { type StateReadTarget, type StateWriteTarget } from './state-keeper.js';
9
+ import type { Mission, MissionResult } from './types.js';
10
+ export interface StartMissionInput {
11
+ title: string;
12
+ system: string;
13
+ }
14
+ /**
15
+ * Register a new mission as in_progress. ID generation happens inside the
16
+ * same locked critical section as the write, so concurrent `start` calls
17
+ * from independent processes can never collide on an ID.
18
+ */
19
+ export declare function startMission(target: StateWriteTarget, input: StartMissionInput): Promise<Mission>;
20
+ export declare function listMissions(target: StateReadTarget): Promise<Mission[]>;
21
+ export declare function getMission(target: StateReadTarget, id: string): Promise<Mission>;
22
+ export interface FinishMissionInput {
23
+ result: MissionResult;
24
+ summary?: string | null;
25
+ }
26
+ /**
27
+ * Transition a mission from in_progress to a terminal state. Any other
28
+ * starting status (already completed/failed/cancelled) is rejected — once
29
+ * terminal, a mission's lifecycle is closed.
30
+ */
31
+ export declare function finishMission(target: StateWriteTarget, id: string, input: FinishMissionInput): Promise<Mission>;
32
+ //# sourceMappingURL=mission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mission.d.ts","sourceRoot":"","sources":["../../src/coordinatorium/mission.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAA0B,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAExG,OAAO,KAAK,EAAuB,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAc9E,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAUD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,gBAAgB,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,CAqBvG;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAG9E;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAKtF;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,gBAAgB,EACxB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,OAAO,CAAC,CAsBlB"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Tactical Coordinatorium — mission lifecycle.
3
+ *
4
+ * A mission is a unit of development work tracked across AI sessions.
5
+ * This module owns mission semantics (ID generation, valid transitions);
6
+ * state-keeper.ts owns persistence and never knows what a mission is.
7
+ */
8
+ import { mutateState, readState } from './state-keeper.js';
9
+ import { MissionNotFoundError, InvalidTransitionError, ValidationError } from './errors.js';
10
+ const MISSION_ID_RE = /^MIS-(\d+)$/;
11
+ const MISSION_RESULTS = ['completed', 'failed', 'cancelled'];
12
+ function nextMissionId(missions) {
13
+ let max = 0;
14
+ for (const id of Object.keys(missions)) {
15
+ const match = MISSION_ID_RE.exec(id);
16
+ if (match)
17
+ max = Math.max(max, Number(match[1]));
18
+ }
19
+ return `MIS-${String(max + 1).padStart(4, '0')}`;
20
+ }
21
+ function requireNonEmptyString(value, field) {
22
+ const trimmed = value.trim();
23
+ if (trimmed.length === 0) {
24
+ throw new ValidationError(`"${field}" must be a non-empty string`);
25
+ }
26
+ return trimmed;
27
+ }
28
+ /**
29
+ * Register a new mission as in_progress. ID generation happens inside the
30
+ * same locked critical section as the write, so concurrent `start` calls
31
+ * from independent processes can never collide on an ID.
32
+ */
33
+ export async function startMission(target, input) {
34
+ const title = requireNonEmptyString(input.title, 'title');
35
+ const system = requireNonEmptyString(input.system, 'system');
36
+ const { result } = await mutateState(target, (draft) => {
37
+ const id = nextMissionId(draft.tactical.missions);
38
+ const now = new Date().toISOString();
39
+ const mission = {
40
+ id,
41
+ title,
42
+ system,
43
+ status: 'in_progress',
44
+ startedAt: now,
45
+ updatedAt: now,
46
+ summary: null,
47
+ };
48
+ draft.tactical.missions[id] = mission;
49
+ return mission;
50
+ });
51
+ return result;
52
+ }
53
+ export async function listMissions(target) {
54
+ const state = await readState(target);
55
+ return Object.values(state.tactical.missions).sort((a, b) => a.startedAt.localeCompare(b.startedAt));
56
+ }
57
+ export async function getMission(target, id) {
58
+ const state = await readState(target);
59
+ const mission = state.tactical.missions[id];
60
+ if (!mission)
61
+ throw new MissionNotFoundError(id);
62
+ return mission;
63
+ }
64
+ /**
65
+ * Transition a mission from in_progress to a terminal state. Any other
66
+ * starting status (already completed/failed/cancelled) is rejected — once
67
+ * terminal, a mission's lifecycle is closed.
68
+ */
69
+ export async function finishMission(target, id, input) {
70
+ if (!MISSION_RESULTS.includes(input.result)) {
71
+ throw new ValidationError(`"result" must be one of ${MISSION_RESULTS.join(', ')}, got "${String(input.result)}"`);
72
+ }
73
+ const { result } = await mutateState(target, (draft) => {
74
+ const mission = draft.tactical.missions[id];
75
+ if (!mission)
76
+ throw new MissionNotFoundError(id);
77
+ if (mission.status !== 'in_progress') {
78
+ throw new InvalidTransitionError(`Mission "${id}" is already "${mission.status}" and cannot transition to "${input.result}"`);
79
+ }
80
+ mission.status = input.result;
81
+ mission.updatedAt = new Date().toISOString();
82
+ if (input.summary !== undefined)
83
+ mission.summary = input.summary;
84
+ return mission;
85
+ });
86
+ return result;
87
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * State Keeper — the single point of read/write access to state.json.
3
+ *
4
+ * Guarantees:
5
+ * - Atomic persistence: writes land via write-temp-then-rename, so a reader
6
+ * never observes a partially written file.
7
+ * - No lost updates: every mutation runs inside a cross-process lock (see
8
+ * lock.ts), read-modify-write, with a monotonic revision bump.
9
+ * - Never silently overwrites corrupted or unsupported state: malformed
10
+ * JSON or a schema mismatch throws before anything is written.
11
+ *
12
+ * AI reports state; this module validates and persists it. It has no
13
+ * knowledge of what a "mission" is — that belongs to mission.ts.
14
+ */
15
+ import { type LockOptions } from './lock.js';
16
+ import { type WriteWarning } from '../root/index.js';
17
+ import { type CoordinatoriumState } from './types.js';
18
+ export declare const STATE_FILE_NAME = "state.json";
19
+ /**
20
+ * Protective ceiling for `state.json` (SEC-001B.1) — NOT a retention policy.
21
+ * Append-only checkpoints grow the file without bound; see STATE-001.
22
+ * Enforced on read (bounded reader) and before an atomic write.
23
+ */
24
+ export declare const MAX_STATE_BYTES: number;
25
+ export declare function stateFilePath(stateDir: string): string;
26
+ /**
27
+ * Where a write may land (SEC-001B.2, ADR-012). There is no default and no
28
+ * plain-string form: the caller states which trust model applies.
29
+ *
30
+ * - `project`: `projectRoot` is the boundary. `stateDir` must lie inside it and
31
+ * no component below the root may be a symlink (even one that stays inside).
32
+ * - `trusted-directory`: the caller asserts that `stateDir` AND ALL ITS
33
+ * ANCESTORS are trusted. Nothing about the ancestors is checked (so a
34
+ * symlinked `.monolith` is invisible in this mode); only what lies below
35
+ * `stateDir` is. A missing `stateDir` is created.
36
+ *
37
+ * `onWarning` receives non-fatal findings reported after a successful publish
38
+ * (durability downgrade, platform note, ...). It is called once per warning,
39
+ * after the state lock has been released; an exception it throws is ignored and
40
+ * never affects the result. Without it they are only returned by `mutateState`.
41
+ */
42
+ export type StateWriteTarget = {
43
+ scope: 'project';
44
+ projectRoot: string;
45
+ stateDir: string;
46
+ onWarning?: (warning: WriteWarning) => void;
47
+ } | {
48
+ scope: 'trusted-directory';
49
+ stateDir: string;
50
+ onWarning?: (warning: WriteWarning) => void;
51
+ };
52
+ /** Project-scoped write target (the CLI always uses this). */
53
+ export declare function projectTarget(projectRoot: string, stateDir: string, onWarning?: (warning: WriteWarning) => void): StateWriteTarget;
54
+ /** Trusted-directory write target: `stateDir` and its ancestors are asserted trusted by the caller. */
55
+ export declare function trustedDirectoryTarget(stateDir: string, onWarning?: (warning: WriteWarning) => void): StateWriteTarget;
56
+ /**
57
+ * Where a read may look (SEC-001B.1-R). Same shape and same scopes as a write
58
+ * target, minus `onWarning`; there is no default and no plain-string form. A
59
+ * `StateWriteTarget` is accepted as is.
60
+ */
61
+ export type StateReadTarget = {
62
+ scope: 'project';
63
+ projectRoot: string;
64
+ stateDir: string;
65
+ } | {
66
+ scope: 'trusted-directory';
67
+ stateDir: string;
68
+ };
69
+ /**
70
+ * Validate the shape of parsed JSON and return a typed, defensively-copied
71
+ * CoordinatoriumState. Throws StateSchemaError for anything that does not
72
+ * match a supported schema version — this function never "fixes" or
73
+ * partially accepts malformed state.
74
+ *
75
+ * Supports exactly two on-disk shapes:
76
+ * - `schemaVersion: 2` (current) — `tactical.checkpoints` is required.
77
+ * - `schemaVersion: 1` (legacy, pre-COORDINATORIUM-002) — migrated
78
+ * in-memory to v2 on read (see `migrateV1ToV2`). Anything else
79
+ * (including a *newer* version this binary doesn't know about) is
80
+ * rejected outright — this is the deliberate fix for the compatibility
81
+ * gap an independent review found in the first COORDINATORIUM-002 cut:
82
+ * that cut kept `schemaVersion` at 1 while adding `tactical.checkpoints`,
83
+ * so an *older* binary (hardcoded to `schemaVersion === 1`, with no
84
+ * notion of checkpoints) could read a checkpoint-bearing v1-tagged file,
85
+ * reconstruct state from only the fields it knew about (`tactical: {
86
+ * missions }`, dropping checkpoints entirely — this function has never
87
+ * been a passthrough for unknown fields), and silently overwrite the
88
+ * file on its next mutateState call, destroying checkpoint history with
89
+ * no error raised. Bumping to 2 converts that silent data loss into a
90
+ * loud, immediate `StateSchemaError` for any reader that doesn't
91
+ * understand v2 — the version check below runs before anything is ever
92
+ * read into a mutable draft, so mutateState never reaches its write step
93
+ * (same proven pattern as the existing corrupted-JSON guard).
94
+ */
95
+ export declare function validateState(raw: unknown): CoordinatoriumState;
96
+ /**
97
+ * Read current state without taking a lock. If the file does not exist,
98
+ * returns a fresh initial state WITHOUT persisting it — read paths never
99
+ * have the side effect of creating state.json. Corrupted JSON or an
100
+ * unsupported schema throws rather than being silently replaced.
101
+ */
102
+ export declare function readState(target: StateReadTarget): Promise<CoordinatoriumState>;
103
+ /**
104
+ * Read-modify-write under an exclusive cross-process lock. `mutator`
105
+ * receives a deep-cloned draft of the current state and an `applyResult`
106
+ * callback to hand back an arbitrary value (e.g. the mission it just
107
+ * created) alongside the write. The revision is incremented exactly once
108
+ * per successful call, after the mutator returns.
109
+ */
110
+ export declare function mutateState<T>(target: StateWriteTarget, mutator: (draft: CoordinatoriumState) => T, lockOptions?: LockOptions): Promise<{
111
+ state: CoordinatoriumState;
112
+ result: T;
113
+ warnings: WriteWarning[];
114
+ }>;
115
+ //# sourceMappingURL=state-keeper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-keeper.d.ts","sourceRoot":"","sources":["../../src/coordinatorium/state-keeper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAE1D,OAAO,EASL,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAIL,KAAK,mBAAmB,EAIzB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,eAAe,eAAe,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAAmB,CAAC;AAEhD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEtD;AAMD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAA;CAAE,GACxG;IAAE,KAAK,EAAE,mBAAmB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAA;CAAE,CAAC;AAElG,8DAA8D;AAC9D,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,GAC1C,gBAAgB,CAElB;AAED,uGAAuG;AACvG,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,GAAG,gBAAgB,CAEtH;AAwCD;;;;GAIG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC3D;IAAE,KAAK,EAAE,mBAAmB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AA8FrD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,mBAAmB,CAmE/D;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAqCrF;AA0ED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,CAAC,EACjC,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,CAAC,EAC1C,WAAW,CAAC,EAAE,WAAW,GACxB,OAAO,CAAC;IAAE,KAAK,EAAE,mBAAmB,CAAC;IAAC,MAAM,EAAE,CAAC,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,CAAC,CA0B9E"}
@@ -0,0 +1,396 @@
1
+ /**
2
+ * State Keeper — the single point of read/write access to state.json.
3
+ *
4
+ * Guarantees:
5
+ * - Atomic persistence: writes land via write-temp-then-rename, so a reader
6
+ * never observes a partially written file.
7
+ * - No lost updates: every mutation runs inside a cross-process lock (see
8
+ * lock.ts), read-modify-write, with a monotonic revision bump.
9
+ * - Never silently overwrites corrupted or unsupported state: malformed
10
+ * JSON or a schema mismatch throws before anything is written.
11
+ *
12
+ * AI reports state; this module validates and persists it. It has no
13
+ * knowledge of what a "mission" is — that belongs to mission.ts.
14
+ */
15
+ import { lstatSync, mkdirSync } from 'fs';
16
+ import { isAbsolute, join } from 'path';
17
+ import { acquireLock } from './lock.js';
18
+ import { StateCorruptedError, StateSchemaError, StateFileError } from './errors.js';
19
+ import { SafeWriteError, openParent, projectBoundary, readBoundedFileStable, replaceFile, trustedDirectoryBoundary, } from '../root/index.js';
20
+ import { STATE_SCHEMA_VERSION, LEGACY_SCHEMA_VERSION_V1, createInitialState, } from './types.js';
21
+ export const STATE_FILE_NAME = 'state.json';
22
+ /**
23
+ * Protective ceiling for `state.json` (SEC-001B.1) — NOT a retention policy.
24
+ * Append-only checkpoints grow the file without bound; see STATE-001.
25
+ * Enforced on read (bounded reader) and before an atomic write.
26
+ */
27
+ export const MAX_STATE_BYTES = 16 * 1024 * 1024;
28
+ export function stateFilePath(stateDir) {
29
+ return join(stateDir, STATE_FILE_NAME);
30
+ }
31
+ function lockFilePath(stateDir) {
32
+ return `${stateFilePath(stateDir)}.lock`;
33
+ }
34
+ /** Project-scoped write target (the CLI always uses this). */
35
+ export function projectTarget(projectRoot, stateDir, onWarning) {
36
+ return { scope: 'project', projectRoot, stateDir, onWarning };
37
+ }
38
+ /** Trusted-directory write target: `stateDir` and its ancestors are asserted trusted by the caller. */
39
+ export function trustedDirectoryTarget(stateDir, onWarning) {
40
+ return { scope: 'trusted-directory', stateDir, onWarning };
41
+ }
42
+ function scopeRequired(fn, stateDir) {
43
+ return new SafeWriteError(`${fn} requires an explicit write scope. Replace ${fn}(dir, ...) with ` +
44
+ `${fn}({ scope: 'trusted-directory', stateDir: dir }, ...) when dir and its ancestors are trusted, or with ` +
45
+ `${fn}({ scope: 'project', projectRoot, stateDir }, ...) when acting on a project (ADR-012).`, {
46
+ code: 'WRITE_SCOPE_REQUIRED',
47
+ phase: 'scope',
48
+ publication: 'NOT_PUBLISHED',
49
+ path: typeof stateDir === 'string' ? stateDir : '',
50
+ });
51
+ }
52
+ /** Validates the scope and builds the boundary. Throws SafeWriteError; touches nothing in `project` scope. */
53
+ function resolveWriteTarget(target, fn) {
54
+ if (typeof target !== 'object' || target === null)
55
+ throw scopeRequired(fn, target);
56
+ const t = target;
57
+ if (typeof t.stateDir !== 'string' || t.stateDir.length === 0)
58
+ throw scopeRequired(fn, t.stateDir);
59
+ if (t.scope === 'project') {
60
+ if (typeof t.projectRoot !== 'string' || t.projectRoot.length === 0)
61
+ throw scopeRequired(fn, t.stateDir);
62
+ return { boundary: projectBoundary(t.projectRoot), stateDir: t.stateDir, onWarning: t.onWarning };
63
+ }
64
+ if (t.scope === 'trusted-directory') {
65
+ // The caller vouches for the directory and its ancestors, so creating it
66
+ // (and any missing ancestors) is within the model they chose.
67
+ mkdirSync(t.stateDir, { recursive: true });
68
+ return { boundary: trustedDirectoryBoundary(t.stateDir), stateDir: t.stateDir, onWarning: t.onWarning };
69
+ }
70
+ throw scopeRequired(fn, t.stateDir);
71
+ }
72
+ /** Validates the read scope; returns the boundary directory. Throws StateFileError, touches nothing. */
73
+ function resolveReadTarget(target, fn) {
74
+ const bad = (why) => new StateFileError('READ_BOUNDARY_INVALID', `${fn} requires an explicit read scope (${why}). Use { scope: 'project', projectRoot, stateDir } for a project, ` +
75
+ `or { scope: 'trusted-directory', stateDir } when stateDir is trusted (SEC-001B.1-R).`);
76
+ if (typeof target !== 'object' || target === null)
77
+ throw bad('got a non-object');
78
+ const t = target;
79
+ // Every path is validated here, before any filesystem call: a path that cannot be
80
+ // trusted must never be probed (and so never "not found" -> empty state) (L-2).
81
+ // The cwd is never a boundary, so a relative path is refused, not resolved.
82
+ if (typeof t.stateDir !== 'string' || t.stateDir.length === 0)
83
+ throw bad('stateDir is missing');
84
+ if (!isAbsolute(t.stateDir))
85
+ throw bad('stateDir must be an absolute path');
86
+ if (t.scope === 'project') {
87
+ if (typeof t.projectRoot !== 'string' || t.projectRoot.length === 0)
88
+ throw bad('projectRoot is missing');
89
+ if (!isAbsolute(t.projectRoot))
90
+ throw bad('projectRoot must be an absolute path');
91
+ return { boundary: t.projectRoot, stateDir: t.stateDir };
92
+ }
93
+ if (t.scope === 'trusted-directory')
94
+ return { boundary: t.stateDir, stateDir: t.stateDir };
95
+ throw bad('unknown scope');
96
+ }
97
+ const MISSION_STATUSES = ['in_progress', 'completed', 'failed', 'cancelled'];
98
+ function isPlainObject(v) {
99
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
100
+ }
101
+ function validateMission(id, raw) {
102
+ if (!isPlainObject(raw)) {
103
+ throw new StateSchemaError(`Mission "${id}" is not an object`);
104
+ }
105
+ const { title, system, status, startedAt, updatedAt, summary } = raw;
106
+ if (raw.id !== id) {
107
+ throw new StateSchemaError(`Mission "${id}" has mismatched id field "${String(raw.id)}"`);
108
+ }
109
+ if (typeof title !== 'string' || title.length === 0) {
110
+ throw new StateSchemaError(`Mission "${id}" has an invalid title`);
111
+ }
112
+ if (typeof system !== 'string' || system.length === 0) {
113
+ throw new StateSchemaError(`Mission "${id}" has an invalid system`);
114
+ }
115
+ if (typeof status !== 'string' || !MISSION_STATUSES.includes(status)) {
116
+ throw new StateSchemaError(`Mission "${id}" has an invalid status "${String(status)}"`);
117
+ }
118
+ if (typeof startedAt !== 'string') {
119
+ throw new StateSchemaError(`Mission "${id}" has an invalid startedAt`);
120
+ }
121
+ if (typeof updatedAt !== 'string') {
122
+ throw new StateSchemaError(`Mission "${id}" has an invalid updatedAt`);
123
+ }
124
+ if (summary !== null && typeof summary !== 'string') {
125
+ throw new StateSchemaError(`Mission "${id}" has an invalid summary`);
126
+ }
127
+ return { id, title, system, status: status, startedAt, updatedAt, summary };
128
+ }
129
+ function validateCheckpoint(id, raw) {
130
+ if (!isPlainObject(raw)) {
131
+ throw new StateSchemaError(`Checkpoint "${id}" is not an object`);
132
+ }
133
+ const { missionId, createdAt, summary, remainingSteps, context } = raw;
134
+ if (raw.id !== id) {
135
+ throw new StateSchemaError(`Checkpoint "${id}" has mismatched id field "${String(raw.id)}"`);
136
+ }
137
+ if (typeof missionId !== 'string' || missionId.length === 0) {
138
+ throw new StateSchemaError(`Checkpoint "${id}" has an invalid missionId`);
139
+ }
140
+ if (typeof createdAt !== 'string') {
141
+ throw new StateSchemaError(`Checkpoint "${id}" has an invalid createdAt`);
142
+ }
143
+ if (typeof summary !== 'string' || summary.length === 0) {
144
+ throw new StateSchemaError(`Checkpoint "${id}" has an invalid summary`);
145
+ }
146
+ if (!Array.isArray(remainingSteps) || !remainingSteps.every((s) => typeof s === 'string' && s.length > 0)) {
147
+ throw new StateSchemaError(`Checkpoint "${id}" has an invalid remainingSteps`);
148
+ }
149
+ if (context !== null && typeof context !== 'string') {
150
+ throw new StateSchemaError(`Checkpoint "${id}" has an invalid context`);
151
+ }
152
+ return {
153
+ id,
154
+ missionId,
155
+ createdAt,
156
+ summary,
157
+ remainingSteps: [...remainingSteps],
158
+ context: context ?? null,
159
+ };
160
+ }
161
+ /**
162
+ * Validate the shape of parsed JSON and return a typed, defensively-copied
163
+ * CoordinatoriumState. Throws StateSchemaError for anything that does not
164
+ * match a supported schema version — this function never "fixes" or
165
+ * partially accepts malformed state.
166
+ *
167
+ * Supports exactly two on-disk shapes:
168
+ * - `schemaVersion: 2` (current) — `tactical.checkpoints` is required.
169
+ * - `schemaVersion: 1` (legacy, pre-COORDINATORIUM-002) — migrated
170
+ * in-memory to v2 on read (see `migrateV1ToV2`). Anything else
171
+ * (including a *newer* version this binary doesn't know about) is
172
+ * rejected outright — this is the deliberate fix for the compatibility
173
+ * gap an independent review found in the first COORDINATORIUM-002 cut:
174
+ * that cut kept `schemaVersion` at 1 while adding `tactical.checkpoints`,
175
+ * so an *older* binary (hardcoded to `schemaVersion === 1`, with no
176
+ * notion of checkpoints) could read a checkpoint-bearing v1-tagged file,
177
+ * reconstruct state from only the fields it knew about (`tactical: {
178
+ * missions }`, dropping checkpoints entirely — this function has never
179
+ * been a passthrough for unknown fields), and silently overwrite the
180
+ * file on its next mutateState call, destroying checkpoint history with
181
+ * no error raised. Bumping to 2 converts that silent data loss into a
182
+ * loud, immediate `StateSchemaError` for any reader that doesn't
183
+ * understand v2 — the version check below runs before anything is ever
184
+ * read into a mutable draft, so mutateState never reaches its write step
185
+ * (same proven pattern as the existing corrupted-JSON guard).
186
+ */
187
+ export function validateState(raw) {
188
+ if (!isPlainObject(raw)) {
189
+ throw new StateSchemaError('State root is not an object');
190
+ }
191
+ if (raw.schemaVersion !== STATE_SCHEMA_VERSION && raw.schemaVersion !== LEGACY_SCHEMA_VERSION_V1) {
192
+ throw new StateSchemaError(`Unsupported schemaVersion "${String(raw.schemaVersion)}" ` +
193
+ `(expected ${STATE_SCHEMA_VERSION}, or legacy ${LEGACY_SCHEMA_VERSION_V1} for migration)`);
194
+ }
195
+ if (typeof raw.revision !== 'number' || !Number.isInteger(raw.revision) || raw.revision < 0) {
196
+ throw new StateSchemaError(`Invalid revision "${String(raw.revision)}"`);
197
+ }
198
+ if (!isPlainObject(raw.strategic) || !isPlainObject(raw.strategic.systems)) {
199
+ throw new StateSchemaError('Invalid strategic.systems');
200
+ }
201
+ if (!isPlainObject(raw.tactical) || !isPlainObject(raw.tactical.missions)) {
202
+ throw new StateSchemaError('Invalid tactical.missions');
203
+ }
204
+ const missions = {};
205
+ for (const [id, m] of Object.entries(raw.tactical.missions)) {
206
+ missions[id] = validateMission(id, m);
207
+ }
208
+ const rawCheckpoints = raw.tactical.checkpoints;
209
+ const checkpoints = {};
210
+ if (raw.schemaVersion === LEGACY_SCHEMA_VERSION_V1) {
211
+ // v1→v2 migration path: a genuine pre-COORDINATORIUM-002 file has no
212
+ // `checkpoints` key at all — default to {}. Tolerate (and validate) one
213
+ // being present anyway, defensively, in case a file was ever written by
214
+ // the since-corrected transitional code that kept schemaVersion at 1
215
+ // while already writing checkpoints — nothing is lost either way.
216
+ if (rawCheckpoints !== undefined && !isPlainObject(rawCheckpoints)) {
217
+ throw new StateSchemaError('Invalid tactical.checkpoints');
218
+ }
219
+ if (rawCheckpoints !== undefined) {
220
+ for (const [id, c] of Object.entries(rawCheckpoints)) {
221
+ checkpoints[id] = validateCheckpoint(id, c);
222
+ }
223
+ }
224
+ }
225
+ else {
226
+ // schemaVersion === STATE_SCHEMA_VERSION (2): every v2 writer always
227
+ // serializes tactical.checkpoints (even as {}), so its absence here is
228
+ // itself a sign of a malformed/hand-edited file, not a legacy case —
229
+ // required, not defaulted.
230
+ if (!isPlainObject(rawCheckpoints)) {
231
+ throw new StateSchemaError('Invalid tactical.checkpoints');
232
+ }
233
+ for (const [id, c] of Object.entries(rawCheckpoints)) {
234
+ checkpoints[id] = validateCheckpoint(id, c);
235
+ }
236
+ }
237
+ // Migration is applied purely in-memory here — revision and every
238
+ // mission/checkpoint field are carried over exactly, only schemaVersion
239
+ // is relabeled. No write happens during a read (same "reads are never a
240
+ // side effect" rule as the missing-file case in readState below); the
241
+ // migrated v2 shape is only ever persisted to disk the next time
242
+ // mutateState's existing atomic read-modify-write cycle runs — no
243
+ // separate migration-write path, no new on-disk mechanism.
244
+ return {
245
+ schemaVersion: STATE_SCHEMA_VERSION,
246
+ revision: raw.revision,
247
+ strategic: { systems: { ...raw.strategic.systems } },
248
+ tactical: { missions, checkpoints },
249
+ };
250
+ }
251
+ /**
252
+ * Read current state without taking a lock. If the file does not exist,
253
+ * returns a fresh initial state WITHOUT persisting it — read paths never
254
+ * have the side effect of creating state.json. Corrupted JSON or an
255
+ * unsupported schema throws rather than being silently replaced.
256
+ */
257
+ export async function readState(target) {
258
+ const { boundary, stateDir } = resolveReadTarget(target, 'readState');
259
+ const filePath = stateFilePath(stateDir);
260
+ // A trusted directory that does not exist holds no state (it is the boundary,
261
+ // so it cannot be walked to). In `project` scope a missing directory is
262
+ // reported by the reader itself as FILE_MISSING. Only ENOENT on the path itself
263
+ // counts as "does not exist": `existsSync` would also say so for a dangling
264
+ // symlink or a loop, and an unusable boundary is not an empty state — those fall
265
+ // through to the reader, which refuses them (READ_BOUNDARY_INVALID) (L-2).
266
+ if (target.scope === 'trusted-directory') {
267
+ try {
268
+ lstatSync(stateDir);
269
+ }
270
+ catch (err) {
271
+ if (err.code === 'ENOENT')
272
+ return createInitialState();
273
+ }
274
+ }
275
+ // Unified safe read (SEC-001B.1-R): explicit boundary, no symlink or hardlink,
276
+ // non-blocking (a FIFO at this path cannot hang us), capped at MAX_STATE_BYTES.
277
+ const read = readBoundedFileStable(filePath, `"${filePath}"`, { boundary, maxBytes: MAX_STATE_BYTES, allowEmpty: true }, true);
278
+ if (!read.ok) {
279
+ if (read.code === 'FILE_MISSING')
280
+ return createInitialState();
281
+ throw new StateFileError(read.code, read.detail);
282
+ }
283
+ const raw = 'content' in read ? read.content : '';
284
+ let parsed;
285
+ try {
286
+ parsed = JSON.parse(raw);
287
+ }
288
+ catch (err) {
289
+ const msg = err instanceof Error ? err.message : String(err);
290
+ throw new StateCorruptedError(`"${filePath}" is not valid JSON: ${msg}`);
291
+ }
292
+ return validateState(parsed);
293
+ }
294
+ /**
295
+ * The read that precedes a write. A symlink or hardlink at the destination is
296
+ * reported with the write-side typed error and `NOT_PUBLISHED`, exactly as before
297
+ * the unified read (SEC-001B.2 compat). Every other read failure — a FIFO or
298
+ * special file included (SEC-001B.1) — stays the StateFileError of the read.
299
+ */
300
+ async function readStateForWrite(target, stateDir) {
301
+ try {
302
+ return await readState(target);
303
+ }
304
+ catch (err) {
305
+ if (err instanceof StateFileError) {
306
+ const code = err.fileCode === 'FILE_SYMLINK'
307
+ ? 'WRITE_TARGET_SYMLINK'
308
+ : err.fileCode === 'FILE_HARDLINKED'
309
+ ? 'WRITE_TARGET_HARDLINKED'
310
+ : null;
311
+ if (code !== null) {
312
+ throw new SafeWriteError(`${err.message} (refusing to replace it)`, {
313
+ code,
314
+ phase: 'validate',
315
+ publication: 'NOT_PUBLISHED',
316
+ path: stateFilePath(stateDir),
317
+ cause: err,
318
+ });
319
+ }
320
+ }
321
+ throw err;
322
+ }
323
+ }
324
+ /**
325
+ * Persist `state` through the safe-write primitive (SEC-001B.2, ADR-012):
326
+ * temp file (`O_EXCL|O_NOFOLLOW`, `0600`) -> fsync -> `rename`, then fsync of
327
+ * the directory. A new `state.json` is `0600`; an existing one keeps its mode.
328
+ * A symlink, FIFO/special file or hardlinked file at the destination is
329
+ * refused (never replaced). Throws SafeWriteError with publication
330
+ * NOT_PUBLISHED or UNKNOWN; once published nothing throws — later findings are
331
+ * returned as warnings.
332
+ */
333
+ function atomicWriteState(parent, filePath, state) {
334
+ const serialized = `${JSON.stringify(state, null, 2)}\n`;
335
+ // Refuse BEFORE creating the temp file: an oversized state.json would be
336
+ // unreadable by readState, locking the project out of its own state. The
337
+ // existing state.json is left untouched; nothing is compacted or dropped.
338
+ const bytes = Buffer.byteLength(serialized, 'utf8');
339
+ if (bytes > MAX_STATE_BYTES) {
340
+ throw new StateFileError('FILE_TOO_LARGE', `refusing to write "${filePath}": ${bytes} bytes exceeds the ${MAX_STATE_BYTES}-byte limit (see STATE-001); state was not modified.`);
341
+ }
342
+ return replaceFile(parent, STATE_FILE_NAME, serialized, {
343
+ mode: 0o600,
344
+ preserveMode: true,
345
+ durability: 'full',
346
+ maxBytes: MAX_STATE_BYTES,
347
+ }).warnings;
348
+ }
349
+ /** The write is already published; a throwing callback must not turn it into a failure. */
350
+ function reportWarnings(warnings, onWarning) {
351
+ if (!onWarning)
352
+ return;
353
+ for (const w of warnings) {
354
+ try {
355
+ onWarning(w);
356
+ }
357
+ catch {
358
+ /* ignored on purpose */
359
+ }
360
+ }
361
+ }
362
+ /**
363
+ * Read-modify-write under an exclusive cross-process lock. `mutator`
364
+ * receives a deep-cloned draft of the current state and an `applyResult`
365
+ * callback to hand back an arbitrary value (e.g. the mission it just
366
+ * created) alongside the write. The revision is incremented exactly once
367
+ * per successful call, after the mutator returns.
368
+ */
369
+ export async function mutateState(target, mutator, lockOptions) {
370
+ const { boundary, stateDir, onWarning } = resolveWriteTarget(target, 'mutateState');
371
+ const warnings = [];
372
+ // The instance's state directory may not exist yet (e.g. an existing
373
+ // project's first mission before any state.json has ever been written).
374
+ // Creating it here — component by component, refusing symlinks below the
375
+ // boundary — is the "safe, explicit initialization path" for that case; it
376
+ // never touches a state.json that already exists.
377
+ const parent = openParent(boundary, stateDir, { create: true });
378
+ const release = await acquireLock(lockFilePath(stateDir), lockOptions, {}, parent);
379
+ let draft;
380
+ let result;
381
+ try {
382
+ const current = await readStateForWrite(target, stateDir);
383
+ draft = structuredClone(current);
384
+ result = mutator(draft);
385
+ draft.revision = current.revision + 1;
386
+ warnings.push(...atomicWriteState(parent, stateFilePath(stateDir), draft));
387
+ }
388
+ finally {
389
+ release();
390
+ }
391
+ // The write is published and the lock is released: caller code runs outside
392
+ // the critical section, so a slow or re-entrant callback cannot hold up (or
393
+ // deadlock against) other writers.
394
+ reportWarnings(warnings, onWarning);
395
+ return { state: draft, result, warnings };
396
+ }