kortext 2.2.3 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +175 -65
  3. package/agents/architect.md +85 -0
  4. package/agents/compliance-expert.md +36 -75
  5. package/agents/copywriter.md +36 -92
  6. package/agents/db-admin.md +43 -91
  7. package/agents/designer.md +40 -77
  8. package/agents/devops-engineer.md +37 -113
  9. package/agents/growth-expert.md +45 -88
  10. package/agents/product-manager.md +38 -83
  11. package/agents/qa-engineer.md +51 -99
  12. package/agents/security-engineer.md +47 -92
  13. package/bin/kortext.js +2 -0
  14. package/dist/app.js +829 -0
  15. package/dist/cli-spawn.js +137 -0
  16. package/dist/daemon.js +40 -0
  17. package/dist/db.js +78 -0
  18. package/dist/design-preview.js +533 -0
  19. package/dist/docs.js +701 -0
  20. package/dist/engines.js +225 -0
  21. package/dist/index.js +109 -0
  22. package/dist/pick-directory.js +38 -0
  23. package/dist/projects.js +189 -0
  24. package/dist/readiness.js +288 -0
  25. package/dist/runner.js +771 -0
  26. package/dist/update.js +55 -0
  27. package/docs/CHANGELOG.md +11 -0
  28. package/package.json +53 -13
  29. package/templates/AGENTS.md +62 -0
  30. package/templates/docs/API.md +67 -0
  31. package/templates/docs/ARCHITECTURE.md +31 -0
  32. package/templates/docs/BRIEF.md +27 -0
  33. package/templates/docs/CONTENT.md +66 -0
  34. package/templates/docs/DATABASE.md +63 -0
  35. package/templates/docs/DESIGN.md +124 -0
  36. package/templates/docs/ENGINEERING.md +66 -0
  37. package/templates/docs/ENVIRONMENT.md +102 -0
  38. package/templates/docs/GROWTH.md +44 -0
  39. package/templates/docs/LEGAL.md +48 -0
  40. package/templates/docs/PRODUCT.md +43 -0
  41. package/templates/docs/SECURITY.md +65 -0
  42. package/templates/docs/STACK.md +57 -0
  43. package/templates/docs/STRUCTURE.md +73 -0
  44. package/templates/docs/TEST.md +62 -0
  45. package/ui/dist/assets/abnfDiagram-VCTEODGH-Bm7Q2BML.js +1 -0
  46. package/ui/dist/assets/arc-BV9hs0Hu.js +1 -0
  47. package/ui/dist/assets/architecture-7GRP2DOG-RckR3gBI.js +1 -0
  48. package/ui/dist/assets/architectureDiagram-5GKGNRK7-CHiJ6JFT.js +36 -0
  49. package/ui/dist/assets/array-BifhSqXX.js +1 -0
  50. package/ui/dist/assets/blockDiagram-I7D4REHJ-qCbaKVhd.js +129 -0
  51. package/ui/dist/assets/c4Diagram-7LVT6UL2-DidCDJrv.js +38 -0
  52. package/ui/dist/assets/channel-CNUQ11ne.js +1 -0
  53. package/ui/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  54. package/ui/dist/assets/chunk-4HAMMTFA-CDGFjfVp.js +62 -0
  55. package/ui/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  56. package/ui/dist/assets/chunk-75Z2AOVW-CbNT4sDh.js +2 -0
  57. package/ui/dist/assets/chunk-DU6HZSFF-Cq2BVv26.js +127 -0
  58. package/ui/dist/assets/chunk-F27PBJKO-CE365m0v.js +1 -0
  59. package/ui/dist/assets/chunk-FOHPRMQF-DHwB1DNv.js +161 -0
  60. package/ui/dist/assets/chunk-GMAD6QVW-DxWL1VC-.js +72 -0
  61. package/ui/dist/assets/chunk-GVQU2GXP-aARV8YbK.js +1 -0
  62. package/ui/dist/assets/chunk-IMKFNOWR-CC790Kt2.js +231 -0
  63. package/ui/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  64. package/ui/dist/assets/chunk-L3NEJ4N5-BfE4VE8J.js +1 -0
  65. package/ui/dist/assets/chunk-OSK3NFVY-BImQbSWH.js +10 -0
  66. package/ui/dist/assets/chunk-P2QGCYS3-CN3XD313.js +1 -0
  67. package/ui/dist/assets/chunk-POPQ4Y6H-Dqb6jOrX.js +1 -0
  68. package/ui/dist/assets/chunk-PWAF6VOD-DfXX6rBk.js +1 -0
  69. package/ui/dist/assets/chunk-SHT3W25Y-DATyyqPK.js +168 -0
  70. package/ui/dist/assets/chunk-SVP7TREG-r1chwRmg.js +88 -0
  71. package/ui/dist/assets/chunk-TICWLB2K-DL4atHUC.js +206 -0
  72. package/ui/dist/assets/chunk-XXDRQBXY-DXrM6jh7.js +1 -0
  73. package/ui/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  74. package/ui/dist/assets/classDiagram-ZZMXUADV-BpQxfIQ7.js +1 -0
  75. package/ui/dist/assets/classDiagram-v2-VYDZK3BY-BpQxfIQ7.js +1 -0
  76. package/ui/dist/assets/cose-bilkent-JH36ORCC-Bhe-ZBlS.js +1 -0
  77. package/ui/dist/assets/cynefin-OW5HDTMX-D1koLd-G.js +1 -0
  78. package/ui/dist/assets/cynefinDiagram-5FMLGOSQ-BMcw40hV.js +62 -0
  79. package/ui/dist/assets/cytoscape.esm-CGd-uY3x.js +321 -0
  80. package/ui/dist/assets/dagre-GXQ25YYZ-DBun-Rs2.js +4 -0
  81. package/ui/dist/assets/dagre-PrKaheQc.js +1 -0
  82. package/ui/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  83. package/ui/dist/assets/diagram-S7CK7UJ4-C1fUIGvo.js +30 -0
  84. package/ui/dist/assets/diagram-UQ7AKVKN-B5FDfkvB.js +41 -0
  85. package/ui/dist/assets/diagram-VSXAHHWV-CCIpOCNC.js +3 -0
  86. package/ui/dist/assets/diagram-VX7I27RA-DV0q2xo_.js +24 -0
  87. package/ui/dist/assets/diagram-Z3DM3KII-B3LCdNgI.js +24 -0
  88. package/ui/dist/assets/dist-EcW76Zvr.js +1 -0
  89. package/ui/dist/assets/ebnfDiagram-PWID7BFC-De_oRbYe.js +1 -0
  90. package/ui/dist/assets/erDiagram-RLTQ6QDP-B9NikMr1.js +99 -0
  91. package/ui/dist/assets/eventmodeling-NTZA5JFV-DZ4t581k.js +1 -0
  92. package/ui/dist/assets/flowDiagram-HODETNUW-DuTOJp1C.js +1 -0
  93. package/ui/dist/assets/ganttDiagram-EL5Y4UJY-BVTqqlPw.js +292 -0
  94. package/ui/dist/assets/gitGraph-4MIJSDKK-xRFaEJdZ.js +1 -0
  95. package/ui/dist/assets/gitGraphDiagram-WWUBYQGX-CK5_GCmX.js +106 -0
  96. package/ui/dist/assets/graphlib-DS17s2tU.js +1 -0
  97. package/ui/dist/assets/index-CjkpNftW.js +46 -0
  98. package/ui/dist/assets/index-DVIDTWin.css +1 -0
  99. package/ui/dist/assets/info-A6RAGUB7-Dg7z-CTI.js +1 -0
  100. package/ui/dist/assets/infoDiagram-27XIBGKW-YaqsOXh0.js +2 -0
  101. package/ui/dist/assets/init-C-OQMol4.js +1 -0
  102. package/ui/dist/assets/ishikawaDiagram-5VMMS53U-C-wtxZPa.js +70 -0
  103. package/ui/dist/assets/journeyDiagram-3NMN7TZE-DsXSXf16.js +139 -0
  104. package/ui/dist/assets/kanban-definition-UXKFOSKX-Ct59fuWk.js +89 -0
  105. package/ui/dist/assets/katex-CXMH3UgJ.js +257 -0
  106. package/ui/dist/assets/line-BUVw9Mhs.js +1 -0
  107. package/ui/dist/assets/linear-D8ncLO0X.js +1 -0
  108. package/ui/dist/assets/mermaid-parser.core-BDC4bG-O.js +7 -0
  109. package/ui/dist/assets/mermaid.core-DZY3WwzX.js +44 -0
  110. package/ui/dist/assets/mindmap-definition-YA3MSWOX-DtiuF6Ce.js +96 -0
  111. package/ui/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  112. package/ui/dist/assets/packet-AYTQ26CC-BX2-w9rD.js +1 -0
  113. package/ui/dist/assets/path-fybaL0A-.js +1 -0
  114. package/ui/dist/assets/pegDiagram-XKGWAZYB-CotwuIHB.js +1 -0
  115. package/ui/dist/assets/pie-WAS4IAKB-Hth8ZVpm.js +1 -0
  116. package/ui/dist/assets/pieDiagram-E7YTZNPT-D9-INs8B.js +39 -0
  117. package/ui/dist/assets/quadrantDiagram-AXDQQJYC-CMtBXZGV.js +7 -0
  118. package/ui/dist/assets/radar-RG4KPBEZ-6UuwQPsw.js +1 -0
  119. package/ui/dist/assets/railroad-74A4TZTK-BF5TZgj0.js +1 -0
  120. package/ui/dist/assets/railroad-abnf-HS5TGJTU-3KYovtYT.js +1 -0
  121. package/ui/dist/assets/railroad-ebnf-LZEXJU2U-DeP4WedE.js +1 -0
  122. package/ui/dist/assets/railroad-peg-WCYAUIDC-i42N1-yk.js +1 -0
  123. package/ui/dist/assets/railroadDiagram-O6MQD6OU-BS6zkU3Z.js +1 -0
  124. package/ui/dist/assets/requirementDiagram-BXWQKSXE-J4IOB-kP.js +84 -0
  125. package/ui/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  126. package/ui/dist/assets/sankeyDiagram-P5KCCOFB-jYTCuYzQ.js +40 -0
  127. package/ui/dist/assets/sequenceDiagram-WJ2MYXX4-Cd2r6PW-.js +162 -0
  128. package/ui/dist/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
  129. package/ui/dist/assets/src-7TsfZXRO.js +1 -0
  130. package/ui/dist/assets/stateDiagram-D77RDMKH-DXOdtOKe.js +1 -0
  131. package/ui/dist/assets/stateDiagram-v2-MP3YSRHH-9h7sIf3R.js +1 -0
  132. package/ui/dist/assets/swimlanes-42K2YHIH-BvJGmGs1.js +1 -0
  133. package/ui/dist/assets/swimlanesDiagram-VR7AAH4N-QA4gPi6a.js +8 -0
  134. package/ui/dist/assets/timeline-definition-24CTP7MA-CAaD6-yd.js +120 -0
  135. package/ui/dist/assets/treeView-Q6P3EWNA-4gDEXIyJ.js +1 -0
  136. package/ui/dist/assets/treemap-WGGIJYW6-CGel5TwW.js +1 -0
  137. package/ui/dist/assets/vennDiagram-4TSXK5OY-DpxsKboL.js +34 -0
  138. package/ui/dist/assets/wardley-WFR3VGLG-Ck38v5ns.js +1 -0
  139. package/ui/dist/assets/wardleyDiagram-VM6X3IG4-Ba0grrqs.js +78 -0
  140. package/ui/dist/assets/xychartDiagram-S5SC5T6Z-DH_gC96K.js +7 -0
  141. package/ui/dist/index.html +13 -0
  142. package/ui/dist/kortext-logo-dark.svg +41 -0
  143. package/ui/dist/kortext-logo.svg +41 -0
  144. package/workflows/existing-project-analysis.md +87 -0
  145. package/workflows/new-project-analysis.md +98 -0
  146. package/workflows/planning-pipeline.md +61 -0
  147. package/AGENTS.md +0 -28
  148. package/agents/backend-developer.md +0 -132
  149. package/agents/delivery-manager.md +0 -121
  150. package/agents/engineering-manager.md +0 -208
  151. package/agents/frontend-developer.md +0 -174
  152. package/agents/operation-manager.md +0 -122
  153. package/hooks/audit-logger.sh +0 -25
  154. package/hooks/auto-locker.sh +0 -74
  155. package/hooks/auto-unlocker.sh +0 -17
  156. package/hooks/backlog-sync-guard.sh +0 -12
  157. package/hooks/branch-guard.sh +0 -32
  158. package/hooks/commit-msg-guard.sh +0 -43
  159. package/hooks/git-pre-commit.sh +0 -76
  160. package/hooks/git-pre-push.sh +0 -17
  161. package/hooks/handover-guard.sh +0 -33
  162. package/hooks/kortext-init.sh +0 -194
  163. package/hooks/kortext-lib.sh +0 -151
  164. package/hooks/lint-guard.sh +0 -50
  165. package/hooks/secret-scanner.sh +0 -89
  166. package/hooks/size-guard.sh +0 -48
  167. package/hooks/snapshot-guard.sh +0 -39
  168. package/hooks/write-guard.sh +0 -77
  169. package/rules/behavior.md +0 -252
  170. package/rules/branching.md +0 -82
  171. package/rules/commands.md +0 -112
  172. package/rules/emergency.md +0 -160
  173. package/rules/mcp.md +0 -47
  174. package/rules/models.md +0 -93
  175. package/scripts/kortext-backlog-add.py +0 -123
  176. package/scripts/kortext-backlog-health.py +0 -112
  177. package/scripts/kortext-backlog-sync.py +0 -117
  178. package/scripts/kortext-bulk-plan.py +0 -63
  179. package/scripts/kortext-cli.py +0 -376
  180. package/scripts/kortext-consistency-check.py +0 -74
  181. package/scripts/kortext-context-check.py +0 -146
  182. package/scripts/kortext-handover.py +0 -107
  183. package/scripts/kortext-item-check.py +0 -61
  184. package/scripts/kortext-item-start.py +0 -70
  185. package/scripts/kortext-item-transition.py +0 -78
  186. package/scripts/kortext-lock.py +0 -174
  187. package/scripts/kortext-session-start.py +0 -127
  188. package/scripts/lock_kortext.sh +0 -34
  189. package/settings/.claude-settings.template.json +0 -48
  190. package/settings/CHANGELOG.md +0 -176
  191. package/settings/INTEGRATION-MAP.md +0 -276
  192. package/settings/README.md +0 -255
  193. package/settings/USER-GUIDE.md +0 -502
  194. package/settings/VERSION +0 -1
  195. package/settings/config.md +0 -8
  196. package/settings/runtime-adapters.md +0 -104
  197. package/skills/backend-developer/.gitkeep +0 -0
  198. package/skills/compliance-expert/.gitkeep +0 -0
  199. package/skills/copywriter/.gitkeep +0 -0
  200. package/skills/db-admin/.gitkeep +0 -0
  201. package/skills/delivery-manager/.gitkeep +0 -0
  202. package/skills/designer/.gitkeep +0 -0
  203. package/skills/devops-engineer/.gitkeep +0 -0
  204. package/skills/engineering-manager/.gitkeep +0 -0
  205. package/skills/frontend-developer/.gitkeep +0 -0
  206. package/skills/growth-expert/.gitkeep +0 -0
  207. package/skills/operation-manager/.gitkeep +0 -0
  208. package/skills/product-manager/.gitkeep +0 -0
  209. package/skills/qa-engineer/.gitkeep +0 -0
  210. package/skills/security-engineer/.gitkeep +0 -0
  211. package/workflows/00-kortext-setup.md +0 -53
  212. package/workflows/01a-analysis-pipeline.md +0 -117
  213. package/workflows/01b-onboarding-pipeline.md +0 -110
  214. package/workflows/02-planning-pipeline.md +0 -146
  215. package/workflows/02b-spike-workflow.md +0 -120
  216. package/workflows/03-environment-setup.md +0 -99
  217. package/workflows/04-development-cycle.md +0 -203
  218. package/workflows/05-test-cycle.md +0 -123
  219. package/workflows/06-deployment-cycle.md +0 -224
  220. package/workflows/07-rollback-pipeline.md +0 -203
  221. package/workflows/08-hotfix-pipeline.md +0 -196
  222. package/workflows/09-maintenance-cycle.md +0 -133
  223. package/workspace/archive/.gitkeep +0 -0
  224. package/workspace/backups/.gitkeep +0 -0
  225. package/workspace/memory/backlog/.gitkeep +0 -0
  226. package/workspace/memory/backlog/BXX-[bug-name].md +0 -2
  227. package/workspace/memory/backlog/DXX-[debt-name].md +0 -2
  228. package/workspace/memory/backlog/README.md +0 -31
  229. package/workspace/memory/backlog/TXX-[task-name].md +0 -2
  230. package/workspace/memory/backlog/debt-dashboard.md +0 -3
  231. package/workspace/memory/backlog/epic-dashboard.md +0 -10
  232. package/workspace/memory/backlog/version-dashboard.md +0 -7
  233. package/workspace/memory/context/.gitkeep +0 -0
  234. package/workspace/memory/context/README.md +0 -35
  235. package/workspace/memory/context/[agent-name]-active.md +0 -2
  236. package/workspace/memory/decisions.md +0 -17
  237. package/workspace/memory/handover.md +0 -40
  238. package/workspace/memory/learned.md +0 -21
  239. package/workspace/references/access.md +0 -47
  240. package/workspace/references/api-reference.md +0 -59
  241. package/workspace/references/blueprint.md +0 -29
  242. package/workspace/references/content-strategy.md +0 -39
  243. package/workspace/references/db-schema.md +0 -51
  244. package/workspace/references/design-system.md +0 -107
  245. package/workspace/references/dictionary.md +0 -42
  246. package/workspace/references/file-system.md +0 -42
  247. package/workspace/references/growth-strategy.md +0 -46
  248. package/workspace/references/legal-strategy.md +0 -35
  249. package/workspace/references/required-skills.md +0 -11
  250. package/workspace/references/security-rules.md +0 -34
  251. package/workspace/references/tech-stack.md +0 -49
  252. package/workspace/references/test-strategy.md +0 -48
  253. package/workspace/reports/analysis-reports.md +0 -40
  254. package/workspace/reports/content-reports.md +0 -29
  255. package/workspace/reports/delivery-reports.md +0 -26
  256. package/workspace/reports/growth-reports.md +0 -9
  257. package/workspace/reports/legal-reports.md +0 -29
  258. package/workspace/reports/product-requirements.md +0 -28
  259. package/workspace/reports/release-notes.md +0 -20
  260. package/workspace/reports/security-reports.md +0 -27
  261. package/workspace/reports/status-reports.md +0 -48
  262. package/workspace/reports/tech-requirements.md +0 -30
  263. package/workspace/reports/test-reports.md +0 -61
  264. package/workspace/templates/BXX-[bug-name].md +0 -34
  265. package/workspace/templates/DXX-[debt-name].md +0 -35
  266. package/workspace/templates/EXX-[epic-name].md +0 -48
  267. package/workspace/templates/HXX-[hotfix-name].md +0 -73
  268. package/workspace/templates/SXX-[spike-name].md +0 -51
  269. package/workspace/templates/TXX-[task-name].md +0 -34
  270. package/workspace/templates/[agent-name]-active.md +0 -4
  271. package/workspace/templates/[original-name]_[YYYY-MM-DD_HHMMSS].md +0 -5
  272. package/workspace/templates/adr-XXX.md +0 -66
  273. package/workspace/templates/debt-dashboard.md +0 -10
  274. package/workspace/templates/epic-dashboard.md +0 -10
  275. package/workspace/templates/version-dashboard.md +0 -10
