mirror-mind 0.0.1 → 0.33.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 (474) hide show
  1. package/.claude/hooks/log-assistant-reply.sh +85 -0
  2. package/.claude/hooks/log-session-end.sh +85 -0
  3. package/.claude/hooks/log-user-prompt.sh +85 -0
  4. package/.claude/hooks/mirror-inject.sh +85 -0
  5. package/.claude/hooks/session-start.sh +85 -0
  6. package/.gemini/hooks/log-assistant.sh +85 -0
  7. package/.gemini/hooks/log-user.sh +85 -0
  8. package/.gemini/hooks/session-end.sh +85 -0
  9. package/.gemini/hooks/session-start.sh +85 -0
  10. package/.pi/extensions/mirror-logger.ts +646 -0
  11. package/.pi/skills/mm-backup/SKILL.md +21 -0
  12. package/.pi/skills/mm-build/SKILL.md +1206 -0
  13. package/.pi/skills/mm-consolidate/SKILL.md +112 -0
  14. package/.pi/skills/mm-consult/SKILL.md +70 -0
  15. package/.pi/skills/mm-conversations/SKILL.md +16 -0
  16. package/.pi/skills/mm-discard/SKILL.md +39 -0
  17. package/.pi/skills/mm-explore/SKILL.md +255 -0
  18. package/.pi/skills/mm-help/SKILL.md +74 -0
  19. package/.pi/skills/mm-identity/SKILL.md +96 -0
  20. package/.pi/skills/mm-journal/SKILL.md +16 -0
  21. package/.pi/skills/mm-journey/SKILL.md +23 -0
  22. package/.pi/skills/mm-journeys/SKILL.md +15 -0
  23. package/.pi/skills/mm-memories/SKILL.md +17 -0
  24. package/.pi/skills/mm-mirror/SKILL.md +98 -0
  25. package/.pi/skills/mm-mute/SKILL.md +18 -0
  26. package/.pi/skills/mm-new/SKILL.md +18 -0
  27. package/.pi/skills/mm-recall/SKILL.md +17 -0
  28. package/.pi/skills/mm-release-notes/SKILL.md +52 -0
  29. package/.pi/skills/mm-seed/SKILL.md +19 -0
  30. package/.pi/skills/mm-shadow/SKILL.md +117 -0
  31. package/.pi/skills/mm-soul/SKILL.md +502 -0
  32. package/.pi/skills/mm-tasks/SKILL.md +24 -0
  33. package/.pi/skills/mm-update/SKILL.md +32 -0
  34. package/.pi/skills/mm-week/SKILL.md +28 -0
  35. package/.pi/skills/mm-welcome/SKILL.md +24 -0
  36. package/AGENTS.md +188 -0
  37. package/README.md +126 -9
  38. package/bin/loader.js +67 -0
  39. package/bin/mirror-hook.js +7 -0
  40. package/bin/mirror.js +7 -0
  41. package/docs/releases/index.md +100 -0
  42. package/docs/releases/pending-cutoffs.md +510 -0
  43. package/docs/releases/v0.10.0.md +123 -0
  44. package/docs/releases/v0.10.1.md +66 -0
  45. package/docs/releases/v0.10.10.md +51 -0
  46. package/docs/releases/v0.10.11.md +39 -0
  47. package/docs/releases/v0.10.12.md +57 -0
  48. package/docs/releases/v0.10.13.md +35 -0
  49. package/docs/releases/v0.10.14.md +42 -0
  50. package/docs/releases/v0.10.15.md +53 -0
  51. package/docs/releases/v0.10.16.md +35 -0
  52. package/docs/releases/v0.10.17.md +43 -0
  53. package/docs/releases/v0.10.2.md +64 -0
  54. package/docs/releases/v0.10.3.md +78 -0
  55. package/docs/releases/v0.10.4.md +42 -0
  56. package/docs/releases/v0.10.5.md +57 -0
  57. package/docs/releases/v0.10.6.md +47 -0
  58. package/docs/releases/v0.10.7.md +53 -0
  59. package/docs/releases/v0.10.8.md +49 -0
  60. package/docs/releases/v0.10.9.md +37 -0
  61. package/docs/releases/v0.11.0.md +86 -0
  62. package/docs/releases/v0.12.0.md +67 -0
  63. package/docs/releases/v0.13.0.md +64 -0
  64. package/docs/releases/v0.14.0.md +68 -0
  65. package/docs/releases/v0.15.0.md +48 -0
  66. package/docs/releases/v0.16.0.md +50 -0
  67. package/docs/releases/v0.16.1.md +55 -0
  68. package/docs/releases/v0.16.2.md +36 -0
  69. package/docs/releases/v0.17.0.md +50 -0
  70. package/docs/releases/v0.17.1.md +58 -0
  71. package/docs/releases/v0.17.2.md +42 -0
  72. package/docs/releases/v0.17.3.md +58 -0
  73. package/docs/releases/v0.18.0.md +84 -0
  74. package/docs/releases/v0.18.1.md +51 -0
  75. package/docs/releases/v0.18.2.md +69 -0
  76. package/docs/releases/v0.19.0.md +85 -0
  77. package/docs/releases/v0.19.1.md +46 -0
  78. package/docs/releases/v0.20.0.md +85 -0
  79. package/docs/releases/v0.21.0.md +61 -0
  80. package/docs/releases/v0.21.1.md +47 -0
  81. package/docs/releases/v0.22.0.md +77 -0
  82. package/docs/releases/v0.22.1.md +42 -0
  83. package/docs/releases/v0.23.0.md +105 -0
  84. package/docs/releases/v0.24.0.md +125 -0
  85. package/docs/releases/v0.25.0.md +107 -0
  86. package/docs/releases/v0.26.0.md +139 -0
  87. package/docs/releases/v0.26.1.md +65 -0
  88. package/docs/releases/v0.26.2.md +43 -0
  89. package/docs/releases/v0.27.0.md +63 -0
  90. package/docs/releases/v0.28.0.md +59 -0
  91. package/docs/releases/v0.29.0.md +61 -0
  92. package/docs/releases/v0.29.1.md +59 -0
  93. package/docs/releases/v0.29.2.md +59 -0
  94. package/docs/releases/v0.29.3.md +93 -0
  95. package/docs/releases/v0.30.0.md +75 -0
  96. package/docs/releases/v0.30.1.md +93 -0
  97. package/docs/releases/v0.30.2.md +66 -0
  98. package/docs/releases/v0.31.0.md +122 -0
  99. package/docs/releases/v0.31.1.md +142 -0
  100. package/docs/releases/v0.31.10.md +118 -0
  101. package/docs/releases/v0.31.11.md +88 -0
  102. package/docs/releases/v0.31.12.md +105 -0
  103. package/docs/releases/v0.31.13.md +99 -0
  104. package/docs/releases/v0.31.14.md +97 -0
  105. package/docs/releases/v0.31.2.md +115 -0
  106. package/docs/releases/v0.31.3.md +142 -0
  107. package/docs/releases/v0.31.4.md +117 -0
  108. package/docs/releases/v0.31.5.md +155 -0
  109. package/docs/releases/v0.31.6.md +132 -0
  110. package/docs/releases/v0.31.7.md +132 -0
  111. package/docs/releases/v0.31.8.md +120 -0
  112. package/docs/releases/v0.31.9.md +120 -0
  113. package/docs/releases/v0.32.0.md +261 -0
  114. package/docs/releases/v0.32.1.md +59 -0
  115. package/docs/releases/v0.33.0.md +164 -0
  116. package/docs/releases/v0.8.0.md +118 -0
  117. package/docs/releases/v0.9.0.md +98 -0
  118. package/docs/releases/v0.9.1.md +83 -0
  119. package/package.json +102 -6
  120. package/plugins/mirror-mind/.claude-plugin/plugin.json +17 -0
  121. package/plugins/mirror-mind/hooks/hooks.json +62 -0
  122. package/plugins/mirror-mind/hooks/log-assistant-reply.sh +98 -0
  123. package/plugins/mirror-mind/hooks/log-session-end.sh +98 -0
  124. package/plugins/mirror-mind/hooks/log-user-prompt.sh +98 -0
  125. package/plugins/mirror-mind/hooks/mirror-inject.sh +98 -0
  126. package/plugins/mirror-mind/hooks/session-start.sh +98 -0
  127. package/plugins/mirror-mind/skills/mm-backup/SKILL.md +27 -0
  128. package/plugins/mirror-mind/skills/mm-build/SKILL.md +1206 -0
  129. package/plugins/mirror-mind/skills/mm-consolidate/SKILL.md +112 -0
  130. package/plugins/mirror-mind/skills/mm-consult/SKILL.md +83 -0
  131. package/plugins/mirror-mind/skills/mm-conversations/SKILL.md +17 -0
  132. package/plugins/mirror-mind/skills/mm-discard/SKILL.md +40 -0
  133. package/plugins/mirror-mind/skills/mm-explore/SKILL.md +241 -0
  134. package/plugins/mirror-mind/skills/mm-help/SKILL.md +104 -0
  135. package/plugins/mirror-mind/skills/mm-identity/SKILL.md +96 -0
  136. package/plugins/mirror-mind/skills/mm-journal/SKILL.md +21 -0
  137. package/plugins/mirror-mind/skills/mm-journey/SKILL.md +37 -0
  138. package/plugins/mirror-mind/skills/mm-journeys/SKILL.md +15 -0
  139. package/plugins/mirror-mind/skills/mm-memories/SKILL.md +20 -0
  140. package/plugins/mirror-mind/skills/mm-mirror/SKILL.md +67 -0
  141. package/plugins/mirror-mind/skills/mm-mute/SKILL.md +18 -0
  142. package/plugins/mirror-mind/skills/mm-new/SKILL.md +18 -0
  143. package/plugins/mirror-mind/skills/mm-recall/SKILL.md +17 -0
  144. package/plugins/mirror-mind/skills/mm-release-notes/SKILL.md +31 -0
  145. package/plugins/mirror-mind/skills/mm-seed/SKILL.md +15 -0
  146. package/plugins/mirror-mind/skills/mm-shadow/SKILL.md +117 -0
  147. package/plugins/mirror-mind/skills/mm-soul/SKILL.md +488 -0
  148. package/plugins/mirror-mind/skills/mm-tasks/SKILL.md +36 -0
  149. package/plugins/mirror-mind/skills/mm-update/SKILL.md +30 -0
  150. package/plugins/mirror-mind/skills/mm-week/SKILL.md +46 -0
  151. package/plugins/mirror-mind/skills/mm-welcome/SKILL.md +22 -0
  152. package/scripts/codex-hooks/session-end.sh +85 -0
  153. package/scripts/codex-hooks/session-start.sh +85 -0
  154. package/scripts/codex-mirror.sh +48 -0
  155. package/templates/identity/README.md +119 -0
  156. package/templates/identity/ego/behavior.yaml +52 -0
  157. package/templates/identity/ego/constraints.yaml +15 -0
  158. package/templates/identity/ego/identity.yaml +43 -0
  159. package/templates/identity/journeys/personal-growth.yaml +39 -0
  160. package/templates/identity/organization/identity.yaml +24 -0
  161. package/templates/identity/organization/principles.yaml +17 -0
  162. package/templates/identity/personas/coach.yaml +94 -0
  163. package/templates/identity/personas/designer.yaml +93 -0
  164. package/templates/identity/personas/doctor.yaml +96 -0
  165. package/templates/identity/personas/engineer.yaml +77 -0
  166. package/templates/identity/personas/financial.yaml +97 -0
  167. package/templates/identity/personas/prompt-engineer.yaml +122 -0
  168. package/templates/identity/personas/researcher.yaml +88 -0
  169. package/templates/identity/personas/strategist.yaml +91 -0
  170. package/templates/identity/personas/teacher.yaml +95 -0
  171. package/templates/identity/personas/therapist.yaml +91 -0
  172. package/templates/identity/personas/thinker.yaml +74 -0
  173. package/templates/identity/personas/writer.yaml +76 -0
  174. package/templates/identity/self/config.yaml +16 -0
  175. package/templates/identity/self/soul.yaml +105 -0
  176. package/templates/identity/user/identity.yaml +30 -0
  177. package/ts/src/backup/staging.ts +62 -0
  178. package/ts/src/backup/zipBackup.ts +267 -0
  179. package/ts/src/backup/zipReader.ts +109 -0
  180. package/ts/src/backup/zipWriter.ts +148 -0
  181. package/ts/src/builder/activeCheckpoint.ts +134 -0
  182. package/ts/src/builder/aggregateStatus.ts +34 -0
  183. package/ts/src/builder/approve.ts +149 -0
  184. package/ts/src/builder/argv.ts +292 -0
  185. package/ts/src/builder/ariadMethod.ts +536 -0
  186. package/ts/src/builder/artifacts/artifactSurfaces.ts +106 -0
  187. package/ts/src/builder/artifacts/artifactWriter.ts +113 -0
  188. package/ts/src/builder/artifacts/closureArtifacts.ts +199 -0
  189. package/ts/src/builder/artifacts/deliveryStoryArtifacts.ts +316 -0
  190. package/ts/src/builder/artifacts/planArtifacts.ts +242 -0
  191. package/ts/src/builder/artifacts/scaffoldSections.ts +273 -0
  192. package/ts/src/builder/artifacts/scaffoldState.ts +227 -0
  193. package/ts/src/builder/artifacts/storyFiles.ts +61 -0
  194. package/ts/src/builder/artifacts/storyIndex.ts +166 -0
  195. package/ts/src/builder/cadence.ts +59 -0
  196. package/ts/src/builder/card.ts +192 -0
  197. package/ts/src/builder/checkpointRefused.ts +66 -0
  198. package/ts/src/builder/closure.ts +964 -0
  199. package/ts/src/builder/commands.ts +2053 -0
  200. package/ts/src/builder/cursorPosition.ts +176 -0
  201. package/ts/src/builder/cursorTransitions.ts +77 -0
  202. package/ts/src/builder/deliveryCursor.ts +577 -0
  203. package/ts/src/builder/deliveryStoryClosure.ts +521 -0
  204. package/ts/src/builder/deliveryStoryPlan.ts +536 -0
  205. package/ts/src/builder/deliveryStoryReady.ts +160 -0
  206. package/ts/src/builder/deliveryStoryRoadmapClosure.ts +151 -0
  207. package/ts/src/builder/expand.ts +336 -0
  208. package/ts/src/builder/flowUnit.ts +339 -0
  209. package/ts/src/builder/homeSurface.ts +122 -0
  210. package/ts/src/builder/implementationGuard.ts +125 -0
  211. package/ts/src/builder/index.ts +11 -0
  212. package/ts/src/builder/lifecycleRefusal.ts +92 -0
  213. package/ts/src/builder/lifecycleRibbon.ts +159 -0
  214. package/ts/src/builder/load.ts +313 -0
  215. package/ts/src/builder/methodAdoption.ts +113 -0
  216. package/ts/src/builder/methodDefinition.ts +429 -0
  217. package/ts/src/builder/methodInspection.ts +371 -0
  218. package/ts/src/builder/plan.ts +408 -0
  219. package/ts/src/builder/planPreauthorization.ts +265 -0
  220. package/ts/src/builder/prepare.ts +225 -0
  221. package/ts/src/builder/projectPaths.ts +64 -0
  222. package/ts/src/builder/pull.ts +356 -0
  223. package/ts/src/builder/pullCandidates.ts +463 -0
  224. package/ts/src/builder/pullCandidatesRender.ts +238 -0
  225. package/ts/src/builder/refinementField.ts +93 -0
  226. package/ts/src/builder/releaseIntent.ts +173 -0
  227. package/ts/src/builder/resumeState.ts +84 -0
  228. package/ts/src/builder/resumeSurface.ts +149 -0
  229. package/ts/src/builder/roadmapGrammar.ts +300 -0
  230. package/ts/src/builder/roadmapScan.ts +97 -0
  231. package/ts/src/builder/roadmapScope.ts +210 -0
  232. package/ts/src/builder/scopePhrases.ts +146 -0
  233. package/ts/src/builder/storyPaths.ts +196 -0
  234. package/ts/src/builder/storyPlanPreauthorization.ts +287 -0
  235. package/ts/src/builder/surfaceProtocol.ts +42 -0
  236. package/ts/src/builder/templateGeneration.ts +148 -0
  237. package/ts/src/builder/transition.ts +178 -0
  238. package/ts/src/consult/args.ts +99 -0
  239. package/ts/src/consult/core.ts +122 -0
  240. package/ts/src/consult/modelCatalog.ts +55 -0
  241. package/ts/src/consult/render.ts +47 -0
  242. package/ts/src/conversation/append.ts +414 -0
  243. package/ts/src/conversation/backfill.ts +387 -0
  244. package/ts/src/conversation/closeTail.ts +467 -0
  245. package/ts/src/conversation/extraction.ts +333 -0
  246. package/ts/src/conversation/extractionDriver.ts +130 -0
  247. package/ts/src/conversation/extractionRun.ts +70 -0
  248. package/ts/src/conversation/hookPayload.ts +51 -0
  249. package/ts/src/conversation/journeyInference.ts +165 -0
  250. package/ts/src/conversation/journeyRepair.ts +114 -0
  251. package/ts/src/conversation/lifecycleFaces.ts +197 -0
  252. package/ts/src/conversation/lifecycleWrites.ts +313 -0
  253. package/ts/src/conversation/listing.ts +77 -0
  254. package/ts/src/conversation/logger.ts +851 -0
  255. package/ts/src/conversation/loggerCli.ts +321 -0
  256. package/ts/src/conversation/loggerRuntime.ts +259 -0
  257. package/ts/src/conversation/metadataLifecycle.ts +443 -0
  258. package/ts/src/conversation/recall.ts +66 -0
  259. package/ts/src/conversation/sessionComposites.ts +258 -0
  260. package/ts/src/conversation/sessionImport.ts +90 -0
  261. package/ts/src/conversation/transcriptBackfill.ts +141 -0
  262. package/ts/src/cultivation/applyActions.ts +241 -0
  263. package/ts/src/cultivation/cluster.ts +86 -0
  264. package/ts/src/cultivation/consolidationStore.ts +281 -0
  265. package/ts/src/cultivation/promptContext.ts +62 -0
  266. package/ts/src/cultivation/propose.ts +305 -0
  267. package/ts/src/cultivation/scan.ts +155 -0
  268. package/ts/src/db/backupGate.ts +47 -0
  269. package/ts/src/db/bootstrap.ts +108 -0
  270. package/ts/src/db/bootstrapLock.ts +223 -0
  271. package/ts/src/db/copyGuard.ts +60 -0
  272. package/ts/src/db/database.ts +397 -0
  273. package/ts/src/db/decode.ts +66 -0
  274. package/ts/src/db/ftsIntegrity.ts +41 -0
  275. package/ts/src/db/migrateOnOpen.ts +214 -0
  276. package/ts/src/db/migrations.ts +576 -0
  277. package/ts/src/db/readyOnOpen.ts +35 -0
  278. package/ts/src/db/rowDecode.ts +42 -0
  279. package/ts/src/db/schema.ts +432 -0
  280. package/ts/src/db/schemaInventory.ts +225 -0
  281. package/ts/src/db/schemaInventorySnapshot.ts +2107 -0
  282. package/ts/src/db/schemaState.ts +91 -0
  283. package/ts/src/db/schemaTsDivergence.ts +114 -0
  284. package/ts/src/descriptor/descriptorRead.ts +49 -0
  285. package/ts/src/docs/docsLint.ts +265 -0
  286. package/ts/src/explorer/handoff.ts +522 -0
  287. package/ts/src/explorer/render.ts +335 -0
  288. package/ts/src/explorer/story.ts +769 -0
  289. package/ts/src/explorer/transition.ts +72 -0
  290. package/ts/src/extensions/bindings.ts +170 -0
  291. package/ts/src/extensions/catalog.ts +249 -0
  292. package/ts/src/extensions/catalogCommands.ts +513 -0
  293. package/ts/src/extensions/catalogWrites.ts +652 -0
  294. package/ts/src/extensions/contextRuntime.ts +325 -0
  295. package/ts/src/extensions/dispatch.ts +374 -0
  296. package/ts/src/extensions/errors.ts +24 -0
  297. package/ts/src/extensions/manifest.ts +356 -0
  298. package/ts/src/extensions/migrations.ts +350 -0
  299. package/ts/src/extraction/conversation.ts +304 -0
  300. package/ts/src/extraction/conversationMetadata.ts +206 -0
  301. package/ts/src/extraction/fencing.ts +106 -0
  302. package/ts/src/extraction/json.ts +17 -0
  303. package/ts/src/extraction/prompts.ts +470 -0
  304. package/ts/src/frontDoor/args.ts +30 -0
  305. package/ts/src/frontDoor/argvShape.ts +106 -0
  306. package/ts/src/frontDoor/buildLoadRuntime.ts +139 -0
  307. package/ts/src/frontDoor/buildRoute.ts +398 -0
  308. package/ts/src/frontDoor/cli.ts +1872 -0
  309. package/ts/src/frontDoor/cloneRoleGuard.ts +98 -0
  310. package/ts/src/frontDoor/consultRoute.ts +50 -0
  311. package/ts/src/frontDoor/contentTailRoute.ts +239 -0
  312. package/ts/src/frontDoor/conversationLoggerRoute.ts +83 -0
  313. package/ts/src/frontDoor/cultivationRoute.ts +210 -0
  314. package/ts/src/frontDoor/dbPath.ts +131 -0
  315. package/ts/src/frontDoor/dbSafetyToolsRoute.ts +240 -0
  316. package/ts/src/frontDoor/exploreRoute.ts +572 -0
  317. package/ts/src/frontDoor/extensionCatalogRoute.ts +252 -0
  318. package/ts/src/frontDoor/frontDoorLog.ts +57 -0
  319. package/ts/src/frontDoor/identityWrite.ts +36 -0
  320. package/ts/src/frontDoor/journeyWriteRoute.ts +26 -0
  321. package/ts/src/frontDoor/lifecycleWriteRoute.ts +110 -0
  322. package/ts/src/frontDoor/liveBackup.ts +100 -0
  323. package/ts/src/frontDoor/mirrorModeRoute.ts +163 -0
  324. package/ts/src/frontDoor/namedRefusal.ts +35 -0
  325. package/ts/src/frontDoor/nodeSupport.ts +24 -0
  326. package/ts/src/frontDoor/render/consolidate.ts +199 -0
  327. package/ts/src/frontDoor/render/conversations.ts +33 -0
  328. package/ts/src/frontDoor/render/cultivationShared.ts +21 -0
  329. package/ts/src/frontDoor/render/descriptor.ts +20 -0
  330. package/ts/src/frontDoor/render/detectPersona.ts +50 -0
  331. package/ts/src/frontDoor/render/icons.ts +13 -0
  332. package/ts/src/frontDoor/render/identity.ts +59 -0
  333. package/ts/src/frontDoor/render/identityRows.ts +17 -0
  334. package/ts/src/frontDoor/render/inspectPersona.ts +53 -0
  335. package/ts/src/frontDoor/render/journeyStatus.ts +78 -0
  336. package/ts/src/frontDoor/render/journeys.ts +79 -0
  337. package/ts/src/frontDoor/render/list.ts +41 -0
  338. package/ts/src/frontDoor/render/memories.ts +67 -0
  339. package/ts/src/frontDoor/render/recall.ts +49 -0
  340. package/ts/src/frontDoor/render/shadow.ts +155 -0
  341. package/ts/src/frontDoor/render/tasks.ts +99 -0
  342. package/ts/src/frontDoor/render/tasksImportSync.ts +63 -0
  343. package/ts/src/frontDoor/render/week.ts +113 -0
  344. package/ts/src/frontDoor/routing.ts +1001 -0
  345. package/ts/src/frontDoor/runtimeRoute.ts +623 -0
  346. package/ts/src/frontDoor/searchRoute.ts +166 -0
  347. package/ts/src/frontDoor/seedPaths.ts +98 -0
  348. package/ts/src/frontDoor/sessionStartWelcomeRoute.ts +166 -0
  349. package/ts/src/frontDoor/soulRoute.ts +433 -0
  350. package/ts/src/frontDoor/stdinSource.ts +37 -0
  351. package/ts/src/frontDoor/tasksImportSyncRoute.ts +110 -0
  352. package/ts/src/frontDoor/tasksWriteRoute.ts +101 -0
  353. package/ts/src/frontDoor/usage.ts +176 -0
  354. package/ts/src/guards/claudePlugin.ts +298 -0
  355. package/ts/src/guards/packContents.ts +306 -0
  356. package/ts/src/guards/piSourcedSkills.ts +70 -0
  357. package/ts/src/guards/retiredRouteShadows.ts +57 -0
  358. package/ts/src/guards/retiredSurfaces.ts +564 -0
  359. package/ts/src/hooks/claude.ts +214 -0
  360. package/ts/src/hooks/codex.ts +45 -0
  361. package/ts/src/hooks/gemini.ts +126 -0
  362. package/ts/src/hooks/main.ts +109 -0
  363. package/ts/src/hooks/mirrorState.ts +71 -0
  364. package/ts/src/hooks/payload.ts +73 -0
  365. package/ts/src/hooks/runtime.ts +179 -0
  366. package/ts/src/hooks/runtimeDetection.ts +34 -0
  367. package/ts/src/identity/applyConsolidationIdentityUpdate.ts +67 -0
  368. package/ts/src/identity/descriptorGenerate.ts +162 -0
  369. package/ts/src/identity/identityEdit.ts +143 -0
  370. package/ts/src/identity/identityKey.ts +77 -0
  371. package/ts/src/identity/identityRead.ts +69 -0
  372. package/ts/src/identity/identityStore.ts +80 -0
  373. package/ts/src/identity/journeyListing.ts +53 -0
  374. package/ts/src/identity/personaInspect.ts +44 -0
  375. package/ts/src/identity/personaListing.ts +37 -0
  376. package/ts/src/identity/setIdentity.ts +64 -0
  377. package/ts/src/index.ts +231 -0
  378. package/ts/src/init/init.ts +111 -0
  379. package/ts/src/journey/journeyOptions.ts +131 -0
  380. package/ts/src/journey/journeyRemoval.ts +158 -0
  381. package/ts/src/journey/journeyStatus.ts +149 -0
  382. package/ts/src/journey/journeySyncFile.ts +87 -0
  383. package/ts/src/journey/journeyWrite.ts +203 -0
  384. package/ts/src/journey/parentJourney.ts +41 -0
  385. package/ts/src/journey/validateParentJourney.ts +55 -0
  386. package/ts/src/mcp/boundary.ts +151 -0
  387. package/ts/src/mcp/guards.ts +244 -0
  388. package/ts/src/mcp/main.ts +121 -0
  389. package/ts/src/mcp/payload.ts +141 -0
  390. package/ts/src/mcp/protocol.ts +141 -0
  391. package/ts/src/mcp/readModels.ts +241 -0
  392. package/ts/src/mcp/registry.ts +201 -0
  393. package/ts/src/mcp/serve.ts +120 -0
  394. package/ts/src/mcp/tools/deterministic.ts +137 -0
  395. package/ts/src/mcp/tools/providerCrossing.ts +187 -0
  396. package/ts/src/mcp/wire.ts +8 -0
  397. package/ts/src/memory/journal.ts +180 -0
  398. package/ts/src/memory/listing.ts +107 -0
  399. package/ts/src/memory/memoryWrite.ts +88 -0
  400. package/ts/src/memory/reinforcement.ts +35 -0
  401. package/ts/src/mirror/context.ts +161 -0
  402. package/ts/src/mirror/defaultResolution.ts +199 -0
  403. package/ts/src/mirror/mirrorForm.ts +43 -0
  404. package/ts/src/mirror/orchestration.ts +364 -0
  405. package/ts/src/mirror/reception.ts +194 -0
  406. package/ts/src/mirror/render.ts +75 -0
  407. package/ts/src/mirror/runtimeSession.ts +205 -0
  408. package/ts/src/mode/operatingMode.ts +148 -0
  409. package/ts/src/observability/callOutcome.ts +115 -0
  410. package/ts/src/observability/inspectLedger.ts +329 -0
  411. package/ts/src/observability/ledgerHooks.ts +126 -0
  412. package/ts/src/observability/llmCalls.ts +154 -0
  413. package/ts/src/persona/detectPersona.ts +81 -0
  414. package/ts/src/planning/promptAssembly.ts +111 -0
  415. package/ts/src/planning/weekPending.ts +81 -0
  416. package/ts/src/planning/weekPlan.ts +183 -0
  417. package/ts/src/planning/weekSave.ts +117 -0
  418. package/ts/src/providers/config.ts +189 -0
  419. package/ts/src/providers/cost.ts +64 -0
  420. package/ts/src/providers/credits.ts +204 -0
  421. package/ts/src/providers/embedding.ts +321 -0
  422. package/ts/src/providers/familyProviders.ts +147 -0
  423. package/ts/src/providers/llm.ts +334 -0
  424. package/ts/src/providers/openrouter.ts +244 -0
  425. package/ts/src/providers/redaction.ts +110 -0
  426. package/ts/src/providers/replay.ts +15 -0
  427. package/ts/src/providers/transport.ts +410 -0
  428. package/ts/src/repair/encodingRepair.ts +253 -0
  429. package/ts/src/runtime/backup.ts +151 -0
  430. package/ts/src/runtime/channel.ts +146 -0
  431. package/ts/src/runtime/config.ts +168 -0
  432. package/ts/src/runtime/diagnose.ts +686 -0
  433. package/ts/src/runtime/git.ts +510 -0
  434. package/ts/src/runtime/installKind.ts +220 -0
  435. package/ts/src/runtime/migrate.ts +154 -0
  436. package/ts/src/runtime/packageIdentity.ts +102 -0
  437. package/ts/src/runtime/releaseNotes.ts +244 -0
  438. package/ts/src/runtime/status.ts +557 -0
  439. package/ts/src/runtime/strategies/clone.ts +200 -0
  440. package/ts/src/runtime/strategies/package.ts +328 -0
  441. package/ts/src/runtime/treeRoot.ts +30 -0
  442. package/ts/src/runtime/update.ts +445 -0
  443. package/ts/src/runtime/updateGate.ts +40 -0
  444. package/ts/src/runtime/updatePipeline.ts +159 -0
  445. package/ts/src/runtime/version.ts +36 -0
  446. package/ts/src/runtime/wiring.ts +130 -0
  447. package/ts/src/search/memorySearch.ts +338 -0
  448. package/ts/src/search/ranker.ts +154 -0
  449. package/ts/src/seed/seed.ts +256 -0
  450. package/ts/src/seed/seedYaml.ts +133 -0
  451. package/ts/src/soul/apply.ts +218 -0
  452. package/ts/src/soul/harvest.ts +267 -0
  453. package/ts/src/soul/prompts/soul_beauty_voice.md +103 -0
  454. package/ts/src/soul/prompts/soul_self_voice.md +219 -0
  455. package/ts/src/soul/prompts/soul_wisdom_voice.md +121 -0
  456. package/ts/src/soul/prompts.ts +85 -0
  457. package/ts/src/soul/render.ts +358 -0
  458. package/ts/src/soul/state.ts +212 -0
  459. package/ts/src/soul/transition.ts +37 -0
  460. package/ts/src/tasks/journeyPathParse.ts +113 -0
  461. package/ts/src/tasks/taskImportSync.ts +142 -0
  462. package/ts/src/tasks/taskStore.ts +312 -0
  463. package/ts/src/tasks/weekView.ts +120 -0
  464. package/ts/src/util/clipCodePoints.ts +26 -0
  465. package/ts/src/util/paths.ts +63 -0
  466. package/ts/src/util/program.ts +9 -0
  467. package/ts/src/util/pyGenerators.ts +144 -0
  468. package/ts/src/util/pythonText.ts +317 -0
  469. package/ts/src/util/semver.ts +45 -0
  470. package/ts/src/util/shellWord.ts +25 -0
  471. package/ts/src/util/slug.ts +64 -0
  472. package/ts/src/welcome/card.ts +339 -0
  473. package/ts/src/welcome/statusLine.ts +101 -0
  474. package/ts/src/welcome/updateCache.ts +129 -0