package/dist/docs.js ADDED
@@ -0,0 +1,701 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * Where a document sits and what it is waiting for. Five inputs decide it, and
6
+ * two of them — the queued rechecks and whether a run carries revision notes —
7
+ * live only in the database, so the answer is computed here rather than shipped
8
+ * raw for the panel to reassemble.
9
+ *
10
+ * Order matters: the first match wins.
11
+ */
12
+ function fileDoc(doc, job) {
13
+ const at = (section, state, detail) => ({
14
+ section,
15
+ state,
16
+ detail,
17
+ });
18
+ // What a run is doing: reading again, the first draft, or a revision of what
19
+ // stands. A recheck that fell over says `failed · recheck`, not `· draft`.
20
+ const pass = job?.kind === 'recheck' ? 'recheck' : job?.isUpdate ? 'revision' : 'draft';
21
+ if (job?.kind === 'doc' && job.status === 'running')
22
+ return at('doing', 'writing', pass);
23
+ // `paused` means one thing: prime stopped it. A run that errored is `failed`,
24
+ // and the two share no button — Continue against Retry.
25
+ if (job?.status === 'stopped')
26
+ return at('doing', 'paused', pass);
27
+ if (job?.status === 'failed')
28
+ return at('needs', 'failed', pass);
29
+ if (doc.status === 'uninitialized')
30
+ return at('todo', 'waiting', 'queue');
31
+ // Every open Action Needed item blocks approval, so one case covers them all:
32
+ // questions left for prime, requests arriving from other documents, and
33
+ // requests this one wants to send. Findings are not among them — they are
34
+ // records written into the document, not decisions owed.
35
+ if (doc.revisionRequests.length > 0 ||
36
+ (doc.status === 'draft' && (doc.openQuestions || doc.outgoing.length > 0)))
37
+ return at('needs', 'waiting', 'review');
38
+ // A recheck in flight is the agent's work too, so it sits in Doing; one that
39
+ // is only queued is a wait. A draft with a recheck queued still owes prime an
40
+ // approval first — the recheck reads whatever prime approves.
41
+ if (job?.kind === 'recheck' && job.status === 'running')
42
+ return at('doing', 'reading', 'recheck');
43
+ if (doc.status === 'draft')
44
+ return at('needs', 'waiting', 'approve');
45
+ if (doc.pendingRecheck)
46
+ return at('todo', 'waiting', 'recheck');
47
+ if (doc.status === 'approved')
48
+ return at('done', 'approved', null);
49
+ return at('done', 'n/a', null);
50
+ }
51
+ /**
52
+ * One grammar for every marked line. Under `## Change Requests` the line has a
53
+ * direction: `` - `TARGET.md` — reason `` is what THIS document asks of another
54
+ * (it leaves when this document is approved), and `` - [ ] from `SOURCE.md` —
55
+ * reason `` is what another document asked of this one (it stays, and is
56
+ * decided here). Findings use the same shape with any path as the subject,
57
+ * because insisting on `.md` is what made a real `.gitignore` finding vanish
58
+ * into prose nobody could act on.
59
+ */
60
+ const MARKED_LINE = /^\s*[-*+]\s+(?:\[([ xX]?)\]\s*)?(from\s+)?`([^`]+)`\s*[—:-]?\s*(.*)$/;
61
+ /** An outcome trailer: indented and a list item of its own. */
62
+ const TRAILER = /^\s+[-*+] /;
63
+ /**
64
+ * A demand is one item, but an agent may wrap it over several lines. The
65
+ * indented lines that follow are the rest of the sentence unless they are
66
+ * list items themselves, which is what an outcome trailer looks like.
67
+ * Returns the whole reason and the index the item ends at.
68
+ */
69
+ function foldWrapped(lines, i, first) {
70
+ let reason = first.trim();
71
+ let j = i + 1;
72
+ for (; j < lines.length; j++) {
73
+ const line = lines[j] ?? '';
74
+ if (!/^\s+\S/.test(line) || TRAILER.test(line))
75
+ break;
76
+ reason = `${reason} ${line.trim()}`.trim();
77
+ }
78
+ return [reason, j];
79
+ }
80
+ /** Every marked line under `heading`, settled or not, with its direction. */
81
+ export function parseMarkedList(content, heading) {
82
+ const out = [];
83
+ const lines = content.split('\n');
84
+ let inSection = false;
85
+ for (let i = 0; i < lines.length; i++) {
86
+ const line = lines[i] ?? '';
87
+ const h = line.match(/^#{1,6}\s+(.*)$/);
88
+ if (h) {
89
+ inSection = heading.test((h[1] ?? '').trim());
90
+ continue;
91
+ }
92
+ if (!inSection)
93
+ continue;
94
+ const m = line.match(MARKED_LINE);
95
+ if (!m)
96
+ continue;
97
+ const [reason, next] = foldWrapped(lines, i, m[4] ?? '');
98
+ const trailer = lines[next] ?? '';
99
+ i = next - 1;
100
+ out.push({
101
+ subject: m[3].replace(/^\.kortext\//, ''),
102
+ reason,
103
+ incoming: !!m[2],
104
+ settled: (m[1] ?? '').trim().toLowerCase() === 'x',
105
+ outcome: TRAILER.test(trailer) ? trailer.replace(TRAILER, '').trim() : '',
106
+ });
107
+ }
108
+ return out;
109
+ }
110
+ const DOC_SUBJECT = /^[A-Za-z][\w./-]*\.md$/;
111
+ /**
112
+ * The three headings, each accepting the name it used to carry. Documents
113
+ * written before the rename are still on disk, and a parser that stopped
114
+ * reading them would drop demands nobody would ever see again.
115
+ */
116
+ export const CHANGE_REQUESTS = /^(change|revision) requests$/i;
117
+ /** The ledger: what prime refused, with the reason. Read by the next writer. */
118
+ export const DECISIONS = /^decisions$/i;
119
+ export const FINDINGS = /^(findings|warnings)$/i;
120
+ export const QUESTIONS = /^(open )?questions( for prime)?$/i;
121
+ /** What other documents asked of this one and prime has not decided yet. */
122
+ export function parseIncoming(content) {
123
+ return parseMarkedList(content, CHANGE_REQUESTS)
124
+ .filter((r) => r.incoming && !r.settled)
125
+ .map((r) => ({ from: r.subject, reason: r.reason }));
126
+ }
127
+ /**
128
+ * What prime refused here — the `## Decisions` ledger, plus the two shapes
129
+ * older documents carry: a ticked `denied` line still under Change Requests,
130
+ * and the `## Conflicts` section before that.
131
+ */
132
+ export function parseDenied(content) {
133
+ const ledger = parseMarkedList(content, DECISIONS);
134
+ const inPlace = parseMarkedList(content, CHANGE_REQUESTS).filter((r) => r.incoming && r.settled && /^(denied|dismissed)/i.test(r.outcome));
135
+ return [...ledger, ...inPlace].map((r) => ({ from: r.subject, reason: r.reason }));
136
+ }
137
+ /**
138
+ * What this document asks of others — the lines the agent wrote while drafting.
139
+ * They travel to the document they name when this one is approved.
140
+ */
141
+ export function parseOutgoing(content) {
142
+ return parseMarkedList(content, CHANGE_REQUESTS)
143
+ .filter((r) => !r.incoming && !r.settled && DOC_SUBJECT.test(r.subject))
144
+ .map((r) => ({ target: r.subject, reason: r.reason }));
145
+ }
146
+ /** The `## Conflicts` section older documents carry. Counted, never written. */
147
+ export function parseConflicts(content) {
148
+ return parseMarkedList(content, /^conflicts$/i)
149
+ .filter((r) => !r.settled)
150
+ .map((r) => ({ from: r.subject, reason: r.reason }));
151
+ }
152
+ /**
153
+ * Findings about something the document set does not own.
154
+ *
155
+ * A demand aimed at a file that is not a document counts as one wherever it was
156
+ * written. An agent found `.env` tracked in git and filed it under Change
157
+ * Requests against `.gitignore`; the demand parser wants a document, so the line
158
+ * became prose nobody could act on. It is a finding, and it is read as one.
159
+ */
160
+ export function parseWarnings(content) {
161
+ const own = parseMarkedList(content, FINDINGS).filter((r) => !r.settled);
162
+ const misfiled = parseMarkedList(content, CHANGE_REQUESTS).filter((r) => !r.incoming && !r.settled && !DOC_SUBJECT.test(r.subject));
163
+ return [...own, ...misfiled].map((r) => ({ subject: r.subject, reason: r.reason }));
164
+ }
165
+ // Read questions only from the questions section; ignore template placeholders.
166
+ export function hasOpenQuestions(content) {
167
+ const lines = content.split('\n');
168
+ let inSection = false;
169
+ for (const line of lines) {
170
+ const heading = line.match(/^#{1,6}\s+(.*)$/);
171
+ if (heading) {
172
+ inSection = QUESTIONS.test((heading[1] ?? '').trim());
173
+ continue;
174
+ }
175
+ if (!inSection)
176
+ continue;
177
+ const t = line.trim();
178
+ if (t === '' || /^[-*+]?\s*\[[^\]]*\]$/.test(t))
179
+ continue;
180
+ return true;
181
+ }
182
+ return false;
183
+ }
184
+ /**
185
+ * Lines the agent was meant to replace and did not. A real test shipped an
186
+ * approved `DATABASE.md` still carrying `### Table: \`[table_name]\``, because the
187
+ * prompt told it to keep headings verbatim and the heading itself was a pattern.
188
+ *
189
+ * Bracketed prose is normal in a template (`[e.g., PostgreSQL]`), so a line only
190
+ * counts as unfilled when it survives verbatim from the shipped skeleton — plus
191
+ * any heading carrying a bracketed span, which is a pattern wherever it came from.
192
+ */
193
+ export function unfilledPlaceholders(content, template) {
194
+ const shipped = new Set((template ?? '')
195
+ .split('\n')
196
+ .map((l) => l.trim())
197
+ .filter(Boolean));
198
+ const out = [];
199
+ for (const line of content.split('\n')) {
200
+ const t = line.trim();
201
+ // An alert marker (`[!WARNING]`) and a task box (`[ ]`, `[x]`) are markdown,
202
+ // not blanks to fill — a template ships them and a finished document keeps
203
+ // them. Only the bracketed prose that is left counts.
204
+ const blanks = t.replace(/\[![A-Z]+\]/g, '').replace(/\[[ xX]\]/g, '');
205
+ if (!t || !/\[[^\]]+\]/.test(blanks))
206
+ continue;
207
+ if (/^#{1,6}\s/.test(t) || shipped.has(t))
208
+ out.push(t);
209
+ }
210
+ return out;
211
+ }
212
+ export function templateFor(pkgRoot, rel) {
213
+ const p = join(pkgRoot, 'templates', 'docs', rel);
214
+ return existsSync(p) ? readFileSync(p, 'utf8') : null;
215
+ }
216
+ // Parses workflow step metadata: numbered steps carrying
217
+ // 1. **+persona:** …
218
+ // - inputs: `.kortext/BRIEF.md`, …
219
+ // - outputs: `.kortext/STACK.md`
220
+ // - approver: +prime
221
+ // Returns one DocStep per output file.
222
+ export function parseWorkflowSteps(md) {
223
+ const steps = [];
224
+ let author = null;
225
+ let inputs = [];
226
+ let outputs = [];
227
+ let approver = null;
228
+ const flush = () => {
229
+ for (const output of outputs)
230
+ steps.push({ output, inputs, author, approver });
231
+ author = null;
232
+ inputs = [];
233
+ outputs = [];
234
+ approver = null;
235
+ };
236
+ for (const line of md.split('\n')) {
237
+ const stepStart = line.match(/^\d+\.\s+\*\*(\+[a-z-]+):?\*\*/);
238
+ if (stepStart) {
239
+ flush();
240
+ author = stepStart[1];
241
+ continue;
242
+ }
243
+ const paths = (s) => [...s.matchAll(/`\.kortext\/([^`]+)`/g)].map((m) => m[1]);
244
+ if (/^\s*-\s*inputs:/.test(line))
245
+ inputs = paths(line);
246
+ else if (/^\s*-\s*outputs:/.test(line))
247
+ outputs = paths(line);
248
+ else if (/^\s*-\s*approver:/.test(line))
249
+ approver = line.split('approver:')[1].trim();
250
+ }
251
+ flush();
252
+ return steps;
253
+ }
254
+ // The dependency map follows the project's kind: a 'new' project reads
255
+ // new-project-analysis, an 'existing' one existing-project-analysis
256
+ // (planning-pipeline.md declares no document steps of its own).
257
+ export function workflowNameFor(kind) {
258
+ return kind === 'existing' ? 'existing-project-analysis' : 'new-project-analysis';
259
+ }
260
+ export function loadDocMap(pkgRoot, kind = 'new') {
261
+ const map = new Map();
262
+ // Only analysis workflows declare document steps; planning produces separate .kopeng/ files.
263
+ const p = join(pkgRoot, 'workflows', `${workflowNameFor(kind)}.md`);
264
+ if (!existsSync(p))
265
+ return map;
266
+ for (const step of parseWorkflowSteps(readFileSync(p, 'utf8'))) {
267
+ if (!map.has(step.output))
268
+ map.set(step.output, step);
269
+ }
270
+ return map;
271
+ }
272
+ export function readFrontmatter(content) {
273
+ const out = {};
274
+ if (!content.startsWith('---'))
275
+ return out;
276
+ const end = content.indexOf('\n---', 3);
277
+ if (end === -1)
278
+ return out;
279
+ for (const line of content.slice(3, end).split('\n')) {
280
+ const m = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
281
+ if (m)
282
+ out[m[1]] = m[2].trim().replace(/^['"]|['"]$/g, '');
283
+ }
284
+ return out;
285
+ }
286
+ export function setFrontmatterStatus(path, status) {
287
+ const body = readFileSync(path, 'utf8');
288
+ if (/^status:/m.test(body)) {
289
+ writeFileSync(path, body.replace(/^status:.*$/m, `status: ${status}`), 'utf8');
290
+ }
291
+ else if (body.startsWith('---\n')) {
292
+ writeFileSync(path, body.replace('---\n', `---\nstatus: ${status}\n`), 'utf8');
293
+ }
294
+ else {
295
+ writeFileSync(path, `---\nstatus: ${status}\n---\n\n${body}`, 'utf8');
296
+ }
297
+ }
298
+ /**
299
+ * Removes a request from the document it is about, once it has been done. A
300
+ * request that was accepted and written needs no record: the next reader sees
301
+ * the document as asked, and git keeps the history. Only a denial is kept —
302
+ * see `markRequestHandled` — because a denial is the one outcome the next
303
+ * writer cannot infer from the text.
304
+ */
305
+ export function removeRequest(project, rel, from, reason) {
306
+ const path = docPath(project, rel);
307
+ if (!existsSync(path))
308
+ return;
309
+ const lines = readFileSync(path, 'utf8').split('\n');
310
+ let inSection = false;
311
+ for (let i = 0; i < lines.length; i++) {
312
+ const line = lines[i] ?? '';
313
+ const h = line.match(/^#{1,6}\s+(.*)$/);
314
+ if (h) {
315
+ inSection = CHANGE_REQUESTS.test((h[1] ?? '').trim());
316
+ continue;
317
+ }
318
+ if (!inSection)
319
+ continue;
320
+ const m = line.match(MARKED_LINE);
321
+ if (!m || !m[2])
322
+ continue;
323
+ if (m[3].replace(/^\.kortext\//, '') !== from.replace(/^.*\//, '') && m[3] !== from)
324
+ continue;
325
+ const [full, afterWrap] = foldWrapped(lines, i, m[4] ?? '');
326
+ if (full !== reason.trim())
327
+ continue;
328
+ let end = afterWrap;
329
+ while (end < lines.length && TRAILER.test(lines[end] ?? ''))
330
+ end++;
331
+ lines.splice(i, end - i);
332
+ writeFileSync(path, lines.join('\n'), 'utf8');
333
+ return;
334
+ }
335
+ }
336
+ /** Drops what this document asked of another, before it was sent. */
337
+ export function discardOutgoing(project, rel, target, reason) {
338
+ const path = docPath(project, rel);
339
+ if (!existsSync(path))
340
+ return;
341
+ const lines = readFileSync(path, 'utf8').split('\n');
342
+ let inSection = false;
343
+ for (let i = 0; i < lines.length; i++) {
344
+ const line = lines[i] ?? '';
345
+ const h = line.match(/^#{1,6}\s+(.*)$/);
346
+ if (h) {
347
+ inSection = CHANGE_REQUESTS.test((h[1] ?? '').trim());
348
+ continue;
349
+ }
350
+ if (!inSection)
351
+ continue;
352
+ const m = line.match(MARKED_LINE);
353
+ if (!m || m[2] || (m[1] ?? '').trim().toLowerCase() === 'x')
354
+ continue;
355
+ if (m[3].replace(/^\.kortext\//, '') !== target)
356
+ continue;
357
+ const [full, afterWrap] = foldWrapped(lines, i, m[4] ?? '');
358
+ if (full !== reason.trim())
359
+ continue;
360
+ lines.splice(i, afterWrap - i);
361
+ writeFileSync(path, lines.join('\n'), 'utf8');
362
+ return;
363
+ }
364
+ }
365
+ /**
366
+ * Records a refusal in the document it is about: the request leaves the
367
+ * mailbox and goes into `## Decisions`, the reason under it. Not a request any
368
+ * more — a decision, which is why it carries no box, no "denied", no "prime"
369
+ * and no date: the heading is the word, prime is the only one who decides, and
370
+ * git keeps the day. The next agent to rewrite `rel` reads it here, and does
371
+ * not raise the same request again.
372
+ */
373
+ export function markRequestHandled(project, rel, from, reason, outcome) {
374
+ removeRequest(project, rel, from, reason);
375
+ appendListItem(project, rel, 'Decisions', from, reason, outcome, false, false);
376
+ }
377
+ /**
378
+ * Adds one marked line under `heading`, creating the section when the document
379
+ * does not carry it. Sections are made on demand rather than shipped in the
380
+ * fifteen templates, so an empty one never exists to be mistaken for work.
381
+ */
382
+ export function appendListItem(project, rel, heading, subject, reason, trailer, incoming = false, boxed = true) {
383
+ const path = docPath(project, rel);
384
+ if (!existsSync(path))
385
+ return;
386
+ const lines = readFileSync(path, 'utf8').split('\n');
387
+ const item = `- ${boxed ? '[ ] ' : ''}${incoming ? 'from ' : ''}\`${subject}\` — ${reason.replace(/\s+/g, ' ').trim()}`;
388
+ const block = trailer ? [item, ` - ${trailer}`] : [item];
389
+ const head = lines.findIndex((l) => {
390
+ const m = l.match(/^#{1,6}\s+(.*?)\s*$/);
391
+ return !!m && new RegExp(`^${heading}$`, 'i').test(m[1]);
392
+ });
393
+ if (head === -1) {
394
+ lines.push('', `## ${heading}`, '', ...block);
395
+ }
396
+ else {
397
+ let end = head + 1;
398
+ while (end < lines.length && !/^#{1,6}\s/.test(lines[end] ?? ''))
399
+ end++;
400
+ let at = end;
401
+ while (at > head + 1 && (lines[at - 1] ?? '').trim() === '')
402
+ at--;
403
+ lines.splice(at, 0, ...block);
404
+ }
405
+ writeFileSync(path, lines.join('\n'), 'utf8');
406
+ }
407
+ /** Files a request about `target`, made by `from`, where it will be decided. */
408
+ export function appendIncomingRequest(project, target, from, reason) {
409
+ appendListItem(project, target, 'Change Requests', from, reason, undefined, true);
410
+ }
411
+ /**
412
+ * Puts back what an agent's rewrite dropped. The lines `from` other documents
413
+ * are not the agent's to remove — an open one is a decision prime still owes,
414
+ * a ticked one is a decision prime made — and the prompt says so, but a prompt
415
+ * is not a guarantee. This is: after every agent write, any such line missing
416
+ * from the new text is appended again, state and outcome intact.
417
+ */
418
+ export function restoreRequests(project, rel, priorText) {
419
+ if (priorText === null)
420
+ return;
421
+ const path = docPath(project, rel);
422
+ if (!existsSync(path))
423
+ return;
424
+ const written = readFileSync(path, 'utf8');
425
+ const missing = (heading, keep) => {
426
+ const kept = parseMarkedList(written, heading).filter(keep);
427
+ return parseMarkedList(priorText, heading)
428
+ .filter(keep)
429
+ .filter((r) => !kept.some((k) => k.subject === r.subject && k.reason === r.reason));
430
+ };
431
+ const line = (r, boxed) => [
432
+ `- ${boxed ? `[${r.settled ? 'x' : ' '}] ` : ''}${r.incoming ? 'from ' : ''}\`${r.subject}\` — ${r.reason}`,
433
+ ...(r.outcome ? [` - ${r.outcome}`] : []),
434
+ ];
435
+ const lines = written.split('\n');
436
+ const put = (heading, title, block) => {
437
+ if (block.length === 0)
438
+ return;
439
+ const head = lines.findIndex((l) => {
440
+ const m = l.match(/^#{1,6}\s+(.*?)\s*$/);
441
+ return !!m && heading.test(m[1]);
442
+ });
443
+ if (head === -1) {
444
+ lines.push('', `## ${title}`, '', ...block);
445
+ }
446
+ else {
447
+ let end = head + 1;
448
+ while (end < lines.length && !/^#{1,6}\s/.test(lines[end] ?? ''))
449
+ end++;
450
+ let at = end;
451
+ while (at > head + 1 && (lines[at - 1] ?? '').trim() === '')
452
+ at--;
453
+ lines.splice(at, 0, ...block);
454
+ }
455
+ };
456
+ const requests = missing(CHANGE_REQUESTS, (r) => r.incoming).flatMap((r) => line(r, true));
457
+ const decisions = missing(DECISIONS, () => true).flatMap((r) => line(r, false));
458
+ if (requests.length === 0 && decisions.length === 0)
459
+ return;
460
+ put(CHANGE_REQUESTS, 'Change Requests', requests);
461
+ put(DECISIONS, 'Decisions', decisions);
462
+ writeFileSync(path, lines.join('\n'), 'utf8');
463
+ }
464
+ /**
465
+ * Moves every open outgoing request of an approved document to the document it
466
+ * names. The agent writes what it asks of others into its own draft, where
467
+ * prime sees it before approving and may delete it; approval is the moment it
468
+ * becomes real, so this runs on approval — and once more on every listing, for
469
+ * documents approved before requests travelled. Idempotent: a moved line is
470
+ * gone from its source.
471
+ */
472
+ export function deliverRequests(project, rel, only) {
473
+ const path = docPath(project, rel);
474
+ if (!existsSync(path))
475
+ return 0;
476
+ const text = readFileSync(path, 'utf8');
477
+ // Without a list this is the sweep for documents approved before requests
478
+ // travelled; with one it is prime pressing Send on a draft.
479
+ if (!only && readFrontmatter(text).status !== 'approved')
480
+ return 0;
481
+ const outgoing = parseOutgoing(text).filter((r) => {
482
+ if (only && !only.some((o) => o.target === r.target && o.reason === r.reason))
483
+ return false;
484
+ try {
485
+ return existsSync(docPath(project, r.target));
486
+ }
487
+ catch {
488
+ return false;
489
+ }
490
+ });
491
+ if (outgoing.length === 0)
492
+ return 0;
493
+ // Remove from the source first, line by line, then file in the target.
494
+ const lines = text.split('\n');
495
+ let inSection = false;
496
+ for (let i = 0; i < lines.length; i++) {
497
+ const line = lines[i] ?? '';
498
+ const h = line.match(/^#{1,6}\s+(.*)$/);
499
+ if (h) {
500
+ inSection = CHANGE_REQUESTS.test((h[1] ?? '').trim());
501
+ continue;
502
+ }
503
+ if (!inSection)
504
+ continue;
505
+ const m = line.match(MARKED_LINE);
506
+ if (!m || m[2] || (m[1] ?? '').trim().toLowerCase() === 'x')
507
+ continue;
508
+ const [reason, next] = foldWrapped(lines, i, m[4] ?? '');
509
+ const name = m[3].replace(/^\.kortext\//, '');
510
+ if (!outgoing.some((r) => r.target === name && r.reason === reason))
511
+ continue;
512
+ lines.splice(i, next - i);
513
+ i--;
514
+ }
515
+ writeFileSync(path, lines.join('\n'), 'utf8');
516
+ for (const r of outgoing)
517
+ appendIncomingRequest(project, r.target, rel, r.reason);
518
+ return outgoing.length;
519
+ }
520
+ export function listDocs(db, project, pkgRoot) {
521
+ const map = loadDocMap(pkgRoot, project.kind ?? 'new');
522
+ const statuses = new Map();
523
+ const docs = [];
524
+ const dir = join(project.repo_path, '.kortext');
525
+ if (existsSync(dir)) {
526
+ // Requests travel on approval. A document approved before they did still
527
+ // carries its outgoing lines; deliver those now, before anything is read.
528
+ for (const file of readdirSync(dir).filter((f) => f.endsWith('.md'))) {
529
+ deliverRequests(project, file);
530
+ }
531
+ // One shelf: every .md in .kortext/ is a document of this project.
532
+ for (const file of readdirSync(dir)
533
+ .filter((f) => f.endsWith('.md'))
534
+ .sort()) {
535
+ const rel = file;
536
+ const body = readFileSync(join(dir, file), 'utf8');
537
+ const fm = readFrontmatter(body);
538
+ const status = fm.status ?? 'uninitialized';
539
+ statuses.set(rel, status);
540
+ docs.push({
541
+ rel,
542
+ name: file.replace(/\.md$/, ''),
543
+ status,
544
+ author: fm.author ?? map.get(rel)?.author ?? null,
545
+ inputs: map.get(rel)?.inputs ?? [],
546
+ blocked: false,
547
+ dependentOn: [],
548
+ openQuestions: status !== 'uninitialized' && hasOpenQuestions(body),
549
+ hasProducingStep: map.has(rel),
550
+ // Read from the document's own section: what others asked of it is
551
+ // filed here when they are approved, and decided here. An unwritten
552
+ // document can already hold some — they go into its first write.
553
+ revisionRequests: parseIncoming(body),
554
+ denied: parseDenied(body),
555
+ outgoing: status === 'uninitialized' ? [] : parseOutgoing(body),
556
+ warnings: status === 'uninitialized' ? [] : parseWarnings(body),
557
+ conflicts: status === 'uninitialized' ? [] : parseConflicts(body),
558
+ // Filled once every document is known; nothing can be filed before then.
559
+ section: 'todo',
560
+ state: 'waiting',
561
+ detail: 'queue',
562
+ pendingRecheck: false,
563
+ });
564
+ }
565
+ }
566
+ // 'not-applicable' satisfies a dependency: the doc was considered and
567
+ // deliberately skipped — downstream steps must not wait on it.
568
+ const settled = (s) => s === 'approved' || s === 'not-applicable';
569
+ const byRel = new Map(docs.map((d) => [d.rel, d]));
570
+ for (const doc of docs) {
571
+ doc.blocked = doc.inputs.some((i) => !settled(statuses.get(i)));
572
+ // Only approved readers need rechecking when an input becomes unsettled or receives a request.
573
+ if (doc.status !== 'approved')
574
+ continue;
575
+ doc.dependentOn = doc.inputs.filter((i) => {
576
+ const input = byRel.get(i);
577
+ if (!input)
578
+ return false;
579
+ return input.revisionRequests.length > 0 || !settled(input.status);
580
+ });
581
+ }
582
+ // Sort by maximum dependency depth. Memoize per document, but keep cycle detection
583
+ // local to each traversal path so shared inputs in a diamond retain their depth.
584
+ const memo = new Map();
585
+ const depth = (rel, path = new Set()) => {
586
+ const done = memo.get(rel);
587
+ if (done !== undefined)
588
+ return done;
589
+ if (path.has(rel))
590
+ return 0; // ponytail: the shipped workflows are acyclic; this is for hand-edited ones
591
+ path.add(rel);
592
+ const ins = map.get(rel)?.inputs ?? [];
593
+ const d = ins.length === 0 ? 0 : 1 + Math.max(...ins.map((i) => depth(i, path)));
594
+ path.delete(rel);
595
+ memo.set(rel, d);
596
+ return d;
597
+ };
598
+ // The last job per document, and the rechecks queued against it. Both live in
599
+ // the database only; the runner cannot be imported here — it imports this file.
600
+ const jobs = new Map();
601
+ for (const row of db
602
+ .prepare(`SELECT doc_rel, kind, status, notes FROM jobs
603
+ WHERE project_id = ? AND id IN (SELECT MAX(id) FROM jobs WHERE project_id = ? GROUP BY doc_rel)`)
604
+ .all(project.id, project.id)) {
605
+ let isUpdate = false;
606
+ try {
607
+ isUpdate = JSON.parse(row.notes || '[]').length > 0;
608
+ }
609
+ catch {
610
+ isUpdate = false;
611
+ }
612
+ jobs.set(row.doc_rel, { kind: row.kind, status: row.status, isUpdate });
613
+ }
614
+ const rechecking = new Set(db
615
+ .prepare('SELECT DISTINCT reader_rel FROM pending_rechecks WHERE project_id = ?')
616
+ .all(project.id).map((r) => r.reader_rel));
617
+ for (const doc of docs) {
618
+ const job = jobs.get(doc.rel) ?? null;
619
+ doc.pendingRecheck =
620
+ rechecking.has(doc.rel) || (job?.kind === 'recheck' && job.status === 'running');
621
+ Object.assign(doc, fileDoc(doc, job));
622
+ }
623
+ docs.sort((a, b) => depth(a.rel) - depth(b.rel) || a.rel.localeCompare(b.rel));
624
+ return docs;
625
+ }
626
+ // The handshake is done when every document the workflow produces is settled
627
+ // (approved or not-applicable). Docs without a producing step (unmapped
628
+ // skeletons a project already carried) don't gate completion.
629
+ export function analysisComplete(db, project, pkgRoot) {
630
+ if (db.prepare('SELECT 1 FROM pending_rechecks WHERE project_id = ? LIMIT 1').get(project.id)) {
631
+ return false;
632
+ }
633
+ if (db
634
+ .prepare("SELECT 1 FROM jobs WHERE project_id = ? AND status = 'running' AND kind != 'plan' LIMIT 1")
635
+ .get(project.id)) {
636
+ return false;
637
+ }
638
+ const map = loadDocMap(pkgRoot, project.kind ?? 'new');
639
+ const docs = listDocs(db, project, pkgRoot);
640
+ const byRel = new Map(docs.map((d) => [d.rel, d.status]));
641
+ const targets = [...map.keys()];
642
+ if (targets.length === 0)
643
+ return false;
644
+ const settled = (s) => s === 'approved' || s === 'not-applicable';
645
+ // The brief gates the new-project flow even though no step produces it
646
+ if ((project.kind ?? 'new') === 'new' && !settled(byRel.get('BRIEF.md')))
647
+ return false;
648
+ if (targets.some((rel) => docs.find((d) => d.rel === rel)?.openQuestions))
649
+ return false;
650
+ if (docs.some((d) => d.revisionRequests.length > 0))
651
+ return false;
652
+ // Conflicts and findings do not gate the handshake. A conflict is not a
653
+ // decision prime avoided; it is one deferred to the moment there is enough
654
+ // information — the build phase, where prime is present anyway. It is handed
655
+ // over, not settled here.
656
+ return targets.every((rel) => settled(byRel.get(rel)));
657
+ }
658
+ // rel names a document on the shelf ("STACK.md"). The pattern forbids traversal
659
+ // ("." never starts the name) and anything outside .kortext/ itself.
660
+ export function docPath(project, rel) {
661
+ if (!/^[A-Za-z][\w.-]*\.md$/.test(rel)) {
662
+ throw new Error(`bad doc path: ${rel}`);
663
+ }
664
+ return join(project.repo_path, '.kortext', rel);
665
+ }
666
+ export function docVersion(content) {
667
+ return createHash('sha256').update(content).digest('hex');
668
+ }
669
+ /**
670
+ * Keeps what a document said, so a revision can be read as a change rather than
671
+ * as a new text. Only the two paths that rewrite the prose record: the agent's
672
+ * write and prime's save. Approving, ticking a demand and appending a recheck's
673
+ * line all touch one line the reader already strips.
674
+ *
675
+ * `priorText` bootstraps the chain: the first recorded write has no predecessor
676
+ * to compare against, so the bytes it replaced go in first.
677
+ */
678
+ export function recordVersion(db, project, rel, content, source, priorText, jobId) {
679
+ const insert = db.prepare('INSERT INTO doc_versions (project_id, rel, sha, content, source, job_id) VALUES (?, ?, ?, ?, ?, ?)');
680
+ const seen = db
681
+ .prepare('SELECT 1 FROM doc_versions WHERE project_id = ? AND rel = ? LIMIT 1')
682
+ .get(project.id, rel);
683
+ if (!seen && priorText !== null) {
684
+ insert.run(project.id, rel, docVersion(priorText), priorText, 'pre-existing', null);
685
+ }
686
+ insert.run(project.id, rel, docVersion(content), content, source, jobId ?? null);
687
+ }
688
+ /** Everything above and including the closing `---` of the frontmatter. */
689
+ function stripFrontmatter(content) {
690
+ return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
691
+ }
692
+ export function listVersions(db, project, rel) {
693
+ return db
694
+ .prepare('SELECT id, sha, content, source, created_at FROM doc_versions WHERE project_id = ? AND rel = ? ORDER BY id DESC')
695
+ .all(project.id, rel).map(({ content, ...v }) => ({ ...v, bodySha: docVersion(stripFrontmatter(content)) }));
696
+ }
697
+ export function readVersion(db, project, id) {
698
+ return (db
699
+ .prepare('SELECT content, rel FROM doc_versions WHERE id = ? AND project_id = ?')
700
+ .get(id, project.id) ?? null);
701
+ }