@@ -0,0 +1,2053 @@
1
+ // CV22.DS7.US8 plateau 1 — `build inspect-method` and `build pull-candidates`,
2
+ // end to end.
3
+ //
4
+ // Port of `cmd_inspect_method`, `cmd_pull_candidates`, and the shared guards
5
+ // `_reject_unknown_method`, `_resolve_builder_journey`, `_require_adopted_method`
6
+ // from `src/memory/cli/build.py`.
7
+ //
8
+ // Nothing is routed here. These return a `CommandResult` rather than writing to
9
+ // the process, so plateau 8's front door wires them and the tests can grade
10
+ // stdout, stderr, and the exit code without a subprocess.
11
+ //
12
+ // `build check-implementation` is deliberately absent: it calls
13
+ // `assert_implementation_allowed`, which reads the delivery cursor, so it belongs
14
+ // with the cursor in plateau 2 rather than being half-ported here.
15
+ //
16
+ // The refusal taxonomy measured at plateau 1 applies directly (D3.17). Everything
17
+ // in this module is CLASS B: a domain refusal at exit 1 whose `Error: …` text is a
18
+ // byte-exact parity obligation. Class A — argparse's own exit 2 with usage text —
19
+ // happens before any of this runs and is the front door's business.
20
+ //
21
+ // One ordering detail no surface reveals but every refusal depends on: the guards
22
+ // run method-first, then journey resolution, then journey existence, then
23
+ // adoption. So `pull-candidates --method bogus` with no journey reports the
24
+ // METHOD error, and the same command with a valid method but no resolvable
25
+ // journey reports the JOURNEY error. A port that resolves the journey first
26
+ // reports the wrong one.
27
+
28
+ import { existsSync } from "node:fs";
29
+ import { dirname, join } from "node:path";
30
+ import type { Database, WritableDatabase } from "#db/database.ts";
31
+ import { getIdentityContent } from "#identity/identityRead.ts";
32
+ import { getProjectPath } from "#journey/journeyStatus.ts";
33
+ import { getRuntimeSession, resolveNamedRuntimeSessionId } from "#mirror/runtimeSession.ts";
34
+ import { getSessionOperatingMode } from "#mode/operatingMode.ts";
35
+ import { PROGRAM } from "#util/program.ts";
36
+ import { pyRStrip } from "#util/pythonText.ts";
37
+ import { shellWord } from "#util/shellWord.ts";
38
+ import { CLOSURE_RECORDS, renderActiveCheckpoint } from "./activeCheckpoint.ts";
39
+ import { approvePlanCheckpoint, renderPlanApproval } from "./approve.ts";
40
+ import { getAriadMethod } from "./ariadMethod.ts";
41
+ import {
42
+ type MaterializedArtifact,
43
+ renderArtifactsMaterializedSurface,
44
+ } from "./artifacts/artifactSurfaces.ts";
45
+ import type { ArtifactOutcome } from "./artifacts/artifactWriter.ts";
46
+ import { planPackageArtifacts } from "./artifacts/planArtifacts.ts";
47
+ import { judgeStoryFiles } from "./artifacts/storyFiles.ts";
48
+ import { effectiveCadenceProfile } from "./cadence.ts";
49
+ import { CARD_WIDTH, cardText, wrapPlainText } from "./card.ts";
50
+ import { renderCheckpointRefused } from "./checkpointRefused.ts";
51
+ import {
52
+ coherenceLifecycleItem,
53
+ doneLifecycleItem,
54
+ renderCoherenceCheckpoint,
55
+ renderDoneCheckpoint,
56
+ renderReviewCheckpoint,
57
+ renderValidationCheckpoint,
58
+ reviewLifecycleItem,
59
+ validateLifecycleItem,
60
+ } from "./closure.ts";
61
+ import {
62
+ type BuilderDeliveryCursor,
63
+ type CursorWriteDeps,
64
+ getDeliveryCursor,
65
+ renderDeliveryCursorSyncReport,
66
+ setDeliveryCursor,
67
+ } from "./deliveryCursor.ts";
68
+ import {
69
+ closureArtifactManifest,
70
+ coherenceDeliveryStory,
71
+ type DeliveryStoryClosureReport,
72
+ doneDeliveryStory,
73
+ renderDeliveryStoryClosureReport,
74
+ reviewDeliveryStory,
75
+ validateDeliveryStory,
76
+ } from "./deliveryStoryClosure.ts";
77
+ import {
78
+ approveDeliveryStoryPlan,
79
+ cancelDeliveryStoryPlanPreauthorization,
80
+ planDeliveryStoryCheckpoint,
81
+ renderDeliveryStoryImplementationStarted,
82
+ renderDeliveryStoryPlanReport,
83
+ renderPlanPreauthorizationMismatch,
84
+ renderPlanPreauthorizationRecorded,
85
+ } from "./deliveryStoryPlan.ts";
86
+ import { renderDeliveryStoryReadyReport } from "./deliveryStoryReady.ts";
87
+ import { inspectAuthoredClosure } from "./deliveryStoryRoadmapClosure.ts";
88
+ import { ExpandBlockedError, expandDeliveryStory, renderExpandBlocked } from "./expand.ts";
89
+ import {
90
+ inspectNavigatorFlowUnit,
91
+ renderFlowUnitScopeConfirmationReport,
92
+ renderNavigatorFlowUnitReport,
93
+ setNavigatorFlowUnit,
94
+ } from "./flowUnit.ts";
95
+ import {
96
+ assertImplementationAllowed,
97
+ ImplementationBlockedError,
98
+ renderImplementationGuardAllowed,
99
+ renderImplementationGuardBlocked,
100
+ } from "./implementationGuard.ts";
101
+ import { LifecycleRefusal } from "./lifecycleRefusal.ts";
102
+ import { renderLifecycleRibbon } from "./lifecycleRibbon.ts";
103
+ import { getAdoptedMethod, setAdoptedMethod } from "./methodAdoption.ts";
104
+ import {
105
+ AVAILABLE_METHODS,
106
+ renderAvailableMethod,
107
+ renderJourneyMethodState,
108
+ renderMethodAdoptionReport,
109
+ renderNoJourneyNamed,
110
+ } from "./methodInspection.ts";
111
+ import { planLifecycleItem, renderPlanCheckpoint } from "./plan.ts";
112
+ import { PlanPreauthorizationMismatch } from "./planPreauthorization.ts";
113
+ import { prepareLifecycleItem, projectContractRules, renderPrepareReport } from "./prepare.ts";
114
+ import { displayPath } from "./projectPaths.ts";
115
+ import { placementCvTitle, pullLifecycleItem, pullPlacement, renderPullReport } from "./pull.ts";
116
+ import { inspectPullCandidates, inspectRoadmapSnapshot } from "./pullCandidates.ts";
117
+ import {
118
+ renderProjectPositionReport,
119
+ renderPullCandidatesReport,
120
+ renderRoadmapSnapshotReport,
121
+ } from "./pullCandidatesRender.ts";
122
+ import {
123
+ inspectReleaseIntent,
124
+ renderReleaseIntentReport,
125
+ setReleaseIntent,
126
+ } from "./releaseIntent.ts";
127
+ import {
128
+ resolveRoadmapScope,
129
+ type ScopedPullCandidates,
130
+ scopePullCandidates,
131
+ siblingsOf,
132
+ } from "./roadmapScope.ts";
133
+ import {
134
+ createStoryDirectory,
135
+ resolveStoryDirectory,
136
+ StoryPackageAmbiguityError,
137
+ } from "./storyPaths.ts";
138
+ import {
139
+ approveStoryPlanWithPreauthorization,
140
+ cancelStoryPlanPreauthorization,
141
+ renderStoryImplementationStarted,
142
+ renderStoryPlanPreauthorizationMismatch,
143
+ renderStoryPlanPreauthorizationRecorded,
144
+ renderStoryPreauthorizationAlreadyConsumed,
145
+ } from "./storyPlanPreauthorization.ts";
146
+ import { wrapAriadSurface } from "./surfaceProtocol.ts";
147
+ import { prepareMethodTemplates, renderTemplatePreparationReport } from "./templateGeneration.ts";
148
+
149
+ /** What a `build` leaf produced, without touching the process. */
150
+ export interface CommandResult {
151
+ readonly stdout: string;
152
+ readonly stderr: string;
153
+ readonly exitCode: number;
154
+ }
155
+
156
+ /**
157
+ * Python `print(text)` — the value PLUS the newline `print` adds.
158
+ *
159
+ * Every renderer here already ends in `\n`, so each surface leaves a blank line
160
+ * behind it and the stream ends with TWO newlines. That is not a quirk worth
161
+ * smoothing: it is the byte a shell sees, and reproducing the expression Python
162
+ * passes to `print` while forgetting `print` itself is one byte short on every
163
+ * invocation. Measured against the real CLI in a subprocess, not reasoned about.
164
+ */
165
+ function printed(text: string): string {
166
+ return `${text}\n`;
167
+ }
168
+
169
+ const IDENTITY_JOURNEY_LAYER = "journey";
170
+
171
+ export interface BuilderCommandContext {
172
+ readonly db: Database;
173
+ /** `MIRROR_SESSION_ID`, passed in rather than read, as every TS route does. */
174
+ readonly environmentSessionId?: string | null;
175
+ }
176
+
177
+ /** The context the writing leaves need: a writable handle and a clock. */
178
+ export interface BuilderWriteContext {
179
+ readonly db: WritableDatabase;
180
+ readonly environmentSessionId?: string | null;
181
+ readonly deps: CursorWriteDeps;
182
+ }
183
+
184
+ /** Python's `print(message, file=sys.stderr); sys.exit(1)` pair. */
185
+ function refuse(message: string): CommandResult {
186
+ return { stdout: "", stderr: printed(message), exitCode: 1 };
187
+ }
188
+
189
+ /** Python `_reject_unknown_method`. */
190
+ function rejectUnknownMethod(method: string): CommandResult | null {
191
+ if (method === "ariad") return null;
192
+ return refuse(
193
+ `Error: Builder method '${method}' not found. Available methods: ${AVAILABLE_METHODS.join(", ")}`,
194
+ );
195
+ }
196
+
197
+ /**
198
+ * The Builder journey of the session the caller NAMED (`--session-id` or
199
+ * `MIRROR_SESSION_ID`), or null.
200
+ *
201
+ * Never a guessed session and never the global mode row (CR008): either can hold
202
+ * another window's journey, and binding to it wrote that journey's cursor and
203
+ * materialized files into its project. A named session that has ended, or that
204
+ * is in another mode, names no journey.
205
+ *
206
+ * Decision C (CR008): when no session is named, `build load` still stamps a
207
+ * GUESSED session, so each window's footer shows its own mode. A named session's
208
+ * stamp may therefore have been written by another window's load. That cannot
209
+ * bind today, because no runtime names its session. Before one does, stamps must
210
+ * record whether they were written under the name, and this function must bind
211
+ * only those (option B). `namedSessionPrecondition.test.ts` fails the day a
212
+ * runtime starts naming its session.
213
+ */
214
+ function namedSessionBuilderJourney(
215
+ context: BuilderCommandContext,
216
+ sessionId: string | null,
217
+ ): string | null {
218
+ const named = resolveNamedRuntimeSessionId(sessionId, context.environmentSessionId ?? null);
219
+ if (!named || !getRuntimeSession(context.db, named)?.active) return null;
220
+ const state = getSessionOperatingMode(context.db, named);
221
+ return state?.mode === "Builder Mode" && state.journey ? state.journey : null;
222
+ }
223
+
224
+ /**
225
+ * The journey a Builder leaf acts on: the explicit `--journey`, else the Builder
226
+ * journey of a named session. Nothing else. With neither, the leaf refuses
227
+ * before it reads a cursor or writes a file.
228
+ *
229
+ * This departs from Python's `_resolve_builder_journey` on purpose (CR008).
230
+ * Python fell back to the most recently touched session and then the global
231
+ * mode row, and an agent shell names no session, so every lifecycle command
232
+ * without `--journey` was bound by a guess.
233
+ */
234
+ function resolveBuilderJourney(
235
+ context: BuilderCommandContext,
236
+ options: { journey: string | null; sessionId: string | null; action: string },
237
+ ): { journey: string } | CommandResult {
238
+ if (options.journey) return { journey: options.journey };
239
+ const journey = namedSessionBuilderJourney(context, options.sessionId);
240
+ if (journey) return { journey };
241
+ return refuse(
242
+ `Error: Builder method ${options.action} requires a journey. ` +
243
+ "Pass --journey <slug>, or name a session in Builder Mode with --session-id or MIRROR_SESSION_ID.",
244
+ );
245
+ }
246
+
247
+ function isCommandResult(value: unknown): value is CommandResult {
248
+ return typeof value === "object" && value !== null && "exitCode" in value;
249
+ }
250
+
251
+ /** Python's `mem.get_identity("journey", slug)` existence guard. */
252
+ function requireJourney(db: Database, journey: string): CommandResult | null {
253
+ const content = getIdentityContent(db, IDENTITY_JOURNEY_LAYER, journey);
254
+ if (content) return null;
255
+ return refuse(`Error: journey '${journey}' not found.`);
256
+ }
257
+
258
+ /** Python `_require_adopted_method`. */
259
+ function requireAdoptedMethod(db: Database, journey: string, method: string): CommandResult | null {
260
+ if (getAdoptedMethod(db, journey) === method) return null;
261
+ return refuse(
262
+ `Error: journey '${journey}' has not adopted Ariad yet. ` +
263
+ `Run: ${PROGRAM} build adopt --journey ${shellWord(journey)} --method ariad`,
264
+ );
265
+ }
266
+
267
+ /**
268
+ * Python `cmd_inspect_method`.
269
+ *
270
+ * Three shapes, and the order of the branches is the behavior:
271
+ *
272
+ * * `--journey X` given -> that journey's state, after an existence check. The
273
+ * positional method is IGNORED in this branch, so `inspect-method ariad
274
+ * --journey j` renders the journey state rather than the method definition.
275
+ * * no positional method -> the Builder journey of a NAMED session if there is
276
+ * one, else the no-journey card. Never an error, and never a guess (CR008).
277
+ * * a positional method -> the built-in definition, or a Class B refusal.
278
+ */
279
+ export function runInspectMethod(
280
+ context: BuilderCommandContext,
281
+ options: { method?: string | null; journey?: string | null; sessionId?: string | null } = {},
282
+ ): CommandResult {
283
+ const method = options.method ?? null;
284
+ const journey = options.journey ?? null;
285
+
286
+ if (journey) {
287
+ const missing = requireJourney(context.db, journey);
288
+ if (missing) return missing;
289
+ return {
290
+ stdout: printed(renderJourneyMethodState(journey, getAdoptedMethod(context.db, journey))),
291
+ stderr: "",
292
+ exitCode: 0,
293
+ };
294
+ }
295
+
296
+ if (method === null) {
297
+ const named = namedSessionBuilderJourney(context, options.sessionId ?? null);
298
+ if (named) {
299
+ return {
300
+ stdout: printed(renderJourneyMethodState(named, getAdoptedMethod(context.db, named))),
301
+ stderr: "",
302
+ exitCode: 0,
303
+ };
304
+ }
305
+ return { stdout: printed(renderNoJourneyNamed()), stderr: "", exitCode: 0 };
306
+ }
307
+
308
+ const unknown = rejectUnknownMethod(method);
309
+ if (unknown) return unknown;
310
+ return { stdout: printed(renderAvailableMethod(getAriadMethod())), stderr: "", exitCode: 0 };
311
+ }
312
+
313
+ /** Python `_surfaces_for_trigger`: the surfaces a DSL route declares for a trigger. */
314
+ export function surfacesForTrigger(trigger: string): readonly string[] {
315
+ for (const route of getAriadMethod().surfaceRoutes) {
316
+ if (route.trigger === trigger) return route.surfaces;
317
+ }
318
+ return [];
319
+ }
320
+
321
+ /**
322
+ * Python `cmd_pull_candidates`.
323
+ *
324
+ * Which surfaces appear is decided by the DSL, not by the command: the
325
+ * `show_roadmap` surface route names them, so removing `roadmap_snapshot` from
326
+ * `ariadMethod.ts` would drop that block from this output. That indirection is
327
+ * the method DSL doing its job and is preserved.
328
+ *
329
+ * The join is Python's exactly: each part is `rstrip()`ed, joined with a single
330
+ * newline, and ONE trailing newline is added — so two surfaces are separated by
331
+ * the newline that ends the first one, with no blank line between them.
332
+ */
333
+ export function runPullCandidates(
334
+ context: BuilderCommandContext,
335
+ options: { method: string; journey?: string | null; sessionId?: string | null },
336
+ ): CommandResult {
337
+ const unknown = rejectUnknownMethod(options.method);
338
+ if (unknown) return unknown;
339
+
340
+ const resolved = resolveBuilderJourney(context, {
341
+ journey: options.journey ?? null,
342
+ sessionId: options.sessionId ?? null,
343
+ action: "pull candidates inspection",
344
+ });
345
+ if (isCommandResult(resolved)) return resolved;
346
+ const journey = resolved.journey;
347
+
348
+ const missing = requireJourney(context.db, journey);
349
+ if (missing) return missing;
350
+ const notAdopted = requireAdoptedMethod(context.db, journey, options.method);
351
+ if (notAdopted) return notAdopted;
352
+
353
+ const projectPath = getProjectPath(context.db, journey);
354
+ const root = projectPath || null;
355
+ const surfaces = surfacesForTrigger("show_roadmap");
356
+ const rendered: string[] = [];
357
+ const view = journeyCandidateView(context.db, journey, root, options.method);
358
+ if (surfaces.includes("roadmap_snapshot")) {
359
+ rendered.push(
360
+ renderRoadmapSnapshotReport(
361
+ inspectRoadmapSnapshot(root, { journey, method: options.method }),
362
+ { view },
363
+ ),
364
+ );
365
+ }
366
+ if (surfaces.includes("pull_candidates")) {
367
+ rendered.push(renderPullCandidatesReport(view));
368
+ }
369
+ return {
370
+ stdout: printed(`${rendered.map((part) => pyRStrip(part)).join("\n")}\n`),
371
+ stderr: "",
372
+ exitCode: 0,
373
+ };
374
+ }
375
+
376
+ /**
377
+ * Python `cmd_adopt_method`.
378
+ *
379
+ * `already_adopted` is computed BEFORE the write, so re-adopting Ariad reports
380
+ * "was already adopted" while a first adoption reports "is now adopted" — and
381
+ * switching from another method reports "is now", because the comparison is
382
+ * against the method being adopted rather than against any adoption at all.
383
+ */
384
+ export function runAdoptMethod(
385
+ context: BuilderWriteContext,
386
+ options: { method: string; journey?: string | null; sessionId?: string | null },
387
+ ): CommandResult {
388
+ const unknown = rejectUnknownMethod(options.method);
389
+ if (unknown) return unknown;
390
+
391
+ const resolved = resolveBuilderJourney(context, {
392
+ journey: options.journey ?? null,
393
+ sessionId: options.sessionId ?? null,
394
+ action: "adoption",
395
+ });
396
+ if (isCommandResult(resolved)) return resolved;
397
+ const journey = resolved.journey;
398
+
399
+ const missing = requireJourney(context.db, journey);
400
+ if (missing) return missing;
401
+
402
+ const alreadyAdopted = getAdoptedMethod(context.db, journey) === options.method;
403
+ const adoption = setAdoptedMethod(context.db, journey, options.method, context.deps.nowIso);
404
+ return {
405
+ stdout: printed(
406
+ renderMethodAdoptionReport(adoption.journey, adoption.method, { alreadyAdopted }),
407
+ ),
408
+ stderr: "",
409
+ exitCode: 0,
410
+ };
411
+ }
412
+
413
+ /**
414
+ * Python `cmd_prepare_templates`.
415
+ *
416
+ * The only leaf in this plateau that writes into the user's repository, and the
417
+ * only one with a fifth guard: a journey with no `project_path` is refused rather
418
+ * than defaulted, because there is nowhere safe to write.
419
+ */
420
+ export function runPrepareTemplates(
421
+ context: BuilderWriteContext,
422
+ options: { method: string; journey?: string | null; sessionId?: string | null },
423
+ ): CommandResult {
424
+ const unknown = rejectUnknownMethod(options.method);
425
+ if (unknown) return unknown;
426
+
427
+ const resolved = resolveBuilderJourney(context, {
428
+ journey: options.journey ?? null,
429
+ sessionId: options.sessionId ?? null,
430
+ action: "template preparation",
431
+ });
432
+ if (isCommandResult(resolved)) return resolved;
433
+ const journey = resolved.journey;
434
+
435
+ const missing = requireJourney(context.db, journey);
436
+ if (missing) return missing;
437
+ const notAdopted = requireAdoptedMethod(context.db, journey, options.method);
438
+ if (notAdopted) return notAdopted;
439
+
440
+ const projectPath = getProjectPath(context.db, journey);
441
+ if (!projectPath) {
442
+ return refuse(
443
+ `Error: journey '${journey}' has no project_path configured. ` +
444
+ "Set a project path before preparing Ariad templates.",
445
+ );
446
+ }
447
+
448
+ const report = prepareMethodTemplates(projectPath, { journey, method: getAriadMethod() });
449
+ return { stdout: printed(renderTemplatePreparationReport(report)), stderr: "", exitCode: 0 };
450
+ }
451
+
452
+ /**
453
+ * Python `cmd_sync_cursor`.
454
+ *
455
+ * The written cursor is fixed, not derived: `last_delivery_event` is always
456
+ * `template_preparation` and `cadence_profile` always `stepwise`, and no active
457
+ * item is inferred. Running it on a journey that already has a cursor therefore
458
+ * RESETS those two fields while carrying the generation forward — which is the
459
+ * cursor's own carry-forward rule, not something this command decides.
460
+ */
461
+ export function runSyncCursor(
462
+ context: BuilderWriteContext,
463
+ options: { method: string; journey?: string | null; sessionId?: string | null },
464
+ ): CommandResult {
465
+ const unknown = rejectUnknownMethod(options.method);
466
+ if (unknown) return unknown;
467
+
468
+ const resolved = resolveBuilderJourney(context, {
469
+ journey: options.journey ?? null,
470
+ sessionId: options.sessionId ?? null,
471
+ action: "cursor sync",
472
+ });
473
+ if (isCommandResult(resolved)) return resolved;
474
+ const journey = resolved.journey;
475
+
476
+ const missing = requireJourney(context.db, journey);
477
+ if (missing) return missing;
478
+ const notAdopted = requireAdoptedMethod(context.db, journey, options.method);
479
+ if (notAdopted) return notAdopted;
480
+
481
+ const cursor = setDeliveryCursor(
482
+ context.db,
483
+ {
484
+ journey,
485
+ method: options.method,
486
+ lastDeliveryEvent: "template_preparation",
487
+ cadenceProfile: "stepwise",
488
+ },
489
+ context.deps,
490
+ );
491
+ return { stdout: printed(renderDeliveryCursorSyncReport(cursor)), stderr: "", exitCode: 0 };
492
+ }
493
+
494
+ /**
495
+ * Python `cmd_check_implementation`.
496
+ *
497
+ * The only leaf whose REFUSAL prints a surface on stdout rather than a message on
498
+ * stderr: a blocked guard renders `IMPLEMENTATION_GUARD` to stdout and still exits
499
+ * 1. A port that routes the block through the ordinary `refuse` helper loses the
500
+ * surface the transport protocol requires to be rendered verbatim.
501
+ */
502
+ export function runCheckImplementation(
503
+ context: BuilderCommandContext,
504
+ options: { method: string; journey?: string | null; sessionId?: string | null },
505
+ ): CommandResult {
506
+ const unknown = rejectUnknownMethod(options.method);
507
+ if (unknown) return unknown;
508
+
509
+ const resolved = resolveBuilderJourney(context, {
510
+ journey: options.journey ?? null,
511
+ sessionId: options.sessionId ?? null,
512
+ action: "implementation check",
513
+ });
514
+ if (isCommandResult(resolved)) return resolved;
515
+ const journey = resolved.journey;
516
+
517
+ const missing = requireJourney(context.db, journey);
518
+ if (missing) return missing;
519
+ const notAdopted = requireAdoptedMethod(context.db, journey, options.method);
520
+ if (notAdopted) return notAdopted;
521
+
522
+ try {
523
+ const cursor = assertImplementationAllowed(context.db, journey);
524
+ return {
525
+ stdout: printed(renderImplementationGuardAllowed(cursor)),
526
+ stderr: "",
527
+ exitCode: 0,
528
+ };
529
+ } catch (error) {
530
+ if (!(error instanceof ImplementationBlockedError)) throw error;
531
+ return {
532
+ stdout: printed(
533
+ renderImplementationGuardBlocked(
534
+ error.message,
535
+ getDeliveryCursor(context.db, journey)?.activeItemLevel ?? null,
536
+ ),
537
+ ),
538
+ stderr: "",
539
+ exitCode: 1,
540
+ };
541
+ }
542
+ }
543
+
544
+ /**
545
+ * CR067 with CR020: the journey's active checkpoint, shown again. Read-only by
546
+ * construction: it takes the read context, and the front door runs it on a read-only
547
+ * database handle like every leaf in `READ_ONLY_BUILDER_SUBCOMMANDS`.
548
+ */
549
+ export function runShowCheckpoint(
550
+ context: BuilderCommandContext,
551
+ options: { method: string; journey?: string | null; sessionId?: string | null },
552
+ ): CommandResult {
553
+ const unknown = rejectUnknownMethod(options.method);
554
+ if (unknown) return unknown;
555
+
556
+ const resolved = resolveBuilderJourney(context, {
557
+ journey: options.journey ?? null,
558
+ sessionId: options.sessionId ?? null,
559
+ action: "checkpoint display",
560
+ });
561
+ if (isCommandResult(resolved)) return resolved;
562
+ const journey = resolved.journey;
563
+
564
+ const missing = requireJourney(context.db, journey);
565
+ if (missing) return missing;
566
+ const notAdopted = requireAdoptedMethod(context.db, journey, options.method);
567
+ if (notAdopted) return notAdopted;
568
+
569
+ const cursor = getDeliveryCursor(context.db, journey);
570
+ const projectPath = getProjectPath(context.db, journey);
571
+ const plan = cursor?.activeItem ? closureArtifactPath(projectPath, cursor, "plan.md") : null;
572
+ const records =
573
+ plan === null
574
+ ? null
575
+ : {
576
+ folder: displayPath(dirname(plan), projectPath),
577
+ artifacts: judgeStoryFiles(dirname(plan)),
578
+ present: new Set(CLOSURE_RECORDS.filter((name) => existsSync(join(dirname(plan), name)))),
579
+ };
580
+ return {
581
+ stdout: printed(renderActiveCheckpoint({ journey, cursor, records })),
582
+ stderr: "",
583
+ exitCode: 0,
584
+ };
585
+ }
586
+
587
+ // --- plateau 3: the story-lifecycle leaves ---------------------------------
588
+ //
589
+ // Port of `cmd_pull_item`, `cmd_prepare_item`, `cmd_plan_item`, `cmd_approve_plan`,
590
+ // and `cmd_cancel_story_plan_preauthorization`.
591
+ //
592
+ // These are the first leaves whose guard chain has a FIFTH link: after method,
593
+ // journey resolution, journey existence, and adoption comes
594
+ // `_require_delivery_cursor`. Its position matters — two refusals that look like
595
+ // they belong to the lifecycle (`active item is required before prepare`, the
596
+ // Delivery Story project-path error) sit BEHIND it, so a port that checks the
597
+ // cursor last reports the wrong refusal for a journey that never ran `sync-cursor`.
598
+ //
599
+ // `pull-item` is also the only leaf here that composes: it runs Pull, then Prepare
600
+ // unconditionally, then for a Delivery Story it runs Expand and renders the
601
+ // composite `DELIVERY_STORY_READY` surface INSTEAD of the Pull and Prepare
602
+ // surfaces. Emitting all three is a duplicate-surface defect no renderer-level test
603
+ // can see.
604
+
605
+ /**
606
+ * The journey's scoped candidate view (CR002), from its persisted cursor. After a
607
+ * Done write the closure verbs carry the active item forward, so the scope is the
608
+ * CV of the item that just closed and "what looks next" stays inside it.
609
+ */
610
+ function journeyCandidateView(
611
+ db: Database,
612
+ journey: string,
613
+ projectPath: string | null,
614
+ method: string,
615
+ ): ScopedPullCandidates {
616
+ const scope = resolveRoadmapScope(projectPath || null, getDeliveryCursor(db, journey));
617
+ return scopePullCandidates(inspectPullCandidates(projectPath, { journey, method }), scope);
618
+ }
619
+
620
+ /** Python `_require_delivery_cursor`. */
621
+ function requireDeliveryCursor(db: Database, journey: string): CommandResult | null {
622
+ if (getDeliveryCursor(db, journey) !== null) return null;
623
+ return refuse(
624
+ `Error: journey '${journey}' has no Builder delivery cursor. ` +
625
+ `Run: ${PROGRAM} build sync-cursor --journey ${shellWord(journey)} --method ariad`,
626
+ );
627
+ }
628
+
629
+ /** The four guards every lifecycle leaf runs, in Python's order. */
630
+ function lifecycleGuards(
631
+ context: BuilderCommandContext,
632
+ options: {
633
+ method: string;
634
+ journey?: string | null;
635
+ sessionId?: string | null;
636
+ action: string;
637
+ requireCursor: boolean;
638
+ },
639
+ ): { journey: string } | CommandResult {
640
+ const unknown = rejectUnknownMethod(options.method);
641
+ if (unknown) return unknown;
642
+
643
+ const resolved = resolveBuilderJourney(context, {
644
+ journey: options.journey ?? null,
645
+ sessionId: options.sessionId ?? null,
646
+ action: options.action,
647
+ });
648
+ if (isCommandResult(resolved)) return resolved;
649
+ const journey = resolved.journey;
650
+
651
+ const missing = requireJourney(context.db, journey);
652
+ if (missing) return missing;
653
+ const notAdopted = requireAdoptedMethod(context.db, journey, options.method);
654
+ if (notAdopted) return notAdopted;
655
+ if (options.requireCursor) {
656
+ const noCursor = requireDeliveryCursor(context.db, journey);
657
+ if (noCursor) return noCursor;
658
+ }
659
+ return { journey };
660
+ }
661
+
662
+ /**
663
+ * Python's `except ValueError as exc: print(f"Error: {exc}", file=sys.stderr)`.
664
+ *
665
+ * The two roadmap errors pass through: Pull renders both as `EXPAND_BLOCKED`, and
666
+ * anywhere else the dispatcher answers a double claim with its own `Error:` line
667
+ * (CR018).
668
+ */
669
+ function refuseValueError(error: unknown): CommandResult {
670
+ if (error instanceof StoryPackageAmbiguityError || error instanceof ExpandBlockedError)
671
+ throw error;
672
+ return refuse(`Error: ${(error as Error).message}`);
673
+ }
674
+
675
+ /** Python `_canonical_package_path`: resolve the authored package, else create it. */
676
+ function canonicalPackagePath(
677
+ projectPath: string | null,
678
+ cursor: { activeItem: string | null; activeItemTitle: string | null } | null,
679
+ ): string | null {
680
+ const activeItem = cursor?.activeItem ?? null;
681
+ if (!projectPath || !activeItem) return null;
682
+ const resolved = resolveStoryDirectory(projectPath, activeItem);
683
+ if (resolved !== null) return resolved;
684
+ return createStoryDirectory(projectPath, activeItem, cursor?.activeItemTitle || activeItem);
685
+ }
686
+
687
+ /** Python `_artifact_existence`: sampled BEFORE the write, or preservation is invisible. */
688
+ function artifactExistence(planPath: string | null): Map<string, boolean> {
689
+ const existence = new Map<string, boolean>();
690
+ if (planPath === null) return existence;
691
+ const directory = dirname(planPath);
692
+ for (const path of [join(directory, "index.md"), planPath, join(directory, "test-guide.md")]) {
693
+ existence.set(path, existsSync(path));
694
+ }
695
+ return existence;
696
+ }
697
+
698
+ /** The surface's last word on a closure record, true to what the writer did (CR079). */
699
+ function closureBoundary(label: string, outcome: ArtifactOutcome | null | undefined): string {
700
+ return outcome === "preserved"
701
+ ? `${label} artifact was preserved as it is: Ariad did not write it, or it was edited since. ` +
702
+ "This checkpoint's fields are above, not in the file."
703
+ : `${label} artifact was materialized.`;
704
+ }
705
+
706
+ /** Python `_print_artifacts_materialized`: no artifacts means NO surface, not an empty one. */
707
+ function artifactsSurface(options: {
708
+ context: string;
709
+ artifacts: readonly MaterializedArtifact[];
710
+ projectPath: string | null;
711
+ journey: string;
712
+ boundary: string;
713
+ }): string {
714
+ if (options.artifacts.length === 0) return "";
715
+ return printed(
716
+ renderArtifactsMaterializedSurface({
717
+ context: options.context,
718
+ artifacts: options.artifacts,
719
+ projectPath: options.projectPath,
720
+ journey: options.journey,
721
+ boundary: options.boundary,
722
+ }),
723
+ );
724
+ }
725
+
726
+ /**
727
+ * Python `_roadmap_plan_context`, reduced to the two facts only the roadmap holds
728
+ * (CR111 D3): the active candidate's whole title (CR018), and its SIBLINGS' titles,
729
+ * which Plan names as Non-Goals. Siblings are the other children of the same parent,
730
+ * never an ancestor or a cousin (CR019). The sentences are the scaffold model's, filled
731
+ * by `planLifecycleItem`; without a roadmap title it takes the title Pull recorded (D5).
732
+ */
733
+ function roadmapPlanFacts(
734
+ projectPath: string | null,
735
+ cursor: { activeItem: string | null } | null,
736
+ ): { title: string | null; siblings: string[] } {
737
+ const activeItem = cursor?.activeItem ?? null;
738
+ if (!projectPath || !activeItem) return { title: null, siblings: [] };
739
+ const candidates = inspectPullCandidates(projectPath, {
740
+ journey: "",
741
+ method: "ariad",
742
+ }).candidates;
743
+ const active = candidates.find((candidate) => candidate.code === activeItem);
744
+ if (!active) return { title: null, siblings: [] };
745
+ return {
746
+ title: active.title || null,
747
+ siblings: siblingsOf(candidates, activeItem).map((candidate) => candidate.title),
748
+ };
749
+ }
750
+
751
+ /** Python `cmd_pull_item`. */
752
+ export function runPullItem(
753
+ context: BuilderWriteContext,
754
+ options: {
755
+ method: string;
756
+ journey?: string | null;
757
+ sessionId?: string | null;
758
+ itemCode: string;
759
+ itemTitle: string;
760
+ itemLevel: string;
761
+ whyNow: string;
762
+ },
763
+ ): CommandResult {
764
+ const guarded = lifecycleGuards(context, { ...options, action: "pull", requireCursor: true });
765
+ if (isCommandResult(guarded)) return guarded;
766
+ const journey = guarded.journey;
767
+
768
+ let pullReport: ReturnType<typeof pullLifecycleItem>;
769
+ try {
770
+ pullReport = pullLifecycleItem(
771
+ context.db,
772
+ {
773
+ journey,
774
+ method: options.method,
775
+ item: {
776
+ code: options.itemCode,
777
+ title: options.itemTitle,
778
+ level: options.itemLevel,
779
+ whyNow: options.whyNow,
780
+ },
781
+ },
782
+ context.deps,
783
+ );
784
+ } catch (error) {
785
+ return refuseValueError(error);
786
+ }
787
+
788
+ const projectPath = getProjectPath(context.db, journey);
789
+ // Prepare runs unconditionally, and OUTSIDE the try above: Python lets a Prepare
790
+ // failure raise rather than reporting it as a Pull refusal.
791
+ const prepareReport = prepareLifecycleItem(
792
+ context.db,
793
+ { journey, method: options.method, projectPath },
794
+ context.deps,
795
+ );
796
+
797
+ if (options.itemLevel === "delivery_story") {
798
+ if (!projectPath) {
799
+ return refuse("Error: Delivery Story expansion requires project_path.");
800
+ }
801
+ let expandReport: ReturnType<typeof expandDeliveryStory>;
802
+ try {
803
+ expandReport = expandDeliveryStory(
804
+ context.db,
805
+ { journey, method: options.method, projectPath },
806
+ context.deps,
807
+ );
808
+ } catch (error) {
809
+ if (error instanceof ExpandBlockedError || error instanceof StoryPackageAmbiguityError) {
810
+ // The refusal renders a SURFACE on stdout and still exits 1.
811
+ return {
812
+ stdout: printed(renderExpandBlocked(options.itemCode, error.message)),
813
+ stderr: "",
814
+ exitCode: 1,
815
+ };
816
+ }
817
+ return refuseValueError(error);
818
+ }
819
+ const stdout =
820
+ printed(
821
+ renderDeliveryStoryReadyReport({
822
+ pull: pullReport,
823
+ prepare: prepareReport,
824
+ expand: expandReport,
825
+ cvTitle: placementCvTitle(projectPath, pullReport.cursor),
826
+ }),
827
+ ) +
828
+ artifactsSurface({
829
+ context: `Expand — ${expandReport.deliveryStory}`,
830
+ artifacts: expandReport.materializedArtifacts,
831
+ projectPath,
832
+ journey,
833
+ boundary: "Files were materialized only. No Plan or implementation was executed.",
834
+ });
835
+ return { stdout, stderr: "", exitCode: 0 };
836
+ }
837
+
838
+ return {
839
+ stdout:
840
+ printed(renderPullReport(pullReport, pullPlacement(projectPath, pullReport.cursor))) +
841
+ printed(renderPrepareReport(prepareReport)),
842
+ stderr: "",
843
+ exitCode: 0,
844
+ };
845
+ }
846
+
847
+ /** Python `cmd_prepare_item`. */
848
+ export function runPrepareItem(
849
+ context: BuilderWriteContext,
850
+ options: { method: string; journey?: string | null; sessionId?: string | null },
851
+ ): CommandResult {
852
+ const guarded = lifecycleGuards(context, { ...options, action: "prepare", requireCursor: true });
853
+ if (isCommandResult(guarded)) return guarded;
854
+ const journey = guarded.journey;
855
+
856
+ const projectPath = getProjectPath(context.db, journey);
857
+ try {
858
+ const report = prepareLifecycleItem(
859
+ context.db,
860
+ { journey, method: options.method, projectPath },
861
+ context.deps,
862
+ );
863
+ return { stdout: printed(renderPrepareReport(report)), stderr: "", exitCode: 0 };
864
+ } catch (error) {
865
+ // CR114 (D3): a Prepare past its place is refused where the cursor stands.
866
+ if (error instanceof LifecycleRefusal) {
867
+ return refusedSurface(context.db, journey, "prepare-item", error);
868
+ }
869
+ return refuseValueError(error);
870
+ }
871
+ }
872
+
873
+ /** Python `cmd_plan_item`. */
874
+ export function runPlanItem(
875
+ context: BuilderWriteContext,
876
+ options: {
877
+ method: string;
878
+ journey?: string | null;
879
+ sessionId?: string | null;
880
+ objective?: string | null;
881
+ preauthorizeApproval?: boolean;
882
+ stopAfter?: string;
883
+ },
884
+ ): CommandResult {
885
+ const guarded = lifecycleGuards(context, { ...options, action: "plan", requireCursor: true });
886
+ if (isCommandResult(guarded)) return guarded;
887
+ const journey = guarded.journey;
888
+
889
+ const projectPath = getProjectPath(context.db, journey);
890
+ const cursor = getDeliveryCursor(context.db, journey);
891
+ const planPath = canonicalPackagePath(projectPath, cursor);
892
+ const planArtifactPath = planPath === null ? null : join(planPath, "plan.md");
893
+ const existedBefore = artifactExistence(planArtifactPath);
894
+ const roadmap = roadmapPlanFacts(projectPath, cursor);
895
+
896
+ try {
897
+ const report = planLifecycleItem(
898
+ context.db,
899
+ {
900
+ journey,
901
+ method: getAriadMethod(),
902
+ objective: options.objective || null,
903
+ title: roadmap.title,
904
+ siblings: roadmap.siblings,
905
+ localRules: projectContractRules(projectPath),
906
+ planArtifactPath,
907
+ projectRoot: projectPath,
908
+ preauthorize: options.preauthorizeApproval ?? false,
909
+ stopBoundary: options.stopAfter ?? "navigator_validation",
910
+ },
911
+ context.deps,
912
+ );
913
+ const authority = report.preauthorizationRecorded
914
+ ? printed(renderStoryPlanPreauthorizationRecorded(report.cursor))
915
+ : "";
916
+ const stdout =
917
+ printed(renderPlanCheckpoint(report)) +
918
+ authority +
919
+ artifactsSurface({
920
+ context: `Plan — ${report.activeItem}`,
921
+ artifacts: planPackageArtifacts(report.planArtifactPath, existedBefore),
922
+ projectPath,
923
+ journey,
924
+ boundary:
925
+ "Plan artifacts were materialized. Implementation remains blocked until approval.",
926
+ });
927
+ return { stdout, stderr: "", exitCode: 0 };
928
+ } catch (error) {
929
+ if (error instanceof LifecycleRefusal) {
930
+ return refusedSurface(context.db, journey, "plan-item", error);
931
+ }
932
+ return refuseValueError(error);
933
+ }
934
+ }
935
+
936
+ /**
937
+ * Python `cmd_approve_plan`.
938
+ *
939
+ * Note the guard chain: approval does NOT require a delivery cursor up front — the
940
+ * lifecycle call reports that itself — so a journey with no cursor reports
941
+ * `delivery cursor is required before plan approval`, not the `sync-cursor` hint.
942
+ */
943
+ export function runApprovePlan(
944
+ context: BuilderWriteContext,
945
+ options: {
946
+ method: string;
947
+ journey?: string | null;
948
+ sessionId?: string | null;
949
+ usePreauthorization?: boolean;
950
+ },
951
+ ): CommandResult {
952
+ const guarded = lifecycleGuards(context, {
953
+ ...options,
954
+ action: "plan approval",
955
+ requireCursor: false,
956
+ });
957
+ if (isCommandResult(guarded)) return guarded;
958
+ const journey = guarded.journey;
959
+
960
+ try {
961
+ if (options.usePreauthorization === true) {
962
+ const cursor = getDeliveryCursor(context.db, journey);
963
+ const projectPath = getProjectPath(context.db, journey);
964
+ const packagePath = canonicalPackagePath(projectPath, cursor);
965
+ const report = approveStoryPlanWithPreauthorization(
966
+ context.db,
967
+ {
968
+ journey,
969
+ method: options.method,
970
+ planArtifactPath: packagePath === null ? null : join(packagePath, "plan.md"),
971
+ },
972
+ context.deps,
973
+ );
974
+ if (report.status === "already_approved") {
975
+ return {
976
+ stdout: printed(renderStoryPreauthorizationAlreadyConsumed(report.cursor)),
977
+ stderr: "",
978
+ exitCode: 0,
979
+ };
980
+ }
981
+ return {
982
+ stdout:
983
+ printed(renderPlanApproval(report.cursor)) +
984
+ printed(renderStoryImplementationStarted(report.cursor)),
985
+ stderr: "",
986
+ exitCode: 0,
987
+ };
988
+ }
989
+ const projectPath = getProjectPath(context.db, journey);
990
+ const cursor = approvePlanCheckpoint(
991
+ context.db,
992
+ {
993
+ journey,
994
+ method: options.method,
995
+ planArtifactPath: closureArtifactPath(
996
+ projectPath,
997
+ getDeliveryCursor(context.db, journey),
998
+ "plan.md",
999
+ ),
1000
+ projectRoot: projectPath,
1001
+ },
1002
+ context.deps,
1003
+ );
1004
+ return { stdout: printed(renderPlanApproval(cursor)), stderr: "", exitCode: 0 };
1005
+ } catch (error) {
1006
+ if (error instanceof PlanPreauthorizationMismatch) {
1007
+ // Exit 0, not 1: a mismatch is a bounded fallback to ordinary approval, and
1008
+ // the surface says so. Treating it as a failure would tell the Navigator the
1009
+ // command broke.
1010
+ const cursor = getDeliveryCursor(context.db, journey);
1011
+ return {
1012
+ stdout: printed(
1013
+ renderStoryPlanPreauthorizationMismatch({
1014
+ activeItem: cursor?.activeItem ?? null,
1015
+ reason: error.reason,
1016
+ toAuthor: error.sections,
1017
+ }),
1018
+ ),
1019
+ stderr: "",
1020
+ exitCode: 0,
1021
+ };
1022
+ }
1023
+ if (error instanceof LifecycleRefusal) {
1024
+ return refusedSurface(context.db, journey, "approve-plan", error);
1025
+ }
1026
+ return refuseValueError(error);
1027
+ }
1028
+ }
1029
+
1030
+ /**
1031
+ * Python `cmd_cancel_story_plan_preauthorization`.
1032
+ *
1033
+ * The one leaf that skips the journey-existence check: Python's version never calls
1034
+ * `get_identity`, so a nonexistent journey reaches the adoption guard instead.
1035
+ */
1036
+ export function runCancelPlanPreauthorization(
1037
+ context: BuilderWriteContext,
1038
+ options: { method: string; journey?: string | null; sessionId?: string | null },
1039
+ ): CommandResult {
1040
+ const unknown = rejectUnknownMethod(options.method);
1041
+ if (unknown) return unknown;
1042
+ const resolved = resolveBuilderJourney(context, {
1043
+ journey: options.journey ?? null,
1044
+ sessionId: options.sessionId ?? null,
1045
+ action: "story Plan preauthorization cancellation",
1046
+ });
1047
+ if (isCommandResult(resolved)) return resolved;
1048
+ const journey = resolved.journey;
1049
+ const notAdopted = requireAdoptedMethod(context.db, journey, options.method);
1050
+ if (notAdopted) return notAdopted;
1051
+
1052
+ try {
1053
+ const cursor = cancelStoryPlanPreauthorization(
1054
+ context.db,
1055
+ { journey, method: options.method },
1056
+ context.deps,
1057
+ );
1058
+ return {
1059
+ stdout: printed(
1060
+ renderStoryPlanPreauthorizationMismatch({
1061
+ activeItem: cursor.activeItem,
1062
+ reason: "navigator_cancelled",
1063
+ }),
1064
+ ),
1065
+ stderr: "",
1066
+ exitCode: 0,
1067
+ };
1068
+ } catch (error) {
1069
+ return refuseValueError(error);
1070
+ }
1071
+ }
1072
+
1073
+ // --- plateau 4: the closure leaves -----------------------------------------
1074
+ //
1075
+ // Port of `cmd_validate_item`, `cmd_review_item`, `cmd_coherence_item`, and
1076
+ // `cmd_done_item`.
1077
+ //
1078
+ // Their refusal shape differs from every other lifecycle leaf: a blocked lifecycle
1079
+ // call renders IMPLEMENTATION_GUARD on STDOUT and exits 1, rather than printing
1080
+ // `Error: …` to stderr. Routing these through `refuse` would lose a surface the
1081
+ // transport protocol requires to be rendered verbatim — the same trap
1082
+ // `check-implementation` set at plateau 2.
1083
+ //
1084
+ // Two of them emit a SECOND, CLI-only surface on the complete path:
1085
+ // `debt_review_started` after a passed Validation, and `done_closure_confirmation`
1086
+ // after a `no_action` Debt Review with nothing missing. Those two exist only here, not
1087
+ // in `lifecycle.py`, so the module-level corpus cannot see them.
1088
+ //
1089
+ // `done-item` additionally prints the project position after its checkpoint, which is
1090
+ // the Navigator's "where are we now" view and reads the roadmap a second time.
1091
+
1092
+ /** Python `_mini_card_text`. */
1093
+ function miniCardText(text: string): string {
1094
+ return cardText(text);
1095
+ }
1096
+
1097
+ /**
1098
+ * Python `_mini_card_wrapped`.
1099
+ *
1100
+ * The NON-chunking wrapper: an over-long word overflows its row instead of being
1101
+ * split, unlike `cardWrapped`. Python keeps two `_wrap_plain_text` behaviors across
1102
+ * ten modules and this is one of the non-chunking ones, so the flag is explicit.
1103
+ */
1104
+ function miniCardWrapped(text: string): string[] {
1105
+ return wrapPlainText(text, { width: CARD_WIDTH, chunkLongWords: false }).map(cardText);
1106
+ }
1107
+
1108
+ /** Python `_render_debt_review_handoff`: a CLI-only surface with its own ribbon literal. */
1109
+ // --- cadence, release intent, continuation (plateau 6) ----------------------
1110
+
1111
+ /** Python `cmd_set_cadence`'s allowed set, checked in the CLI rather than the DSL. */
1112
+ const CADENCE_PROFILES = ["stepwise", "checkpoint", "accelerated", "autonomous"];
1113
+
1114
+ /**
1115
+ * Python `cmd_set_cadence`.
1116
+ *
1117
+ * No module: the whole leaf is the CLI function, and its guard ORDER is unlike
1118
+ * every other leaf here — method, then the PROFILE, then the autonomous-limits
1119
+ * rule, and only then journey resolution and adoption. So an unknown profile
1120
+ * against a journey that cannot be resolved reports the profile, not the journey.
1121
+ * The corpus pins that with `set_cadence_profile_guard_precedes_journey`.
1122
+ *
1123
+ * It also writes the cursor directly rather than through a lifecycle verb, which
1124
+ * is why it carries every other field forward by hand: cadence is a Navigator
1125
+ * preference, not a lifecycle transition.
1126
+ */
1127
+ export function runSetCadence(
1128
+ context: BuilderWriteContext,
1129
+ options: {
1130
+ method: string;
1131
+ journey?: string | null;
1132
+ sessionId?: string | null;
1133
+ profile?: string | null;
1134
+ limits?: readonly string[];
1135
+ },
1136
+ ): CommandResult {
1137
+ const unknown = rejectUnknownMethod(options.method);
1138
+ if (unknown) return unknown;
1139
+ const profile = options.profile ?? "";
1140
+ const limits = options.limits ?? [];
1141
+ if (!CADENCE_PROFILES.includes(profile)) {
1142
+ return refuse(
1143
+ "Error: cadence profile must be one of stepwise, checkpoint, accelerated, autonomous",
1144
+ );
1145
+ }
1146
+ if (profile === "autonomous" && limits.length === 0) {
1147
+ return refuse("Error: autonomous cadence requires at least one --limit");
1148
+ }
1149
+ const resolved = resolveBuilderJourney(context, {
1150
+ journey: options.journey ?? null,
1151
+ sessionId: options.sessionId ?? null,
1152
+ action: "cadence profile update",
1153
+ });
1154
+ if (isCommandResult(resolved)) return resolved;
1155
+ const journey = resolved.journey;
1156
+ const notAdopted = requireAdoptedMethod(context.db, journey, options.method);
1157
+ if (notAdopted) return notAdopted;
1158
+ const cursor = getDeliveryCursor(context.db, journey);
1159
+ if (cursor === null) {
1160
+ // Its own message, not `_require_delivery_cursor`'s.
1161
+ return refuse(`Error: journey '${journey}' has no Builder delivery cursor.`);
1162
+ }
1163
+ const updated = setDeliveryCursor(
1164
+ context.db,
1165
+ {
1166
+ journey,
1167
+ method: options.method,
1168
+ activeItem: cursor.activeItem,
1169
+ activeItemTitle: cursor.activeItemTitle,
1170
+ activeItemLevel: cursor.activeItemLevel,
1171
+ activeCheckpoint: cursor.activeCheckpoint,
1172
+ pendingConfirmation: cursor.pendingConfirmation,
1173
+ lastDeliveryEvent: cursor.lastDeliveryEvent,
1174
+ cadenceProfile: profile,
1175
+ cadenceLimits: limits,
1176
+ granularityDecision: cursor.granularityDecision,
1177
+ navigatorFlowUnit: cursor.navigatorFlowUnit,
1178
+ childWorkItems: cursor.childWorkItems,
1179
+ aggregateCheckpointStatus: cursor.aggregateCheckpointStatus,
1180
+ },
1181
+ context.deps,
1182
+ );
1183
+ return { stdout: printed(renderDeliveryCursorSyncReport(updated)), stderr: "", exitCode: 0 };
1184
+ }
1185
+
1186
+ /** Python `cmd_release_intent`: set with `--intent`, inspect without it. */
1187
+ export function runReleaseIntent(
1188
+ context: BuilderWriteContext,
1189
+ options: {
1190
+ method: string;
1191
+ journey?: string | null;
1192
+ sessionId?: string | null;
1193
+ intent?: string | null;
1194
+ },
1195
+ ): CommandResult {
1196
+ const guarded = lifecycleGuards(context, {
1197
+ ...options,
1198
+ action: "Delivery Story release intent",
1199
+ requireCursor: false,
1200
+ });
1201
+ if (isCommandResult(guarded)) return guarded;
1202
+ const intent = options.intent ?? null;
1203
+ try {
1204
+ const report =
1205
+ intent === null
1206
+ ? inspectReleaseIntent(context.db, { journey: guarded.journey, method: options.method })
1207
+ : setReleaseIntent(
1208
+ context.db,
1209
+ { journey: guarded.journey, method: options.method, intent },
1210
+ context.deps,
1211
+ );
1212
+ return { stdout: printed(renderReleaseIntentReport(report)), stderr: "", exitCode: 0 };
1213
+ } catch (error) {
1214
+ return refuseValueError(error);
1215
+ }
1216
+ }
1217
+
1218
+ /**
1219
+ * Python `cmd_continue_lifecycle`.
1220
+ *
1221
+ * Also CLI-only, and narrower than its name suggests: the ONLY continuation it can
1222
+ * perform is crossing Done from a completed Debt Review or Coherence. Everything
1223
+ * else is one of five refusals, each rendering IMPLEMENTATION_GUARD on stdout with
1224
+ * exit 1 rather than an `Error:` line on stderr.
1225
+ *
1226
+ * Two shapes reproduced deliberately. It accepts `--process`, `--project`,
1227
+ * `--product`, and `--difference` and IGNORES all four — it never runs Coherence,
1228
+ * despite taking Coherence's evidence. And unlike `done-item`, which prints the
1229
+ * roadmap snapshot after closing, it prints its checkpoint and stops. Both are
1230
+ * parity-bound; both have cases.
1231
+ */
1232
+ export function runContinueLifecycle(
1233
+ context: BuilderWriteContext,
1234
+ options: {
1235
+ method: string;
1236
+ journey?: string | null;
1237
+ sessionId?: string | null;
1238
+ historyAction?: string | null;
1239
+ roadmapUpdate?: string | null;
1240
+ nextRecommendation?: string | null;
1241
+ },
1242
+ ): CommandResult {
1243
+ const guarded = lifecycleGuards(context, {
1244
+ ...options,
1245
+ action: "lifecycle continuation",
1246
+ requireCursor: true,
1247
+ });
1248
+ if (isCommandResult(guarded)) return guarded;
1249
+ const journey = guarded.journey;
1250
+ const cursor = getDeliveryCursor(context.db, journey);
1251
+ if (cursor === null) {
1252
+ return refuse("Error: delivery cursor is required before continuation");
1253
+ }
1254
+ const blocked = (reason: string): CommandResult => ({
1255
+ stdout: printed(
1256
+ renderCheckpointRefused({ request: "continue-lifecycle", reason, journey, cursor }),
1257
+ ),
1258
+ stderr: "",
1259
+ exitCode: 1,
1260
+ });
1261
+ const profile = effectiveCadenceProfile(cursor);
1262
+ if (profile === "stepwise") {
1263
+ return blocked("Stepwise cadence does not continue automatically.");
1264
+ }
1265
+ if (profile === "autonomous" && cursor.cadenceLimits.length === 0) {
1266
+ return blocked("Autonomous cadence requires explicit limits.");
1267
+ }
1268
+ if (cursor.pendingConfirmation) {
1269
+ return blocked(`Continuation is blocked: pending confirmation ${cursor.pendingConfirmation}.`);
1270
+ }
1271
+ const projectPath = getProjectPath(context.db, journey);
1272
+ const planPath = closureArtifactPath(projectPath, cursor, "plan.md");
1273
+ if (!["review_complete", "coherence_complete"].includes(cursor.lastDeliveryEvent ?? "")) {
1274
+ return blocked(
1275
+ `No bypassable continuation is available after ${cursor.lastDeliveryEvent || "none"}.`,
1276
+ );
1277
+ }
1278
+ if (!(options.historyAction && options.roadmapUpdate && options.nextRecommendation)) {
1279
+ return blocked("Done requires history, roadmap, and next-step evidence.");
1280
+ }
1281
+ // The same Done as `done-item`, with the same record to read (CR112): a cadence that
1282
+ // continues on its own must not close a story `done-item` would refuse.
1283
+ try {
1284
+ const report = doneLifecycleItem(
1285
+ context.db,
1286
+ {
1287
+ journey,
1288
+ method: getAriadMethod(),
1289
+ historyAction: options.historyAction,
1290
+ roadmapUpdate: options.roadmapUpdate,
1291
+ nextRecommendation: options.nextRecommendation,
1292
+ doneArtifactPath: planPath === null ? null : join(dirname(planPath), "done.md"),
1293
+ indexArtifactPath: planPath === null ? null : join(dirname(planPath), "index.md"),
1294
+ projectRoot: projectPath,
1295
+ },
1296
+ context.deps,
1297
+ );
1298
+ return { stdout: printed(renderDoneCheckpoint(report)), stderr: "", exitCode: 0 };
1299
+ } catch (error) {
1300
+ if (error instanceof LifecycleRefusal) return blocked(error.message);
1301
+ throw error;
1302
+ }
1303
+ }
1304
+
1305
+ // --- Delivery Story leaves (plateau 5) --------------------------------------
1306
+ //
1307
+ // The aggregate leaves differ from their story-level twins in three CLI-visible
1308
+ // ways, all reproduced here:
1309
+ //
1310
+ // * no CLI cursor guard — `lifecycleGuards` runs with `requireCursor: false`,
1311
+ // so a missing cursor surfaces the MODULE's message, not the CLI's;
1312
+ // * refusals are ordinary stderr lines (`Error: …`, exit 1), never the
1313
+ // IMPLEMENTATION_GUARD surface the story closure leaves print on stdout;
1314
+ // * `approve-delivery-story-plan` prints the mismatch surface and exits **0**,
1315
+ // because a refused authority is a normal Navigator-facing outcome rather than
1316
+ // a failure — the ordinary Plan gate is still there.
1317
+
1318
+ /** Python `cmd_set_flow_unit`: inspect with no `--unit`, select with one. */
1319
+ export function runSetFlowUnit(
1320
+ context: BuilderWriteContext,
1321
+ options: {
1322
+ method: string;
1323
+ journey?: string | null;
1324
+ sessionId?: string | null;
1325
+ unit?: string | null;
1326
+ },
1327
+ ): CommandResult {
1328
+ const guarded = lifecycleGuards(context, {
1329
+ ...options,
1330
+ action: "navigator flow unit",
1331
+ requireCursor: false,
1332
+ });
1333
+ if (isCommandResult(guarded)) return guarded;
1334
+ const unit = options.unit ?? null;
1335
+ try {
1336
+ if (unit === null) {
1337
+ const report = inspectNavigatorFlowUnit(context.db, {
1338
+ journey: guarded.journey,
1339
+ method: options.method,
1340
+ });
1341
+ return { stdout: printed(renderNavigatorFlowUnitReport(report)), stderr: "", exitCode: 0 };
1342
+ }
1343
+ const report = setNavigatorFlowUnit(
1344
+ context.db,
1345
+ { journey: guarded.journey, method: options.method, flowUnit: unit },
1346
+ context.deps,
1347
+ );
1348
+ return {
1349
+ stdout: printed(renderFlowUnitScopeConfirmationReport(report)),
1350
+ stderr: "",
1351
+ exitCode: 0,
1352
+ };
1353
+ } catch (error) {
1354
+ return refuseValueError(error);
1355
+ }
1356
+ }
1357
+
1358
+ /** Python `cmd_plan_delivery_story`. */
1359
+ export function runPlanDeliveryStory(
1360
+ context: BuilderWriteContext,
1361
+ options: {
1362
+ method: string;
1363
+ journey?: string | null;
1364
+ sessionId?: string | null;
1365
+ objective?: string | null;
1366
+ children?: readonly string[];
1367
+ preauthorizeApproval?: boolean;
1368
+ stopAfter?: string;
1369
+ },
1370
+ ): CommandResult {
1371
+ const guarded = lifecycleGuards(context, {
1372
+ ...options,
1373
+ action: "Delivery Story Plan",
1374
+ requireCursor: false,
1375
+ });
1376
+ if (isCommandResult(guarded)) return guarded;
1377
+ const journey = guarded.journey;
1378
+ const cursor = getDeliveryCursor(context.db, journey);
1379
+ const projectPath = getProjectPath(context.db, journey);
1380
+ const planPath = closureArtifactPath(projectPath, cursor, "plan.md");
1381
+ try {
1382
+ const report = planDeliveryStoryCheckpoint(
1383
+ context.db,
1384
+ {
1385
+ journey,
1386
+ method: options.method,
1387
+ objective: options.objective ?? "",
1388
+ childWorkItems: options.children ?? [],
1389
+ planArtifactPath: planPath,
1390
+ projectRoot: projectPath,
1391
+ preauthorize: options.preauthorizeApproval ?? false,
1392
+ stopBoundary: options.stopAfter ?? "navigator_validation",
1393
+ },
1394
+ context.deps,
1395
+ );
1396
+ const receipt = options.preauthorizeApproval
1397
+ ? printed(renderPlanPreauthorizationRecorded(report))
1398
+ : "";
1399
+ return {
1400
+ stdout:
1401
+ printed(renderDeliveryStoryPlanReport(report)) +
1402
+ receipt +
1403
+ artifactsSurface({
1404
+ context: `Delivery Story Plan — ${report.cursor.activeItem || "active item"}`,
1405
+ artifacts: report.materializedArtifacts,
1406
+ projectPath,
1407
+ journey,
1408
+ boundary:
1409
+ "Plan artifacts were materialized. Implementation remains blocked until approval.",
1410
+ }),
1411
+ stderr: "",
1412
+ exitCode: 0,
1413
+ };
1414
+ } catch (error) {
1415
+ return refuseValueError(error);
1416
+ }
1417
+ }
1418
+
1419
+ /** Python `cmd_approve_delivery_story_plan`. */
1420
+ export function runApproveDeliveryStoryPlan(
1421
+ context: BuilderWriteContext,
1422
+ options: {
1423
+ method: string;
1424
+ journey?: string | null;
1425
+ sessionId?: string | null;
1426
+ usePreauthorization?: boolean;
1427
+ },
1428
+ ): CommandResult {
1429
+ const guarded = lifecycleGuards(context, {
1430
+ ...options,
1431
+ action: "Delivery Story Plan approval",
1432
+ requireCursor: false,
1433
+ });
1434
+ if (isCommandResult(guarded)) return guarded;
1435
+ const journey = guarded.journey;
1436
+ const cursor = getDeliveryCursor(context.db, journey);
1437
+ const projectPath = getProjectPath(context.db, journey);
1438
+ try {
1439
+ const report = approveDeliveryStoryPlan(
1440
+ context.db,
1441
+ {
1442
+ journey,
1443
+ method: options.method,
1444
+ planArtifactPath: closureArtifactPath(projectPath, cursor, "plan.md"),
1445
+ projectRoot: projectPath,
1446
+ usePreauthorization: options.usePreauthorization ?? false,
1447
+ },
1448
+ context.deps,
1449
+ );
1450
+ // A repeat consumption returns after the card: no artifacts, no handoff.
1451
+ if (report.status === "already_approved") {
1452
+ return { stdout: printed(renderDeliveryStoryPlanReport(report)), stderr: "", exitCode: 0 };
1453
+ }
1454
+ const started = report.implementationStarted
1455
+ ? printed(renderDeliveryStoryImplementationStarted(report))
1456
+ : "";
1457
+ return {
1458
+ stdout:
1459
+ printed(renderDeliveryStoryPlanReport(report)) +
1460
+ artifactsSurface({
1461
+ context: `Delivery Story Plan Approval — ${report.cursor.activeItem || "active item"}`,
1462
+ artifacts: report.materializedArtifacts,
1463
+ projectPath,
1464
+ journey,
1465
+ boundary:
1466
+ "Plan approval artifacts were materialized. Implementation may proceed under the approved plan.",
1467
+ }) +
1468
+ started,
1469
+ stderr: "",
1470
+ exitCode: 0,
1471
+ };
1472
+ } catch (error) {
1473
+ if (error instanceof PlanPreauthorizationMismatch) {
1474
+ // Re-read: the mismatch path invalidated the receipt, so the card reports the
1475
+ // cursor as it stands now.
1476
+ const current = getDeliveryCursor(context.db, journey);
1477
+ return {
1478
+ stdout: printed(
1479
+ renderPlanPreauthorizationMismatch({
1480
+ activeItem: current?.activeItem ?? null,
1481
+ reason: error.reason,
1482
+ }),
1483
+ ),
1484
+ stderr: "",
1485
+ exitCode: 0,
1486
+ };
1487
+ }
1488
+ return refuseValueError(error);
1489
+ }
1490
+ }
1491
+
1492
+ /** Python `cmd_cancel_delivery_story_plan_preauthorization`. */
1493
+ export function runCancelDeliveryStoryPlanPreauthorization(
1494
+ context: BuilderWriteContext,
1495
+ options: { method: string; journey?: string | null; sessionId?: string | null },
1496
+ ): CommandResult {
1497
+ const guarded = lifecycleGuards(context, {
1498
+ ...options,
1499
+ action: "Delivery Story Plan preauthorization cancellation",
1500
+ requireCursor: false,
1501
+ });
1502
+ if (isCommandResult(guarded)) return guarded;
1503
+ try {
1504
+ const cursor = cancelDeliveryStoryPlanPreauthorization(
1505
+ context.db,
1506
+ { journey: guarded.journey, method: options.method },
1507
+ context.deps,
1508
+ );
1509
+ return {
1510
+ stdout: printed(
1511
+ renderPlanPreauthorizationMismatch({
1512
+ activeItem: cursor.activeItem,
1513
+ reason: "navigator_cancelled",
1514
+ }),
1515
+ ),
1516
+ stderr: "",
1517
+ exitCode: 0,
1518
+ };
1519
+ } catch (error) {
1520
+ return refuseValueError(error);
1521
+ }
1522
+ }
1523
+
1524
+ /** The four aggregate closure leaves share everything but their call and artifact. */
1525
+ function runDeliveryStoryClosure(
1526
+ context: BuilderWriteContext,
1527
+ options: { method: string; journey?: string | null; sessionId?: string | null },
1528
+ spec: {
1529
+ action: string;
1530
+ kind: string;
1531
+ filename: string;
1532
+ label: string;
1533
+ run: (
1534
+ journey: string,
1535
+ artifactPath: string | null,
1536
+ projectRoot: string | null,
1537
+ ) => DeliveryStoryClosureReport;
1538
+ preflight?: (journey: string, projectPath: string | null) => CommandResult | null;
1539
+ trailer?: (report: DeliveryStoryClosureReport, projectPath: string | null) => string;
1540
+ },
1541
+ ): CommandResult {
1542
+ const guarded = lifecycleGuards(context, {
1543
+ ...options,
1544
+ action: spec.action,
1545
+ requireCursor: false,
1546
+ });
1547
+ if (isCommandResult(guarded)) return guarded;
1548
+ const journey = guarded.journey;
1549
+ const projectPath = getProjectPath(context.db, journey);
1550
+ try {
1551
+ const refusal = spec.preflight?.(journey, projectPath) ?? null;
1552
+ if (refusal !== null) return refusal;
1553
+ const cursor = getDeliveryCursor(context.db, journey);
1554
+ const artifactPath = closureArtifactPath(projectPath, cursor, spec.filename);
1555
+ const report = spec.run(journey, artifactPath, projectPath);
1556
+ return {
1557
+ stdout:
1558
+ printed(renderDeliveryStoryClosureReport(report)) +
1559
+ artifactsSurface({
1560
+ context: `Delivery Story ${spec.label} — ${report.cursor.activeItem || "active item"}`,
1561
+ artifacts: closureArtifactManifest(spec.kind, artifactPath, report.artifactOutcome),
1562
+ projectPath,
1563
+ journey,
1564
+ boundary: closureBoundary(spec.label, report.artifactOutcome),
1565
+ }) +
1566
+ (spec.trailer?.(report, projectPath) ?? ""),
1567
+ stderr: "",
1568
+ exitCode: 0,
1569
+ };
1570
+ } catch (error) {
1571
+ return refuseValueError(error);
1572
+ }
1573
+ }
1574
+
1575
+ /** Python `cmd_validate_delivery_story`. */
1576
+ export function runValidateDeliveryStory(
1577
+ context: BuilderWriteContext,
1578
+ options: {
1579
+ method: string;
1580
+ journey?: string | null;
1581
+ sessionId?: string | null;
1582
+ summary?: string | null;
1583
+ navigatorAccepted?: boolean;
1584
+ },
1585
+ ): CommandResult {
1586
+ return runDeliveryStoryClosure(context, options, {
1587
+ action: "Delivery Story validation",
1588
+ kind: "validation",
1589
+ filename: "validation.md",
1590
+ label: "Validation",
1591
+ run: (journey, artifactPath, projectRoot) =>
1592
+ validateDeliveryStory(
1593
+ context.db,
1594
+ {
1595
+ journey,
1596
+ method: options.method,
1597
+ summary: options.summary ?? "",
1598
+ navigatorAccepted: options.navigatorAccepted ?? false,
1599
+ artifactPath,
1600
+ projectRoot,
1601
+ },
1602
+ context.deps,
1603
+ ),
1604
+ // Same CLI-only mini-card the story-level Validate prints, on the same
1605
+ // condition: a passed validation offers Debt Review.
1606
+ trailer: (report) =>
1607
+ report.status === "passed" ? printed(renderDebtReviewHandoff(report.cursor)) : "",
1608
+ });
1609
+ }
1610
+
1611
+ /** Python `cmd_review_delivery_story`. */
1612
+ export function runReviewDeliveryStory(
1613
+ context: BuilderWriteContext,
1614
+ options: {
1615
+ method: string;
1616
+ journey?: string | null;
1617
+ sessionId?: string | null;
1618
+ decision?: string | null;
1619
+ summary?: string | null;
1620
+ },
1621
+ ): CommandResult {
1622
+ return runDeliveryStoryClosure(context, options, {
1623
+ action: "Delivery Story debt review",
1624
+ kind: "review",
1625
+ filename: "review.md",
1626
+ label: "Review",
1627
+ run: (journey, artifactPath, projectRoot) =>
1628
+ reviewDeliveryStory(
1629
+ context.db,
1630
+ {
1631
+ journey,
1632
+ method: options.method,
1633
+ decision: options.decision ?? "",
1634
+ summary: options.summary ?? "",
1635
+ artifactPath,
1636
+ projectRoot,
1637
+ },
1638
+ context.deps,
1639
+ ),
1640
+ // Keyed off the REQUESTED decision, as Python is — not off the report.
1641
+ trailer: (report) =>
1642
+ options.decision === "no_action" ? printed(renderDoneClosureConfirmation(report.cursor)) : "",
1643
+ });
1644
+ }
1645
+
1646
+ /** Python `cmd_coherence_delivery_story`. */
1647
+ export function runCoherenceDeliveryStory(
1648
+ context: BuilderWriteContext,
1649
+ options: {
1650
+ method: string;
1651
+ journey?: string | null;
1652
+ sessionId?: string | null;
1653
+ summary?: string | null;
1654
+ },
1655
+ ): CommandResult {
1656
+ return runDeliveryStoryClosure(context, options, {
1657
+ action: "Delivery Story coherence",
1658
+ kind: "coherence",
1659
+ filename: "coherence.md",
1660
+ label: "Coherence",
1661
+ run: (journey, artifactPath, projectRoot) =>
1662
+ coherenceDeliveryStory(
1663
+ context.db,
1664
+ {
1665
+ journey,
1666
+ method: options.method,
1667
+ summary: options.summary ?? "",
1668
+ artifactPath,
1669
+ projectRoot,
1670
+ },
1671
+ context.deps,
1672
+ ),
1673
+ });
1674
+ }
1675
+
1676
+ /**
1677
+ * Python `cmd_done_delivery_story`.
1678
+ *
1679
+ * The preflight runs BEFORE any artifact path is derived or any cursor is written,
1680
+ * so a refused Done leaves the project exactly as it was. Its message is the `; `
1681
+ * join of the issues, which is why the preflight's ORDER is behavior.
1682
+ */
1683
+ export function runDoneDeliveryStory(
1684
+ context: BuilderWriteContext,
1685
+ options: {
1686
+ method: string;
1687
+ journey?: string | null;
1688
+ sessionId?: string | null;
1689
+ summary?: string | null;
1690
+ },
1691
+ ): CommandResult {
1692
+ return runDeliveryStoryClosure(context, options, {
1693
+ action: "Delivery Story Done",
1694
+ kind: "done",
1695
+ filename: "done.md",
1696
+ label: "Done",
1697
+ preflight: (journey, projectPath) => {
1698
+ if (!projectPath) {
1699
+ return refuse("Error: project path is required before Delivery Story Done");
1700
+ }
1701
+ const cursor = getDeliveryCursor(context.db, journey);
1702
+ if (cursor === null || !cursor.activeItem) {
1703
+ return refuse("Error: active Delivery Story is required before Done");
1704
+ }
1705
+ const authored = inspectAuthoredClosure(projectPath, {
1706
+ deliveryStory: cursor.activeItem,
1707
+ childWorkItems: cursor.childWorkItems,
1708
+ });
1709
+ if (!authored.ready) {
1710
+ return refuse(
1711
+ `Error: authored roadmap is not ready for Delivery Story Done: ${authored.issues.join("; ")}`,
1712
+ );
1713
+ }
1714
+ return null;
1715
+ },
1716
+ run: (journey, artifactPath, projectRoot) =>
1717
+ doneDeliveryStory(
1718
+ context.db,
1719
+ {
1720
+ journey,
1721
+ method: options.method,
1722
+ summary: options.summary ?? "",
1723
+ artifactPath,
1724
+ projectRoot,
1725
+ },
1726
+ context.deps,
1727
+ ),
1728
+ // Python `_print_roadmap_snapshot_at_done_end`: the roadmap is read AGAIN, after
1729
+ // the write, so the position reflects the Delivery Story that just closed.
1730
+ trailer: (report, projectPath) =>
1731
+ printed(
1732
+ renderProjectPositionReport(
1733
+ inspectRoadmapSnapshot(projectPath, { journey: report.journey, method: options.method }),
1734
+ {
1735
+ view: journeyCandidateView(context.db, report.journey, projectPath, options.method),
1736
+ justMoved: `🟩[${report.deliveryStory}] ${report.deliveryStoryTitle || "Delivery Story"} closed`,
1737
+ },
1738
+ ),
1739
+ ),
1740
+ });
1741
+ }
1742
+
1743
+ /**
1744
+ * The handoff after an accepted validation. Its ribbon was a fixed string, so it drew a
1745
+ * story's Expand as done where every other surface now draws it not applicable (CR113).
1746
+ */
1747
+ function renderDebtReviewHandoff(cursor: BuilderDeliveryCursor): string {
1748
+ const activeItem = cursor.activeItem;
1749
+ const body = `${[
1750
+ "Delivery",
1751
+ renderLifecycleRibbon("debt_review", cursor.activeItemLevel),
1752
+ "",
1753
+ "╭────────────────────────────────────────────────────────╮",
1754
+ "│ 🔎 DEBT REVIEW STARTED │",
1755
+ "│ │",
1756
+ miniCardText("What changed?"),
1757
+ ...miniCardWrapped(
1758
+ "Validation was accepted. Before closure, review whether any relevant technical debt remains.",
1759
+ ),
1760
+ "│ │",
1761
+ miniCardText("Active item"),
1762
+ miniCardText(activeItem || "active item"),
1763
+ "│ │",
1764
+ miniCardText("Navigator check"),
1765
+ // "no action needed" is the runtime's own word for `no_action`. The oracle had a
1766
+ // Portuguese phrase here, in an English sentence (CR090).
1767
+ ...miniCardWrapped(
1768
+ "If there is no relevant debt to address now, I can record this as no action needed and continue toward closure.",
1769
+ ),
1770
+ "╰────────────────────────────────────────────────────────╯",
1771
+ ].join("\n")}\n`;
1772
+ return wrapAriadSurface("debt_review_started", body);
1773
+ }
1774
+
1775
+ /** Python `_render_done_closure_confirmation`, its ribbon told the level (CR113). */
1776
+ function renderDoneClosureConfirmation(cursor: BuilderDeliveryCursor): string {
1777
+ const activeItem = cursor.activeItem;
1778
+ const body = `${[
1779
+ "Delivery",
1780
+ renderLifecycleRibbon("done", cursor.activeItemLevel),
1781
+ "",
1782
+ "╭────────────────────────────────────────────────────────╮",
1783
+ "│ 🧭 DONE CLOSURE CONFIRMATION │",
1784
+ "│ │",
1785
+ miniCardText("My understanding"),
1786
+ ...miniCardWrapped(
1787
+ "Debt review found no relevant action to take now. The story is ready to close.",
1788
+ ),
1789
+ "│ │",
1790
+ miniCardText("Active item"),
1791
+ miniCardText(activeItem || "active item"),
1792
+ "│ │",
1793
+ miniCardText("Before I close"),
1794
+ ...miniCardWrapped("Is there anything else to do in this story before Done?"),
1795
+ "╰────────────────────────────────────────────────────────╯",
1796
+ ].join("\n")}\n`;
1797
+ return wrapAriadSurface("done_closure_confirmation", body);
1798
+ }
1799
+
1800
+ /**
1801
+ * A refused lifecycle request, rendered where the cursor stands (CR067). Only a
1802
+ * `LifecycleRefusal` qualifies: it is raised before any write, which is what the
1803
+ * surface's boundary promises. A failure after the cursor write, such as a record the
1804
+ * artifact writer refuses or cannot write, keeps the plain `Error:` line instead.
1805
+ */
1806
+ function refusedSurface(
1807
+ db: Database,
1808
+ journey: string,
1809
+ request: string,
1810
+ refusal: LifecycleRefusal,
1811
+ ): CommandResult {
1812
+ const cursor = getDeliveryCursor(db, journey);
1813
+ const reason = refusal.message;
1814
+ return {
1815
+ stdout: printed(renderCheckpointRefused({ request, reason, journey, cursor })),
1816
+ stderr: "",
1817
+ exitCode: 1,
1818
+ };
1819
+ }
1820
+
1821
+ /** The closure leaves' shared artifact path: the Plan package's sibling file. */
1822
+ function closureArtifactPath(
1823
+ projectPath: string | null,
1824
+ cursor: { activeItem: string | null; activeItemTitle: string | null } | null,
1825
+ filename: string,
1826
+ ): string | null {
1827
+ const packagePath = canonicalPackagePath(projectPath, cursor);
1828
+ return packagePath === null ? null : join(packagePath, filename);
1829
+ }
1830
+
1831
+ /** Python `cmd_validate_item`. */
1832
+ export function runValidateItem(
1833
+ context: BuilderWriteContext,
1834
+ options: {
1835
+ method: string;
1836
+ journey?: string | null;
1837
+ sessionId?: string | null;
1838
+ checks?: readonly string[];
1839
+ checksStatus?: string;
1840
+ e2eDecision?: string;
1841
+ e2eEvidence?: string | null;
1842
+ navigatorRoute?: string | null;
1843
+ navigatorAccepted?: boolean;
1844
+ expectedObservation?: string | null;
1845
+ passCondition?: string | null;
1846
+ failCondition?: string | null;
1847
+ implementationComplete?: boolean;
1848
+ },
1849
+ ): CommandResult {
1850
+ const guarded = lifecycleGuards(context, {
1851
+ ...options,
1852
+ action: "validation",
1853
+ requireCursor: true,
1854
+ });
1855
+ if (isCommandResult(guarded)) return guarded;
1856
+ const journey = guarded.journey;
1857
+
1858
+ const cursor = getDeliveryCursor(context.db, journey);
1859
+ const projectPath = getProjectPath(context.db, journey);
1860
+ try {
1861
+ const report = validateLifecycleItem(
1862
+ context.db,
1863
+ {
1864
+ journey,
1865
+ method: getAriadMethod(),
1866
+ automatedChecks: options.checks ?? [],
1867
+ checksStatus: options.checksStatus ?? "not_run",
1868
+ e2eDecision: options.e2eDecision ?? "not_required",
1869
+ e2eEvidence: options.e2eEvidence ?? null,
1870
+ navigatorValidationRoute: options.navigatorRoute ?? null,
1871
+ navigatorAccepted: options.navigatorAccepted ?? false,
1872
+ expectedObservation: options.expectedObservation ?? null,
1873
+ passCondition: options.passCondition ?? null,
1874
+ failCondition: options.failCondition ?? null,
1875
+ implementationComplete: options.implementationComplete ?? false,
1876
+ validationArtifactPath: closureArtifactPath(projectPath, cursor, "validation.md"),
1877
+ projectRoot: projectPath,
1878
+ },
1879
+ context.deps,
1880
+ );
1881
+ const handoff =
1882
+ report.missingEvidence.length === 0 ? printed(renderDebtReviewHandoff(report.cursor)) : "";
1883
+ return {
1884
+ stdout: printed(renderValidationCheckpoint(report)) + handoff,
1885
+ stderr: "",
1886
+ exitCode: 0,
1887
+ };
1888
+ } catch (error) {
1889
+ if (error instanceof LifecycleRefusal) {
1890
+ return refusedSurface(context.db, journey, "validate-item", error);
1891
+ }
1892
+ return refuseValueError(error);
1893
+ }
1894
+ }
1895
+
1896
+ /** Python `cmd_review_item`. */
1897
+ export function runReviewItem(
1898
+ context: BuilderWriteContext,
1899
+ options: {
1900
+ method: string;
1901
+ journey?: string | null;
1902
+ sessionId?: string | null;
1903
+ debtFindings?: readonly string[];
1904
+ debtDecision?: string;
1905
+ deferReason?: string | null;
1906
+ revisitTrigger?: string | null;
1907
+ },
1908
+ ): CommandResult {
1909
+ const guarded = lifecycleGuards(context, {
1910
+ ...options,
1911
+ action: "debt review",
1912
+ requireCursor: true,
1913
+ });
1914
+ if (isCommandResult(guarded)) return guarded;
1915
+ const journey = guarded.journey;
1916
+
1917
+ const cursor = getDeliveryCursor(context.db, journey);
1918
+ const projectPath = getProjectPath(context.db, journey);
1919
+ try {
1920
+ const report = reviewLifecycleItem(
1921
+ context.db,
1922
+ {
1923
+ journey,
1924
+ method: getAriadMethod(),
1925
+ debtFindings: options.debtFindings ?? [],
1926
+ debtDecision: options.debtDecision ?? "pending",
1927
+ deferReason: options.deferReason ?? null,
1928
+ revisitTrigger: options.revisitTrigger ?? null,
1929
+ reviewArtifactPath: closureArtifactPath(projectPath, cursor, "review.md"),
1930
+ projectRoot: projectPath,
1931
+ },
1932
+ context.deps,
1933
+ );
1934
+ // Only `no_action` WITH nothing missing offers closure. A completed `defer` does
1935
+ // not, which is the distinction the second surface carries.
1936
+ const confirmation =
1937
+ report.debtDecision === "no_action" && report.missingDecision.length === 0
1938
+ ? printed(renderDoneClosureConfirmation(report.cursor))
1939
+ : "";
1940
+ return {
1941
+ stdout: printed(renderReviewCheckpoint(report)) + confirmation,
1942
+ stderr: "",
1943
+ exitCode: 0,
1944
+ };
1945
+ } catch (error) {
1946
+ if (error instanceof LifecycleRefusal) {
1947
+ return refusedSurface(context.db, journey, "review-item", error);
1948
+ }
1949
+ return refuseValueError(error);
1950
+ }
1951
+ }
1952
+
1953
+ /** Python `cmd_coherence_item`. */
1954
+ export function runCoherenceItem(
1955
+ context: BuilderWriteContext,
1956
+ options: {
1957
+ method: string;
1958
+ journey?: string | null;
1959
+ sessionId?: string | null;
1960
+ processAlignment?: string | null;
1961
+ projectAlignment?: string | null;
1962
+ productAlignment?: string | null;
1963
+ localDifferences?: readonly string[];
1964
+ },
1965
+ ): CommandResult {
1966
+ const guarded = lifecycleGuards(context, {
1967
+ ...options,
1968
+ action: "coherence",
1969
+ requireCursor: true,
1970
+ });
1971
+ if (isCommandResult(guarded)) return guarded;
1972
+ const journey = guarded.journey;
1973
+
1974
+ const cursor = getDeliveryCursor(context.db, journey);
1975
+ const projectPath = getProjectPath(context.db, journey);
1976
+ try {
1977
+ const report = coherenceLifecycleItem(
1978
+ context.db,
1979
+ {
1980
+ journey,
1981
+ method: getAriadMethod(),
1982
+ processAlignment: options.processAlignment ?? null,
1983
+ projectAlignment: options.projectAlignment ?? null,
1984
+ productAlignment: options.productAlignment ?? null,
1985
+ localDifferences: options.localDifferences ?? [],
1986
+ coherenceArtifactPath: closureArtifactPath(projectPath, cursor, "coherence.md"),
1987
+ projectRoot: projectPath,
1988
+ },
1989
+ context.deps,
1990
+ );
1991
+ return { stdout: printed(renderCoherenceCheckpoint(report)), stderr: "", exitCode: 0 };
1992
+ } catch (error) {
1993
+ if (error instanceof LifecycleRefusal) {
1994
+ return refusedSurface(context.db, journey, "coherence-item", error);
1995
+ }
1996
+ return refuseValueError(error);
1997
+ }
1998
+ }
1999
+
2000
+ /** Python `cmd_done_item`, including the project position it prints afterwards. */
2001
+ export function runDoneItem(
2002
+ context: BuilderWriteContext,
2003
+ options: {
2004
+ method: string;
2005
+ journey?: string | null;
2006
+ sessionId?: string | null;
2007
+ historyAction?: string | null;
2008
+ roadmapUpdate?: string | null;
2009
+ nextRecommendation?: string | null;
2010
+ },
2011
+ ): CommandResult {
2012
+ const guarded = lifecycleGuards(context, { ...options, action: "done", requireCursor: true });
2013
+ if (isCommandResult(guarded)) return guarded;
2014
+ const journey = guarded.journey;
2015
+
2016
+ const cursor = getDeliveryCursor(context.db, journey);
2017
+ const projectPath = getProjectPath(context.db, journey);
2018
+ try {
2019
+ const report = doneLifecycleItem(
2020
+ context.db,
2021
+ {
2022
+ journey,
2023
+ method: getAriadMethod(),
2024
+ historyAction: options.historyAction ?? null,
2025
+ roadmapUpdate: options.roadmapUpdate ?? null,
2026
+ nextRecommendation: options.nextRecommendation ?? null,
2027
+ doneArtifactPath: closureArtifactPath(projectPath, cursor, "done.md"),
2028
+ indexArtifactPath: closureArtifactPath(projectPath, cursor, "index.md"),
2029
+ projectRoot: projectPath,
2030
+ },
2031
+ context.deps,
2032
+ );
2033
+ // Python `_print_roadmap_snapshot_at_done_end`: the roadmap is read again, after
2034
+ // the write, so the position reflects the story that just closed.
2035
+ const position = renderProjectPositionReport(
2036
+ inspectRoadmapSnapshot(projectPath, { journey, method: options.method }),
2037
+ {
2038
+ view: journeyCandidateView(context.db, journey, projectPath, options.method),
2039
+ justMoved: `🟩[${report.activeItem}] ${report.activeItemTitle || "Story"} closed`,
2040
+ },
2041
+ );
2042
+ return {
2043
+ stdout: printed(renderDoneCheckpoint(report)) + printed(position),
2044
+ stderr: "",
2045
+ exitCode: 0,
2046
+ };
2047
+ } catch (error) {
2048
+ if (error instanceof LifecycleRefusal) {
2049
+ return refusedSurface(context.db, journey, "done-item", error);
2050
+ }
2051
+ return refuseValueError(error);
2052
+ }
2053
+ }