naive-framework 0.1.0 → 0.3.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 (281) hide show
  1. package/AGENTS.md +2 -1
  2. package/docs/decisions/decisions.json +42 -0
  3. package/docs/reference/ROUTES.md +36 -37
  4. package/evidence/skills/fidelity-ladder-audit/ladder-census.mjs +2 -1
  5. package/governance/layer.json +2 -1
  6. package/governance/venture-template/roles/venture-roles.json +62 -0
  7. package/interface/acting.mjs +83 -0
  8. package/interface/adapters/claude-code.json +16 -0
  9. package/interface/adapters/codex.json +14 -0
  10. package/interface/adapters/cursor.json +16 -0
  11. package/interface/adapters.mjs +183 -0
  12. package/interface/api/server.mjs +4 -2
  13. package/interface/boundaries.mjs +219 -0
  14. package/interface/brand.mjs +215 -0
  15. package/interface/cli/freshness.mjs +59 -0
  16. package/interface/cli/guard.mjs +6 -0
  17. package/interface/cli/mentura.mjs +82 -11
  18. package/interface/cli/naive.mjs +44 -14
  19. package/interface/context.mjs +525 -0
  20. package/interface/demo/board.mjs +9 -8
  21. package/interface/demo/decision-harness.mjs +4 -3
  22. package/interface/dev/setup.mjs +84 -0
  23. package/interface/gate-hooks.mjs +37 -0
  24. package/interface/harness/evals.mjs +10 -9
  25. package/interface/harness/run.mjs +16 -15
  26. package/interface/instrumentation.mjs +87 -0
  27. package/interface/kit-boundary.mjs +73 -0
  28. package/interface/layer.json +8 -2
  29. package/interface/mcp/server.mjs +47 -7
  30. package/interface/package-routes.mjs +197 -0
  31. package/interface/provenance.mjs +28 -4
  32. package/interface/reads.mjs +174 -0
  33. package/interface/release-card.mjs +43 -0
  34. package/interface/router.mjs +110 -36
  35. package/interface/skills/naive-setup/SKILL.md +71 -0
  36. package/interface/skills/naive-setup/artifact.html +83 -0
  37. package/interface/vitals.mjs +116 -0
  38. package/interface/writes.mjs +190 -0
  39. package/knowledge/connectors/catalog.mjs +3 -2
  40. package/knowledge/connectors/filesystem.mjs +10 -5
  41. package/knowledge/connectors/index.mjs +3 -2
  42. package/knowledge/freshness.json +85 -0
  43. package/knowledge/layer.json +11 -3
  44. package/knowledge/templates/glossary.md +23 -0
  45. package/operations/layer.json +10 -3
  46. package/operations/marketing/check-public.mjs +7 -6
  47. package/operations/marketing/site/plan.mjs +4 -3
  48. package/operations/marketing/site/presence.mjs +4 -3
  49. package/operations/templates/marketing-brand.md +46 -0
  50. package/operations/templates/public-assets.md +39 -0
  51. package/package.json +3 -1
  52. package/packages/catalog.json +8 -0
  53. package/packages/customers/package.json +21 -1
  54. package/packages/deals/package.json +27 -0
  55. package/packages/decisions/package.json +12 -1
  56. package/packages/finance/frames/fund-without-ceding-control.frame.json +177 -0
  57. package/packages/foundation/package.json +8 -1
  58. package/packages/fund/package.json +48 -0
  59. package/packages/marketing/package.json +10 -4
  60. package/packages/money/package.json +24 -3
  61. package/packages/validation/package.json +13 -0
  62. package/platform/README.md +6 -0
  63. package/platform/adapter.json +15 -0
  64. package/platform/integrations/remote-image.mjs +79 -0
  65. package/platform/integrations/rho.mjs +9 -7
  66. package/platform/kits/README.md +8 -1
  67. package/platform/kits/agent-upgrade/kit.json +15 -0
  68. package/platform/kits/agent-upgrade/run.mjs +82 -0
  69. package/platform/kits/agent-upgrade/stand-in-agent.mjs +32 -0
  70. package/platform/kits/validation-kickoff/kit.json +29 -0
  71. package/platform/webhooks/server.mjs +70 -0
  72. package/purpose/frameworks/idea-map/ideamap.mjs +4 -2
  73. package/purpose/frameworks/idea-map/route.mjs +5 -4
  74. package/purpose/frameworks/product-anatomy/anatomy.mjs +2 -1
  75. package/schema/README.md +1 -0
  76. package/schema/access.mjs +58 -0
  77. package/schema/adapter.schema.json +33 -0
  78. package/schema/add-department.mjs +3 -2
  79. package/schema/assess-problem.mjs +3 -2
  80. package/schema/assets.mjs +213 -0
  81. package/schema/brand.mjs +374 -0
  82. package/schema/build-release.mjs +61 -0
  83. package/schema/caching.mjs +506 -0
  84. package/schema/check-assets.mjs +24 -0
  85. package/schema/check-brand.mjs +24 -0
  86. package/schema/check-config.mjs +111 -0
  87. package/schema/check-data-model.mjs +2 -1
  88. package/schema/check-data-packages.mjs +5 -4
  89. package/schema/check-execution.mjs +4 -3
  90. package/schema/check-nextjs-status.mjs +3 -1
  91. package/schema/check-people.mjs +84 -0
  92. package/schema/check-private.mjs +2 -1
  93. package/schema/check-ready.mjs +6 -5
  94. package/schema/check-sources.mjs +11 -2
  95. package/schema/check-where-used.mjs +4 -3
  96. package/schema/data-model.json +334 -8
  97. package/schema/deal.schema.json +80 -0
  98. package/schema/decision-analytics.mjs +2 -1
  99. package/schema/decisions.mjs +21 -12
  100. package/schema/examples/frames/judge.mjs +10 -0
  101. package/schema/examples/venture-type/jev.mjs +12 -0
  102. package/schema/examples/workspace/ventures/acme-labs/AGENTS.md +17 -0
  103. package/schema/examples/workspace/ventures/acme-labs/accounts/.gitkeep +0 -0
  104. package/schema/examples/workspace/ventures/acme-labs/decisions/discovery-goal.decision.json +23 -0
  105. package/schema/examples/workspace/ventures/acme-labs/decisions/statement-icp.decision.json +22 -0
  106. package/schema/examples/workspace/ventures/acme-labs/decisions/statement-problem.decision.json +22 -0
  107. package/schema/examples/workspace/ventures/acme-labs/decisions/venture.team.decision.json +37 -0
  108. package/schema/examples/workspace/ventures/acme-labs/knowledge/README.md +3 -0
  109. package/schema/examples/workspace/ventures/acme-labs/knowledge/sources.json +46 -0
  110. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000001.json +12 -0
  111. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000002.json +12 -0
  112. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000003.json +12 -0
  113. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000004.json +12 -0
  114. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000005.json +12 -0
  115. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000006.json +12 -0
  116. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000007.json +12 -0
  117. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000008.json +12 -0
  118. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000009.json +12 -0
  119. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000010.json +12 -0
  120. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000011.json +12 -0
  121. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000012.json +12 -0
  122. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000013.json +12 -0
  123. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000014.json +12 -0
  124. package/schema/examples/workspace/ventures/acme-labs/money/_streams/transactions/stream.json +5 -0
  125. package/schema/examples/workspace/ventures/acme-labs/money/_summary.json +30 -0
  126. package/schema/examples/workspace/ventures/acme-labs/operations/departments/customer-service/README.md +5 -0
  127. package/schema/examples/workspace/ventures/acme-labs/operations/departments/customer-service/department.json +5 -0
  128. package/schema/examples/workspace/ventures/acme-labs/operations/departments/finance/README.md +5 -0
  129. package/schema/examples/workspace/ventures/acme-labs/operations/departments/finance/department.json +5 -0
  130. package/schema/examples/workspace/ventures/acme-labs/operations/departments/human-capital/README.md +5 -0
  131. package/schema/examples/workspace/ventures/acme-labs/operations/departments/human-capital/department.json +5 -0
  132. package/schema/examples/workspace/ventures/acme-labs/operations/departments/it/README.md +5 -0
  133. package/schema/examples/workspace/ventures/acme-labs/operations/departments/it/department.json +5 -0
  134. package/schema/examples/workspace/ventures/acme-labs/operations/departments/marketing-and-sales/README.md +5 -0
  135. package/schema/examples/workspace/ventures/acme-labs/operations/departments/marketing-and-sales/department.json +5 -0
  136. package/schema/examples/workspace/ventures/acme-labs/operations/departments/products/README.md +5 -0
  137. package/schema/examples/workspace/ventures/acme-labs/operations/departments/products/department.json +5 -0
  138. package/schema/examples/workspace/ventures/acme-labs/operations/departments/strategy/README.md +5 -0
  139. package/schema/examples/workspace/ventures/acme-labs/operations/departments/strategy/department.json +5 -0
  140. package/schema/examples/workspace/ventures/acme-labs/people/founders/maya-chen.json +15 -0
  141. package/schema/examples/workspace/ventures/acme-labs/people/people.json +22 -0
  142. package/schema/examples/workspace/ventures/acme-labs/pillars/brand.md +5 -0
  143. package/schema/examples/workspace/ventures/acme-labs/pillars/business.md +5 -0
  144. package/schema/examples/workspace/ventures/acme-labs/pillars/product.md +5 -0
  145. package/schema/examples/workspace/ventures/acme-labs/products/.gitkeep +0 -0
  146. package/schema/examples/workspace/ventures/acme-labs/products/decisions/product.core-product.decision.json +28 -0
  147. package/schema/examples/workspace/ventures/acme-labs/products/decisions/product.exists.decision.json +38 -0
  148. package/schema/examples/workspace/ventures/acme-labs/products/decisions/product.flagship.decision.json +27 -0
  149. package/schema/examples/workspace/ventures/acme-labs/profile.md +12 -0
  150. package/schema/examples/workspace/ventures/acme-labs/provenance.jsonl +12 -0
  151. package/schema/examples/workspace/ventures/acme-labs/raw/dump.md +6 -0
  152. package/schema/examples/workspace/ventures/acme-labs/venture.json +27 -0
  153. package/schema/examples/workspace/ventures/northstar/AGENTS.md +17 -0
  154. package/schema/examples/workspace/ventures/northstar/accounts/.gitkeep +0 -0
  155. package/schema/examples/workspace/ventures/northstar/deals/ductline.json +8 -0
  156. package/schema/examples/workspace/ventures/northstar/deals/kilnworks.json +14 -0
  157. package/schema/examples/workspace/ventures/northstar/decisions/venture.team.decision.json +31 -0
  158. package/schema/examples/workspace/ventures/northstar/evidence/README.md +3 -0
  159. package/schema/examples/workspace/ventures/northstar/fund/decisions/terms.decision.json +27 -0
  160. package/schema/examples/workspace/ventures/northstar/fund/decisions/terms.first-close.decision.json +27 -0
  161. package/schema/examples/workspace/ventures/northstar/fund/decisions/thesis.decision.json +26 -0
  162. package/schema/examples/workspace/ventures/northstar/fund/terms.json +22 -0
  163. package/schema/examples/workspace/ventures/northstar/fund/thesis.json +22 -0
  164. package/schema/examples/workspace/ventures/northstar/governance/README.md +3 -0
  165. package/schema/examples/workspace/ventures/northstar/governance/departments/external-relationships/charter.md +5 -0
  166. package/schema/examples/workspace/ventures/northstar/governance/departments/external-relationships/department.json +5 -0
  167. package/schema/examples/workspace/ventures/northstar/governance/departments/finance/charter.md +5 -0
  168. package/schema/examples/workspace/ventures/northstar/governance/departments/finance/department.json +5 -0
  169. package/schema/examples/workspace/ventures/northstar/governance/departments/human-capital/charter.md +5 -0
  170. package/schema/examples/workspace/ventures/northstar/governance/departments/human-capital/department.json +5 -0
  171. package/schema/examples/workspace/ventures/northstar/governance/departments/risk-and-compliance/charter.md +5 -0
  172. package/schema/examples/workspace/ventures/northstar/governance/departments/risk-and-compliance/department.json +5 -0
  173. package/schema/examples/workspace/ventures/northstar/governance/departments/strategy/charter.md +5 -0
  174. package/schema/examples/workspace/ventures/northstar/governance/departments/strategy/department.json +5 -0
  175. package/schema/examples/workspace/ventures/northstar/knowledge/README.md +3 -0
  176. package/schema/examples/workspace/ventures/northstar/operations/README.md +3 -0
  177. package/schema/examples/workspace/ventures/northstar/operations/departments/external-relationships/README.md +5 -0
  178. package/schema/examples/workspace/ventures/northstar/operations/departments/external-relationships/department.json +5 -0
  179. package/schema/examples/workspace/ventures/northstar/operations/departments/finance/README.md +5 -0
  180. package/schema/examples/workspace/ventures/northstar/operations/departments/finance/department.json +5 -0
  181. package/schema/examples/workspace/ventures/northstar/operations/departments/human-capital/README.md +5 -0
  182. package/schema/examples/workspace/ventures/northstar/operations/departments/human-capital/department.json +5 -0
  183. package/schema/examples/workspace/ventures/northstar/operations/departments/risk-and-compliance/README.md +5 -0
  184. package/schema/examples/workspace/ventures/northstar/operations/departments/risk-and-compliance/department.json +5 -0
  185. package/schema/examples/workspace/ventures/northstar/operations/departments/strategy/README.md +5 -0
  186. package/schema/examples/workspace/ventures/northstar/operations/departments/strategy/department.json +5 -0
  187. package/schema/examples/workspace/ventures/northstar/people/founders/ana-ruiz.json +13 -0
  188. package/schema/examples/workspace/ventures/northstar/people/founders/ben-okafor.json +7 -0
  189. package/schema/examples/workspace/ventures/northstar/people/people.json +27 -0
  190. package/schema/examples/workspace/ventures/northstar/pillars/brand.md +5 -0
  191. package/schema/examples/workspace/ventures/northstar/pillars/business.md +5 -0
  192. package/schema/examples/workspace/ventures/northstar/pillars/product.md +5 -0
  193. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/AGENTS.md +17 -0
  194. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/accounts/.gitkeep +0 -0
  195. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/decisions/venture.team.decision.json +31 -0
  196. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/knowledge/README.md +3 -0
  197. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/customer-service/README.md +5 -0
  198. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/customer-service/department.json +5 -0
  199. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/finance/README.md +5 -0
  200. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/finance/department.json +5 -0
  201. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/human-capital/README.md +5 -0
  202. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/human-capital/department.json +5 -0
  203. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/it/README.md +5 -0
  204. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/it/department.json +5 -0
  205. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/marketing-and-sales/README.md +5 -0
  206. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/marketing-and-sales/department.json +5 -0
  207. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/products/README.md +5 -0
  208. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/products/department.json +5 -0
  209. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/strategy/README.md +5 -0
  210. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/strategy/department.json +5 -0
  211. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/pillars/brand.md +5 -0
  212. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/pillars/business.md +5 -0
  213. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/pillars/product.md +5 -0
  214. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/.gitkeep +0 -0
  215. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/decisions/product.core-product.decision.json +28 -0
  216. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/decisions/product.exists.decision.json +32 -0
  217. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/decisions/product.flagship.decision.json +27 -0
  218. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/profile.md +5 -0
  219. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/venture.json +27 -0
  220. package/schema/examples/workspace/ventures/northstar/products/.gitkeep +0 -0
  221. package/schema/examples/workspace/ventures/northstar/profile.md +5 -0
  222. package/schema/examples/workspace/ventures/northstar/provenance.jsonl +3 -0
  223. package/schema/examples/workspace/ventures/northstar/purpose/README.md +3 -0
  224. package/schema/examples/workspace/ventures/northstar/venture.json +49 -0
  225. package/schema/foundation.mjs +8 -7
  226. package/schema/founder.schema.json +72 -0
  227. package/schema/frame.schema.json +240 -0
  228. package/schema/frames.mjs +229 -0
  229. package/schema/freshness-log.schema.json +40 -0
  230. package/schema/freshness-profiles.schema.json +44 -0
  231. package/schema/freshness.mjs +474 -0
  232. package/schema/fund-terms.schema.json +107 -0
  233. package/schema/fund-thesis.schema.json +87 -0
  234. package/schema/gate-drafts.mjs +131 -0
  235. package/schema/gate.mjs +105 -11
  236. package/schema/glossary.schema.json +25 -0
  237. package/schema/init-venture.mjs +69 -21
  238. package/schema/judgment.schema.json +38 -0
  239. package/schema/kit.schema.json +47 -0
  240. package/schema/kits.mjs +24 -0
  241. package/schema/knowledge-sources.schema.json +145 -15
  242. package/schema/layer.json +16 -3
  243. package/schema/marketing-brand.schema.json +116 -0
  244. package/schema/migrate-venture.mjs +8 -7
  245. package/schema/migrations/kit-to-package.mjs +95 -0
  246. package/schema/migrations/package-layout.mjs +90 -0
  247. package/schema/model-steps.mjs +718 -0
  248. package/schema/money-summary.schema.json +138 -0
  249. package/schema/naive-config.mjs +166 -0
  250. package/schema/naive-config.schema.json +268 -0
  251. package/schema/need-now.json +171 -0
  252. package/schema/need-now.mjs +8 -6
  253. package/schema/packages.mjs +21 -15
  254. package/schema/payouts.mjs +3 -2
  255. package/schema/people.schema.json +85 -0
  256. package/schema/port-venture.mjs +206 -132
  257. package/schema/public-assets.schema.json +76 -0
  258. package/schema/release-read.mjs +112 -0
  259. package/schema/releases.mjs +498 -0
  260. package/schema/run-handoff.mjs +5 -4
  261. package/schema/setup.mjs +165 -0
  262. package/schema/stale.mjs +112 -0
  263. package/schema/start-release.mjs +41 -0
  264. package/schema/structure.mjs +187 -0
  265. package/schema/systems-catalog.json +126 -0
  266. package/schema/systems-catalog.schema.json +78 -0
  267. package/schema/time-to-ready.mjs +7 -6
  268. package/schema/trigger.schema.json +44 -0
  269. package/schema/triggers.mjs +238 -0
  270. package/schema/upgrade-registry.schema.json +60 -0
  271. package/schema/upgrade-venture.mjs +96 -118
  272. package/schema/upgrades.json +69 -0
  273. package/schema/upgrades.mjs +386 -0
  274. package/schema/validate.py +19 -10
  275. package/schema/venture-paths.mjs +85 -0
  276. package/schema/venture-role.schema.json +64 -0
  277. package/schema/venture-type.mjs +62 -0
  278. package/schema/venture-types.json +346 -0
  279. package/schema/venture-types.schema.json +129 -0
  280. package/schema/venture.schema.json +61 -0
  281. package/schema/workspace.schema.json +5 -1
@@ -0,0 +1,525 @@
1
+ #!/usr/bin/env node
2
+ // Context: what the router sends an agent, in what order, and what it costs. Naive's translation of
3
+ // Next.js Linking and Navigating, layouts that keep their state, prefetching, streaming and bundle
4
+ // size, for a client that is a model instead of a browser. Code decides everything here; no model
5
+ // is called.
6
+ //
7
+ // navigate read a route the AI-native way: header first, then the layout (once per session),
8
+ // the template (fresh every time), the record, and prefetched refs. Reached as the
9
+ // router actions navigate and head, the CLI and the MCP tool mentura_read.
10
+ // header first (O-4) a record's header (what it is, its state, its owner, its size) comes before
11
+ // its body; head stops there, so an agent can decide not to read on
12
+ // layout (L-2, N-7) the stable context, split into parts (the layer and package stack, each
13
+ // AGENTS.md, the merged instructions, the record's schema and gate). In a session each
14
+ // part is sent once and only again when it changes; a sibling route keeps the shared
15
+ // parts and swaps only the record's own
16
+ // template (P-12, F-6) what is recomputed on every action: the need-now ranking and the open
17
+ // decisions. It is never in the layout, and it runs with a time limit, so a slow
18
+ // computation never blocks the read
19
+ // prefetch (N-3, L-12) short summaries of the routes the agent is likely to need next: the
20
+ // record's refs and links, the records it reads first and the ones that wait on it
21
+ // (the after links in packages/catalog.json), and the need-now next step. Confirmed
22
+ // records come in full when small, changing ones send only their state (N-4); large
23
+ // collections send a count unless the task names them (N-12); every entry costs
24
+ // tokens from the prefetch budget (C-16)
25
+ // budget (N-13) each route has a context budget in tokens, measured like bundle size; every
26
+ // answer reports what it used, and budget() reports every route of a venture with its
27
+ // largest part and where to move it (S-14: give context as low in the tree as it is
28
+ // needed; F-6: keep computed values out of layouts)
29
+ // streaming (N-5, N-9) parts are handed to onPart as they are ready; a computed (command) route
30
+ // answers at once with its state and the last result, then the new one. runStreaming
31
+ // and batch report progress, which the MCP server sends as progress notifications
32
+ //
33
+ // Settings come from "context" in the venture's naive.config.json:
34
+ // { "context": { "budget": 6000, "routes": { "purpose/**": 4000 }, "prefetch_budget": 1500,
35
+ // "large_collection": 20, "full_max": 800, "template_ms": 3000 } }
36
+ // A scoped instruction applies only under its routes: a line "[purpose/validation/**] key: value" in
37
+ // any AGENTS.md (the router's plain layout ignores it, so other routes do not pay for it).
38
+ //
39
+ // node interface/context.mjs <venture dir> budget [--check] [--json]
40
+
41
+ import { execFile, spawn } from "node:child_process";
42
+ import { createHash } from "node:crypto";
43
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
44
+ import { tmpdir } from "node:os";
45
+ import { dirname, join, relative } from "node:path";
46
+ import { call, draftOf, gateOf, glob, layout as routerLayout, resolve, routes, ventureDir } from "./router.mjs";
47
+ import { entries as provEntries } from "./provenance.mjs";
48
+ import { tokens } from "./instrumentation.mjs";
49
+ import { ventureFile } from "../schema/venture-paths.mjs";
50
+
51
+ const here = dirname(new URL(import.meta.url).pathname);
52
+ const repoRoot = join(here, "..");
53
+ const load = (p) => JSON.parse(readFileSync(p, "utf8"));
54
+ const sha = (x) => createHash("sha256").update(typeof x === "string" ? x : JSON.stringify(x)).digest("hex").slice(0, 16);
55
+
56
+ export const DEFAULTS = { budget: 6000, prefetch_budget: 1500, large_collection: 20, full_max: 800, template_ms: 3000 };
57
+ // Keys that hold computed values: they belong in the template, never in a layout (F-6).
58
+ export const COMPUTED = ["need_now", "need-now", "next", "ranking", "rank", "open_decisions", "decisions", "status", "todo", "due"];
59
+
60
+ /** The venture's context settings: the defaults, overridden by naive.config.json context. */
61
+ export function settings(dir) {
62
+ const f = dir && join(ventureFile(dir, "naive.config.json"));
63
+ let c = {};
64
+ try { if (f && existsSync(f)) c = load(f).context ?? {}; } catch {}
65
+ return { ...DEFAULTS, ...c };
66
+ }
67
+
68
+ /** A route's context budget in tokens: the first matching glob in context.routes, else the default. */
69
+ export function budgetFor(set, route) {
70
+ const hit = Object.entries(set.routes ?? {}).find(([g]) => glob(g).test(route));
71
+ return hit ? hit[1] : set.budget;
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------------------------
75
+ // Header first (O-4)
76
+
77
+ /** The state of one venture file: missing, draft waiting, agent-written, confirmed, baseline or unlogged. */
78
+ export function stateOf(dir, file, draft) {
79
+ if (draft && existsSync(ventureFile(dir, draft))) return { state: "draft waiting", draft };
80
+ if (!file || !existsSync(ventureFile(dir, file))) return { state: "missing" };
81
+ let state = "unlogged", last = null;
82
+ for (const e of provEntries(dir).filter((x) => x.entity === file)) {
83
+ last = e;
84
+ if (e.activity === "baseline") state = "baseline";
85
+ else if (e.activity === "accept" || e.activity === "confirm" || e.agent?.kind === "person") state = "confirmed";
86
+ else if (e.agent?.kind === "agent") state = "agent-written";
87
+ }
88
+ return { state, ...(last ? { updated: last.at, by: `${last.agent?.kind ?? "unknown"}${last.agent?.id ? ` ${last.agent.id}` : ""}` } : {}) };
89
+ }
90
+
91
+ const TITLE = ["name", "title", "statement", "claim", "end", "question", "promise", "summary", "description", "id"];
92
+ /** A one-line summary of a record: its first naming field, cut to 140 characters. */
93
+ export function titleOf(rec) {
94
+ if (rec === null || rec === undefined) return null;
95
+ if (typeof rec === "string") return rec.split("\n").find((l) => l.trim())?.replace(/^#+\s*/, "").slice(0, 140) ?? null;
96
+ if (typeof rec !== "object" || Array.isArray(rec)) return null;
97
+ for (const k of TITLE) if (typeof rec[k] === "string" && rec[k].trim()) return rec[k].slice(0, 140);
98
+ for (const v of Object.values(rec)) if (v && typeof v === "object" && !Array.isArray(v)) { const t = titleOf(v); if (t) return t; }
99
+ const s = Object.values(rec).find((v) => typeof v === "string" && v.trim());
100
+ return s ? s.slice(0, 140) : null;
101
+ }
102
+
103
+ function ownerOf(r, dir, route) {
104
+ const g = r.kind === "record" ? gateOf(r, dir) : null;
105
+ if (g?.owner) return g.owner;
106
+ const plan = [join(ventureFile(dir, "people/team-plan.json")), join(ventureFile(dir, "operations/team-plan.json"))].find((p) => existsSync(p));
107
+ if (!plan) return null;
108
+ try { return load(plan).roles.find((x) => (x.routes?.write ?? []).some((gl) => glob(gl).test(route)))?.id ?? null; } catch { return null; }
109
+ }
110
+
111
+ function fileOfRead(res) {
112
+ const a = res?.source?.address;
113
+ return a ? a.replace(/^[a-z0-9-]+:\/\//, "") : null;
114
+ }
115
+
116
+ /** The header of a read: what it is, its state, its owner and what reading it costs, without its body. */
117
+ export function headerOf({ r, route, dir, venture, workspace, res }) {
118
+ const h = { route, record: r.record ?? null, kind: r.kind, ...(r.collection ? { collection: r.collection } : {}) };
119
+ if (r.kind === "command") return { ...h, state: "computed", note: r.note };
120
+ if (r.collection === "list") {
121
+ const n = res?.result ? Object.keys(res.result).length : 0;
122
+ return { ...h, state: n ? "collection" : "missing", count: n, owner: ownerOf(r, dir, route), tokens: tokens(res?.result) };
123
+ }
124
+ const d = draftOf(route, venture, workspace);
125
+ const file = (res?.result !== null && res?.result !== undefined && (fileOfRead(res) ?? d?.file)) || d?.file;
126
+ const st = stateOf(dir, res?.result === null ? null : file, d?.draft);
127
+ if (st.state === "missing" && res?.result !== null && res?.result !== undefined) st.state = "present";
128
+ return { ...h, ...st, ...(st.state === "draft waiting" && res?.result ? { has_record: true } : {}), owner: ownerOf(r, dir, route), title: titleOf(res?.result), tokens: tokens(res?.result),
129
+ ...(res?.source ? { source: res.source.source, version: res.source.version } : {}) };
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------------------------
133
+ // Layout parts, sessions and scoped instructions (L-2, N-7, S-14)
134
+
135
+ /** Every AGENTS.md from the venture root down the route's segments, nearest last. */
136
+ export function chainOf(dir, route) {
137
+ const out = [];
138
+ const add = (rel) => { const f = ventureFile(dir, rel); if (existsSync(f)) out.push({ path: rel, text: readFileSync(f, "utf8") }); };
139
+ add("AGENTS.md");
140
+ let acc = "";
141
+ for (const p of route.split("/").filter(Boolean)) { acc = acc ? `${acc}/${p}` : p; add(`${acc}/AGENTS.md`); }
142
+ return out;
143
+ }
144
+
145
+ const PLAIN = /^([a-z][a-z0-9_-]*)\s*:\s*(.+)$/;
146
+ const SCOPED = /^\[([^\]]+)\]\s*([a-z][a-z0-9_-]*)\s*:\s*(.+)$/;
147
+ /** The merged key: value instructions for a route; a scoped line applies only where its glob matches. Nearest wins. */
148
+ export function instructionsFor(chain, route) {
149
+ const out = {};
150
+ for (const f of chain) for (const line of f.text.split("\n")) {
151
+ const s = line.match(SCOPED);
152
+ if (s) { if (s[1].split(",").map((x) => x.trim()).some((g) => glob(g).test(route))) out[s[2]] = s[3].trim(); continue; }
153
+ const m = line.match(PLAIN);
154
+ if (m) out[m[1]] = m[2].trim();
155
+ }
156
+ return out;
157
+ }
158
+
159
+ /** The stable layout of a route, as named parts. Nothing computed is in it (F-6). */
160
+ export function layoutParts(r, dir) {
161
+ const base = routerLayout(r, null); // no venture: no need-now spawn, no chain; both come from here instead
162
+ const chain = chainOf(dir, r.route.replace(/\/\[id\]$/, ""));
163
+ // One part per layer or package in the stack, so routes in the same layer share theirs.
164
+ const parts = Object.fromEntries(base.stack.map((k) => [`stack:${k.path}`, k]));
165
+ for (const f of chain) parts[`chain:${f.path}`] = f.text;
166
+ parts.instructions = instructionsFor(chain, r.route);
167
+ parts[`record:${r.route}`] = r.kind === "record"
168
+ ? { schema: r.schema, template: r.template, checks: r.checks, gate: gateOf(r, dir), package: base.package, groups: r.groups, note: r.note }
169
+ : { note: r.note, script: r.script ?? null };
170
+ return parts;
171
+ }
172
+
173
+ const sessionDir = () => process.env.NAIVE_SESSION_DIR ?? join(tmpdir(), "naive-sessions");
174
+ const sessionFile = (dir, session) => join(sessionDir(), `${sha(`${dir}\0${session}`)}.json`);
175
+ /** What a session has been sent: part key to hash, and the routes it visited. */
176
+ export function sessionOf(dir, session) {
177
+ try { return load(sessionFile(dir, session)); } catch { return { session, sent: {}, routes: [] }; }
178
+ }
179
+ function saveSession(dir, s) { mkdirSync(sessionDir(), { recursive: true }); writeFileSync(sessionFile(dir, s.session), JSON.stringify(s)); }
180
+
181
+ /**
182
+ * The layout to send: every part without a session; with one, only the parts this session has not
183
+ * seen or that changed since (L-2). A sibling route keeps the shared parts and swaps the record's (N-7).
184
+ */
185
+ export function layoutFor(r, dir, session) {
186
+ const parts = layoutParts(r, dir);
187
+ if (!session) return { parts };
188
+ const s = sessionOf(dir, session);
189
+ const send = {}, kept = [], changed = [];
190
+ for (const [k, v] of Object.entries(parts)) {
191
+ const h = sha(v);
192
+ if (s.sent[k] === h) kept.push(k);
193
+ else { send[k] = v; if (s.sent[k]) changed.push(k); s.sent[k] = h; }
194
+ }
195
+ const first = s.routes.length === 0;
196
+ s.routes = [...s.routes.filter((x) => x !== r.route), r.route].slice(-50);
197
+ saveSession(dir, s);
198
+ return { session, first, parts: send, kept, ...(changed.length ? { changed } : {}) };
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------------------------
202
+ // Template: recomputed on every action, with a time limit (P-12, F-6)
203
+
204
+ function runJson(script, args, ms) {
205
+ return new Promise((done) => {
206
+ const t0 = Date.now();
207
+ execFile(process.execPath, [join(repoRoot, script), ...args], { timeout: ms, maxBuffer: 64 * 1024 * 1024 }, (err, stdout) => {
208
+ if (err?.killed) return done({ ok: false, timed_out: true, ms: Date.now() - t0 });
209
+ try { done({ ok: true, json: JSON.parse(stdout), ms: Date.now() - t0 }); } catch { done({ ok: false, error: String(err?.message ?? "no JSON").split("\n")[0], ms: Date.now() - t0 }); }
210
+ });
211
+ });
212
+ }
213
+
214
+ /** The template for a route: the need-now ranking and open decisions, computed now. Parts that run past template_ms come back pending. */
215
+ export async function templateFor(r, dir, set = settings(dir)) {
216
+ const [nn, ds] = await Promise.all([runJson("schema/need-now.mjs", [dir, "--json"], set.template_ms), runJson("schema/decisions.mjs", [dir, "--json"], set.template_ms)]);
217
+ const out = { computed_at: new Date().toISOString() };
218
+ const pending = [];
219
+ if (nn.ok) {
220
+ const row = nn.json.rows.find((x) => x.type === r.record);
221
+ out.need_now = { phase: nn.json.phase, ...(row ? { this: { status: row.status, now: row.now, why: row.why } } : {}),
222
+ suggest_now: nn.json.rows.filter((x) => x.status === "suggest now").sort((a, b) => b.now - a.now).slice(0, 5).map((x) => x.type) };
223
+ } else pending.push(`need_now (${nn.timed_out ? `over ${set.template_ms}ms` : nn.error})`);
224
+ if (ds.ok) {
225
+ const open = (ds.json.decisions ?? []).filter((d) => ["now", "overdue"].includes(d.render) && d.source !== "root");
226
+ out.decisions = { open: open.length, overdue: open.filter((d) => d.render === "overdue").map((d) => d.id), now: open.slice(0, 5).map((d) => ({ id: d.id, question: d.question })) };
227
+ } else pending.push(`decisions (${ds.timed_out ? `over ${set.template_ms}ms` : ds.error})`);
228
+ if (pending.length) Object.assign(out, { pending, note: "still computing: run system/need-now or governance/decisions for the rest; the read did not wait" });
229
+ return out;
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------------------------
233
+ // Prefetch (N-3, N-4, N-12, L-12, C-16)
234
+
235
+ const MODEL = () => load(join(repoRoot, "schema/data-model.json"));
236
+ const AFTER = () => { try { return load(join(repoRoot, "packages/catalog.json")).records ?? {}; } catch { return {}; } };
237
+ const valuesAt = (o, path) => path.split(".").reduce((xs, k) => xs.flatMap((x) => (Array.isArray(x) ? x : [x]).flatMap((y) => (y && typeof y === "object" && y[k] !== undefined ? [y[k]] : []))), [o]).flat();
238
+ const ROUTEISH = /^[a-z][a-z0-9-]*(\/[a-z0-9.\-[\]]+)+$/;
239
+ function strings(o, out = [], depth = 0) {
240
+ if (out.length > 300 || depth > 6) return out;
241
+ if (typeof o === "string") out.push(o);
242
+ else if (Array.isArray(o)) for (const x of o) strings(x, out, depth + 1);
243
+ else if (o && typeof o === "object") for (const x of Object.values(o)) strings(x, out, depth + 1);
244
+ return out;
245
+ }
246
+ /** The route a record type is read at: its list route for a collection, else its one route. */
247
+ export function routeOfType(type, id) {
248
+ const rs = routes().filter((x) => x.kind === "record" && x.record === type);
249
+ const item = rs.find((x) => x.collection === "item"), list = rs.find((x) => x.collection === "list"), one = rs.find((x) => !x.collection);
250
+ if (item && typeof id === "string" && /^[a-z0-9][a-z0-9-]*$/.test(id)) return item.route.replace("[id]", id);
251
+ return (list ?? one)?.route ?? null;
252
+ }
253
+
254
+ /** The routes an agent is likely to need after this one, most likely first, each with why. */
255
+ export function candidates(r, record, template, { hover = [] } = {}) {
256
+ const out = [];
257
+ const add = (route, why) => { if (route && route !== r.route && !out.some((x) => x.route === route)) out.push({ route, why }); };
258
+ for (const h of hover) add(h, "asked");
259
+ if (r.kind === "record" && record && typeof record === "object") {
260
+ const def = MODEL().records.find((x) => x.id === r.record);
261
+ for (const ref of def?.refs ?? []) {
262
+ const vals = valuesAt(record, ref.field).filter((v) => typeof v === "string");
263
+ for (const v of vals) add(routeOfType(ref.to, v), `ref ${ref.field}`);
264
+ }
265
+ // Links: a string in the record that is itself a route (foundation/icp), Naive's <Link>.
266
+ for (const s of strings(record).filter((x) => ROUTEISH.test(x))) { const { r: hit } = resolve(s); if (hit?.kind === "record") add(s, "link"); }
267
+ }
268
+ const after = AFTER();
269
+ for (const t of after[r.record]?.after ?? []) add(routeOfType(t), "reads first");
270
+ const next = template?.need_now?.suggest_now?.find((t) => t !== r.record);
271
+ if (next) add(routeOfType(next), "next step");
272
+ for (const [t, v] of Object.entries(after)) if ((v.after ?? []).includes(r.record)) add(routeOfType(t), "waits on this");
273
+ return out;
274
+ }
275
+
276
+ /** Prefetches the candidates within the prefetch budget: full, summary, state or count, by what each is and what is left. */
277
+ export async function prefetch({ r, record, template, venture, dir, workspace, actor_id, task = "", hover = [], set }) {
278
+ const cands = candidates(r, record, template, { hover });
279
+ const named = (route) => hover.includes(route) || (task && (task.includes(route) || task.includes(route.split("/").at(-1))));
280
+ const entries = [], dropped = [];
281
+ let spent = 0;
282
+ for (const c of cands) {
283
+ const { r: cr } = resolve(c.route);
284
+ if (!cr) continue;
285
+ const res = await safeCall({ venture, route: c.route, action: "read", workspace, actor_id, via: "prefetch" });
286
+ let options; // most to least, each an entry the agent could receive
287
+ if (!res.ok) options = [{ ...c, state: "unreadable" }];
288
+ else if (cr.collection === "list") {
289
+ const items = res.result ? Object.entries(res.result) : [];
290
+ const counted = { ...c, state: items.length ? "collection" : "missing", count: items.length };
291
+ if (items.length > set.large_collection && !named(c.route)) options = [counted];
292
+ else options = [{ ...counted, items: items.map(([id, v]) => ({ id, title: titleOf(v) })) }, counted];
293
+ } else {
294
+ const h = headerOf({ r: cr, route: c.route, dir, venture, workspace, res });
295
+ const st = { ...c, state: h.state };
296
+ const summary = { ...st, title: h.title, tokens: h.tokens };
297
+ const stable = h.state === "confirmed";
298
+ if (h.state === "missing") options = [st];
299
+ else if ((stable || named(c.route)) && h.tokens <= set.full_max) options = [{ ...summary, full: res.result }, summary, st];
300
+ else if (stable || named(c.route)) options = [summary, st];
301
+ else options = [st]; // changing (agent-written, a draft, unlogged): only its state (N-4)
302
+ }
303
+ const fit = options.find((o) => spent + tokens(o) <= set.prefetch_budget);
304
+ if (!fit) { dropped.push(c.route); continue; }
305
+ spent += tokens(fit);
306
+ entries.push({ ...fit, cost: tokens(fit) });
307
+ }
308
+ return { entries, spent, budget: set.prefetch_budget, ...(dropped.length ? { dropped } : {}) };
309
+ }
310
+
311
+ // ---------------------------------------------------------------------------------------------
312
+ // Navigate: the whole answer, streamed part by part (N-5, N-9)
313
+
314
+ // A router call that answers a throw as an error, so one broken route never stops a navigation or a report.
315
+ const safeCall = async (args) => { try { return await call(args); } catch (e) { return { ok: false, error: String(e.message ?? e).split("\n")[0] }; } };
316
+
317
+ const runCacheFile = (dir, route) => join(sessionDir(), "runs", `${sha(`${dir}\0${route}`)}.json`);
318
+
319
+ /**
320
+ * Reads a route the AI-native way. Options: session (layout once per session), task (what the agent
321
+ * is doing; a route it names is prefetched in full), prefetch (false to turn it off, or a list of
322
+ * routes to prefetch in full, like hover), header_only (stop after the header), onPart (called with
323
+ * { part, data } as each part is ready). Returns { ok, result: { header, budget, layout, template,
324
+ * result, prefetch } } in that order.
325
+ */
326
+ export async function navigate({ venture, route, workspace, actor = "agent", actor_id, via = "router", session = process.env.MENTURA_SESSION, task, prefetch: pf = true, header_only = false, onPart, _template }) {
327
+ const { r, params } = resolve(route);
328
+ if (!r) return { ok: false, error: `no route ${route}` };
329
+ const dir = ventureDir(venture, workspace);
330
+ if (!dir) return { ok: false, error: `no venture ${venture}` };
331
+ const concrete = r.collection === "item" ? r.route.replace("[id]", params.id) : r.route;
332
+ const set = settings(dir);
333
+ const send = (part, data) => { try { onPart?.({ part, data: data === undefined ? data : JSON.parse(JSON.stringify(data)) }); } catch {} };
334
+ const out = {};
335
+
336
+ if (r.kind === "command") {
337
+ // A computed route answers at once with its state and the last result, then streams the new one (N-9).
338
+ let last = null;
339
+ try { last = load(runCacheFile(dir, concrete)); } catch {}
340
+ out.header = { ...headerOf({ r, route: concrete, dir, venture, workspace }), state: "computing", ...(last ? { last_at: last.at } : {}) };
341
+ send("header", out.header);
342
+ if (last) { out.stale_result = last.result; send("stale_result", { as_of: last.at, result: last.result }); }
343
+ if (header_only) return { ok: true, result: out };
344
+ out.layout = layoutFor(r, dir, session); send("layout", out.layout);
345
+ const res = await safeCall({ venture, route: concrete, action: "run", workspace, actor, actor_id, via });
346
+ if (!res.ok) return res;
347
+ out.header.state = res.needs_input ? "needs input" : "computed";
348
+ out.result = res.result ?? null;
349
+ if (res.needs_input) out.needs_input = res.needs_input;
350
+ else { mkdirSync(dirname(runCacheFile(dir, concrete)), { recursive: true }); writeFileSync(runCacheFile(dir, concrete), JSON.stringify({ at: new Date().toISOString(), result: res.result })); }
351
+ send("result", out.result);
352
+ out.budget = budgetOf(out, budgetFor(set, r.route));
353
+ send("budget", out.budget);
354
+ return { ok: true, result: order(out) };
355
+ }
356
+
357
+ // The template starts first, in other processes, so it runs while the record is read.
358
+ const templating = header_only ? null : _template ? Promise.resolve(_template) : templateFor(r, dir, set);
359
+ const res = await safeCall({ venture, route: concrete, action: "read", workspace, actor, actor_id, via });
360
+ if (!res.ok) return res;
361
+ out.header = headerOf({ r, route: concrete, dir, venture, workspace, res });
362
+ send("header", out.header);
363
+ if (header_only) { out.budget = budgetOf(out, budgetFor(set, r.route)); return { ok: true, result: order(out) }; }
364
+ out.layout = layoutFor(r, dir, session);
365
+ send("layout", out.layout);
366
+ out.template = await templating;
367
+ send("template", out.template);
368
+ out.result = res.result;
369
+ for (const k of ["not_found", "pending_draft", "pending_pull_requests", "source", "sources", "deprecated"]) if (res[k]) out[k] = res[k];
370
+ send("result", out.result);
371
+ if (pf !== false) {
372
+ out.prefetch = await prefetch({ r, record: res.result, template: out.template, venture, dir, workspace, actor_id, task, hover: Array.isArray(pf) ? pf : [], set });
373
+ // Over the route's budget: prefetch is the first thing given back (C-16).
374
+ const limit = budgetFor(set, r.route);
375
+ while (out.prefetch.entries.length && budgetOf(out, limit).over) out.prefetch.dropped = [...(out.prefetch.dropped ?? []), out.prefetch.entries.pop().route];
376
+ out.prefetch.spent = out.prefetch.entries.reduce((n, e) => n + e.cost, 0);
377
+ send("prefetch", out.prefetch);
378
+ }
379
+ out.budget = budgetOf(out, budgetFor(set, r.route));
380
+ send("budget", out.budget);
381
+ return { ok: true, result: order(out) };
382
+ }
383
+
384
+ const PARTS = ["header", "layout", "template", "stale_result", "result", "prefetch"];
385
+ function budgetOf(out, limit) {
386
+ const parts = Object.fromEntries(PARTS.filter((k) => out[k] !== undefined).map((k) => [k, tokens(out[k])]));
387
+ const used = Object.values(parts).reduce((a, b) => a + b, 0);
388
+ return { limit, used, over: used > limit, parts };
389
+ }
390
+ // Header first, then the budget, then the rest in the order an agent needs it.
391
+ const order = (o) => Object.fromEntries(["header", "budget", "needs_input", "layout", "template", "stale_result", "result", "not_found", "pending_draft", "pending_pull_requests", "source", "sources", "deprecated", "prefetch"].filter((k) => o[k] !== undefined).map((k) => [k, o[k]]));
392
+
393
+ /** Runs a command route in a child process and reports progress while it runs (N-5). Gates apply: it goes through the router. */
394
+ export function runStreaming({ venture, route, input, workspace, actor_id }, onProgress = () => {}, every = 250) {
395
+ return new Promise((done) => {
396
+ const args = [join(repoRoot, "interface/cli/mentura.mjs"), route, "run", "--venture", venture, "--json",
397
+ ...(input?.value !== undefined ? ["--value", String(input.value)] : []), ...(input?.as_is ? ["--as-is"] : []), ...(workspace ? ["--workspace", workspace] : [])];
398
+ const env = { ...process.env, ...(actor_id ? { MENTURA_ACTOR_ID: actor_id } : {}) };
399
+ const p = spawn(process.execPath, args, { env });
400
+ let stdout = "", stderr = "", n = 0;
401
+ const t0 = Date.now();
402
+ onProgress({ progress: 0, message: `started ${route}` });
403
+ const timer = setInterval(() => onProgress({ progress: ++n, message: `running ${route}: ${((Date.now() - t0) / 1000).toFixed(1)}s` }), every);
404
+ p.stdout.on("data", (d) => { stdout += d; });
405
+ p.stderr.on("data", (d) => {
406
+ stderr += d;
407
+ for (const line of String(d).split("\n")) { const m = line.match(/^progress\s+(\d+)\/(\d+)\s*(.*)$/); if (m) onProgress({ progress: Number(m[1]), total: Number(m[2]), message: m[3] }); }
408
+ });
409
+ p.on("close", (code) => {
410
+ clearInterval(timer);
411
+ onProgress({ progress: ++n, message: `finished ${route} in ${((Date.now() - t0) / 1000).toFixed(1)}s` });
412
+ if (code !== 0) return done({ ok: false, error: (stderr || stdout).trim() });
413
+ try { const j = JSON.parse(stdout); done(j.ok !== undefined ? j : { ok: true, result: j }); } catch { done({ ok: true, result: stdout.trim() }); }
414
+ });
415
+ });
416
+ }
417
+
418
+ /** Runs calls one after another (a batch of reads, checks or runs) and reports each as it finishes (N-5). */
419
+ export async function batch({ venture, workspace, calls = [], actor_id, via = "router" }, onProgress = () => {}) {
420
+ const results = [];
421
+ for (const [i, c] of calls.entries()) {
422
+ const r = await safeCall({ venture, workspace, actor_id, via, ...c });
423
+ results.push({ route: c.route, action: c.action, ok: r.ok, ...(r.ok ? { result: r.result } : { error: r.error }) });
424
+ onProgress({ progress: i + 1, total: calls.length, message: `${c.action} ${c.route}: ${r.ok ? "ok" : "failed"}` });
425
+ }
426
+ return { ok: results.every((x) => x.ok), result: results };
427
+ }
428
+
429
+ // ---------------------------------------------------------------------------------------------
430
+ // The context budget report (N-13, S-14, F-6)
431
+
432
+ const ADVICE = {
433
+ layout: "move procedural instructions out of AGENTS.md into the router's code or a check, and scope the rest to the routes that need them (S-14)",
434
+ template: "the template is computed every time: keep it to rankings and counts",
435
+ result: "the record is large: read its header first (head) and split it if agents rarely need all of it",
436
+ prefetch: "lower context.prefetch_budget for this route, or turn prefetch off where the refs are rarely followed",
437
+ header: "the header should be small: check the record's title field",
438
+ };
439
+
440
+ /** Every route of a venture against its context budget, with its largest part, plus the layout rules it breaks. */
441
+ export async function budget(dir) {
442
+ // The report's own reads are measurement, not an agent's work: they stay out of the events log and vitals.
443
+ const was = process.env.NAIVE_INSTRUMENT;
444
+ process.env.NAIVE_INSTRUMENT = "0";
445
+ try { return await measureBudget(dir); } finally { if (was === undefined) delete process.env.NAIVE_INSTRUMENT; else process.env.NAIVE_INSTRUMENT = was; }
446
+ }
447
+
448
+ async function measureBudget(dir) {
449
+ const v = load(join(dir, "venture.json"));
450
+ const workspace = dirname(dir);
451
+ const set = settings(dir);
452
+ const rows = [];
453
+ let tmpl = null;
454
+ for (const r of routes().filter((x) => x.kind === "record" && x.collection !== "item")) {
455
+ tmpl ??= await templateFor(r, dir, set);
456
+ const t = { ...tmpl, need_now: tmpl.need_now ? { ...tmpl.need_now, this: undefined } : undefined };
457
+ const n = await navigate({ venture: v.id, route: r.route, workspace, _template: t, via: "budget" });
458
+ if (!n.ok) { rows.push({ route: r.route, error: n.error }); continue; }
459
+ const b = n.result.budget;
460
+ const largest = Object.entries(b.parts).sort((a, c) => c[1] - a[1])[0]?.[0];
461
+ rows.push({ route: r.route, limit: b.limit, used: b.used, over: b.over, largest, ...(b.over ? { advice: ADVICE[largest] } : {}) });
462
+ }
463
+ // Layout rules, over every AGENTS.md in the venture.
464
+ const rules = [];
465
+ const agentsFiles = (d, rel = "") => readdirSync(join(d, rel), { withFileTypes: true }).flatMap((e) => e.name.startsWith(".") || e.name.startsWith("_") ? []
466
+ : e.isDirectory() ? (existsSync(join(d, rel, e.name, "venture.json")) ? [] : agentsFiles(d, rel ? `${rel}/${e.name}` : e.name)) : e.name === "AGENTS.md" ? [rel ? `${rel}/AGENTS.md` : "AGENTS.md"] : []);
467
+ for (const f of agentsFiles(dir)) {
468
+ const folder = dirname(f) === "." ? "" : dirname(f);
469
+ const text = readFileSync(ventureFile(dir, f), "utf8");
470
+ for (const line of text.split("\n")) {
471
+ const s = line.match(SCOPED), m = s ? [null, s[2], s[3]] : line.match(PLAIN);
472
+ if (!m) continue;
473
+ // F-6: a computed value in a layout blocks or goes stale; it belongs in the template.
474
+ if (COMPUTED.includes(m[1])) rules.push({ rule: "F-6", file: f, key: m[1], fix: `${m[1]} is computed: leave it to the template (need-now and decisions), not ${f}` });
475
+ // S-14: a scoped line whose routes all sit under one folder belongs in that folder's AGENTS.md.
476
+ if (s) {
477
+ const fixed = s[1].split(",").map((g) => g.trim().split("/").filter((x) => !x.includes("*")).join("/"));
478
+ const common = fixed.reduce((a, b) => { const x = a.split("/"), y = b.split("/"); let i = 0; while (i < x.length && x[i] === y[i]) i++; return x.slice(0, i).join("/"); });
479
+ if (common && common !== folder && common.startsWith(folder)) rules.push({ rule: "S-14", file: f, key: s[2], fix: `move "${line.trim()}" to ${common}/AGENTS.md, so routes outside ${common} do not pay ${tokens(line)} tokens for it` });
480
+ }
481
+ }
482
+ const t = tokens(text);
483
+ if (t > set.budget / 4) rules.push({ rule: "N-13", file: f, tokens: t, fix: `${f} is ${t} tokens, over a quarter of the ${set.budget}-token budget of every route under it: ${ADVICE.layout}` });
484
+ }
485
+ const over = rows.filter((x) => x.over);
486
+ return { venture: v.id, budget: set.budget, prefetch_budget: set.prefetch_budget, routes: rows, over: over.map((x) => x.route), rules, passed: !over.length && !rules.length };
487
+ }
488
+
489
+ // ---------------------------------------------------------------------------------------------
490
+ // Command line: the router's command route system/context, and the CLI hook for navigate, head and batch.
491
+
492
+ /** mentura <route> navigate|head --venture <id> [--session s] [--task text] [--prefetch a,b | --no-prefetch] [--stream]; mentura batch --venture <id> --input calls.json [--stream]. */
493
+ export async function cli(argv, { workspace } = {}) {
494
+ const opt = (n) => { const i = argv.indexOf(`--${n}`); return i >= 0 ? argv[i + 1] : undefined; };
495
+ const flag = (n) => argv.includes(`--${n}`);
496
+ const [route, action] = argv.filter((a, i) => !a.startsWith("--") && !(i > 0 && argv[i - 1].startsWith("--") && !["json", "stream", "no-prefetch", "header", "as-is"].includes(argv[i - 1].slice(2))));
497
+ const venture = opt("venture");
498
+ const stream = flag("stream");
499
+ const line = (x) => process.stdout.write(JSON.stringify(x) + "\n");
500
+ if (route === "batch") {
501
+ const calls = JSON.parse(readFileSync(opt("input") === "-" ? 0 : opt("input"), "utf8"));
502
+ const r = await batch({ venture, workspace, calls: calls.calls ?? calls, via: "cli" }, stream ? (p) => line({ progress: p }) : undefined);
503
+ if (stream) line({ part: "result", data: r.result }); else console.log(JSON.stringify(r.result, null, 2));
504
+ return r.ok ? 0 : 1;
505
+ }
506
+ // Through the router, so the navigation is one instrumented call with its reads under it.
507
+ const r = await call({ venture, route, action, workspace, via: "cli", actor: opt("as") === "person" ? "person" : "agent", input: { session: opt("session") ?? process.env.MENTURA_SESSION, task: opt("task"),
508
+ prefetch: flag("no-prefetch") ? false : opt("prefetch") ? opt("prefetch").split(",").filter(Boolean) : true, onPart: stream ? line : undefined } });
509
+ if (!r.ok) { console.error(r.error); return 1; }
510
+ if (!stream) console.log(JSON.stringify(r.result, null, 2));
511
+ return 0;
512
+ }
513
+
514
+ if (import.meta.url === `file://${process.argv[1]}`) {
515
+ const [dir, what] = process.argv.slice(2);
516
+ if (!dir || !existsSync(join(dir, "venture.json")) || what !== "budget") { console.error("usage: node interface/context.mjs <venture dir> budget [--check] [--json]"); process.exit(2); }
517
+ const r = await budget(dir);
518
+ if (process.argv.includes("--json")) console.log(JSON.stringify(r, null, 2));
519
+ else {
520
+ console.log(`${r.venture}: ${r.routes.length} routes, budget ${r.budget} tokens (prefetch ${r.prefetch_budget})`);
521
+ for (const x of r.routes) console.log(` ${x.error ? "err " : x.over ? "OVER" : "ok "} ${x.route.padEnd(40)} ${x.error ? `could not read: ${x.error}` : `${String(x.used).padStart(6)} / ${x.limit} largest ${x.largest}`}`);
522
+ for (const x of r.rules) console.log(` ${x.rule.padEnd(5)} ${x.file}: ${x.fix}`);
523
+ }
524
+ if (process.argv.includes("--check") && !r.passed) { console.error(`context budget failing: ${[...r.over, ...r.rules.map((x) => `${x.rule} ${x.file}`)].join(", ")}`); process.exit(1); }
525
+ }
@@ -23,6 +23,7 @@ import { scan } from "../harness/site.mjs";
23
23
  import { MATRIX } from "../../operations/marketing/site/presence.mjs";
24
24
  import { plan } from "../../operations/marketing/site/plan.mjs";
25
25
  import { packages, write as writePackage, OUT as PACKAGE } from "../../schema/packages.mjs";
26
+ import { ventureFile } from "../../schema/venture-paths.mjs";
26
27
 
27
28
  const arg = (n, d) => { const i = process.argv.indexOf(`--${n}`); return i >= 0 ? process.argv[i + 1] : d; };
28
29
  const port = Number(arg("port", 4331));
@@ -31,14 +32,14 @@ const venture = arg("venture", ventures()[0]?.id);
31
32
  function state(id) {
32
33
  const dir = ventureDir(id);
33
34
  if (!dir) return { error: `no venture ${id}` };
34
- const log = existsSync(join(dir, "provenance.jsonl")) ? readFileSync(join(dir, "provenance.jsonl"), "utf8").trim().split("\n").filter(Boolean).slice(-8).map((l) => JSON.parse(l)).reverse() : [];
35
+ const log = existsSync(join(ventureFile(dir, "provenance.jsonl"))) ? readFileSync(join(ventureFile(dir, "provenance.jsonl")), "utf8").trim().split("\n").filter(Boolean).slice(-8).map((l) => JSON.parse(l)).reverse() : [];
35
36
  // The kit for now (EE-028): what need-now says this venture needs in its phase, and which kit holds it.
36
37
  const nn = spawnSync("node", [new URL("../../schema/need-now.mjs", import.meta.url).pathname, dir, "--json"], { encoding: "utf8" });
37
38
  const kitOf = new Map(routes().filter((r) => r.record).map((r) => [r.record, { kit: r.kit, route: r.route.replace(/\/\[id\]$/, "") }]));
38
39
  const rows = nn.status === 0 ? JSON.parse(nn.stdout).rows : [];
39
40
  const kit = rows.filter((r) => r.now >= 0.5 || r.status === "coming soon").map((r) => ({ type: r.type, status: r.status, now: r.now, next: r.next, why: r.why, ...(kitOf.get(r.type) ?? {}) }));
40
41
  // Money (EE-070): only the summary, never the transactions stream; the source stays the system of record.
41
- const money = existsSync(join(dir, "money/summary.json")) ? JSON.parse(readFileSync(join(dir, "money/summary.json"), "utf8")) : null;
42
+ const money = existsSync(join(ventureFile(dir, "money/_summary.json"))) ? JSON.parse(readFileSync(join(ventureFile(dir, "money/_summary.json")), "utf8")) : null;
42
43
  return { ...decisions(dir), kit, money, log: log.map((e) => ({ at: e.at, activity: e.activity, entity: e.entity, who: `${e.agent?.kind ?? "?"}${e.agent?.id ? ` ${e.agent.id}` : ""}`, via: e.agent?.via })) };
43
44
  }
44
45
 
@@ -46,9 +47,9 @@ function state(id) {
46
47
  function ideas(id) {
47
48
  const dir = ventureDir(id);
48
49
  if (!dir) return { error: `no venture ${id}` };
49
- if (!existsSync(join(dir, MAP))) return { venture: id, waiting: "no idea dump yet" };
50
+ if (!existsSync(ventureFile(dir, MAP))) return { venture: id, waiting: "no idea dump yet" };
50
51
  const v = JSON.parse(readFileSync(join(dir, "venture.json"), "utf8"));
51
- const map = parse(readFileSync(join(dir, MAP), "utf8"));
52
+ const map = parse(readFileSync(ventureFile(dir, MAP), "utf8"));
52
53
  return { venture: id, name: v.name, phase: v.phase, meta: map.meta, ideas: map.ideas, summary: summary(map, v.phase), pillars: CONVENTIONS.pillars, departments: CONVENTIONS.departments };
53
54
  }
54
55
 
@@ -132,8 +133,8 @@ ReactDOM.createRoot(document.getElementById("flow")).render(h(App));
132
133
  function graph(id) {
133
134
  const dir = ventureDir(id);
134
135
  if (!dir) return { error: `no venture ${id}` };
135
- if (!existsSync(join(dir, GRAPH))) return { venture: id, waiting: "no idea graph yet" };
136
- return { venture: id, name: JSON.parse(readFileSync(join(dir, "venture.json"), "utf8")).name, ...JSON.parse(readFileSync(join(dir, GRAPH), "utf8")) };
136
+ if (!existsSync(ventureFile(dir, GRAPH))) return { venture: id, waiting: "no idea graph yet" };
137
+ return { venture: id, name: JSON.parse(readFileSync(join(dir, "venture.json"), "utf8")).name, ...JSON.parse(readFileSync(ventureFile(dir, GRAPH), "utf8")) };
137
138
  }
138
139
 
139
140
  const GRAPH_PAGE = MAP_PAGE.replace("<title>Idea map</title>", "<title>Idea graph</title>").replace(/<script>\nconst h=React[\s\S]*<\/script><\/body>/, `<script>
@@ -500,7 +501,7 @@ async function vendor(path) {
500
501
  function packageOf(dir, writeIt = false) {
501
502
  const r = packages(dir);
502
503
  if (writeIt) writePackage(dir, r);
503
- const has = (f) => existsSync(join(dir, f));
504
+ const has = (f) => existsSync(ventureFile(dir, f));
504
505
  let rt = null; try { rt = route(dir); } catch {}
505
506
  const d = decisions(dir).decisions;
506
507
  const settled = d.filter((x) => ["decided", "accepted"].includes(x.status)).length;
@@ -537,7 +538,7 @@ export function serve(p = port, id = venture) {
537
538
  if (u.pathname === "/routes.json") { res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }); res.end(JSON.stringify(routesOf(u.searchParams.get("venture") ?? id))); return; }
538
539
  if (u.pathname === "/site") { res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); res.end(SITE_PAGE); return; }
539
540
  if (u.pathname === "/site.json") {
540
- const dir = ventureDir(u.searchParams.get("venture") ?? id), f = dir && join(dir, "operations/marketing/site-presence.json");
541
+ const dir = ventureDir(u.searchParams.get("venture") ?? id), f = dir && join(ventureFile(dir, "operations/marketing/site-presence.json"));
541
542
  res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }); res.end(f && existsSync(f) ? readFileSync(f) : "{}"); return;
542
543
  }
543
544
  if (u.pathname === "/site/plan.json") {
@@ -14,6 +14,7 @@ import { cpSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
14
14
  import { tmpdir } from "node:os";
15
15
  import { join } from "node:path";
16
16
  import { createInterface } from "node:readline";
17
+ import { ventureFile } from "../../schema/venture-paths.mjs";
17
18
 
18
19
  const repo = new URL("../../", import.meta.url).pathname;
19
20
  const auto = process.argv.includes("--auto");
@@ -57,7 +58,7 @@ try {
57
58
  expect(!p.error && p.body.awaiting === "approval", `propose: ${JSON.stringify(p.body)}`);
58
59
  await say("naive", `Proposed ${p.body.proposed}. It waits for a person: an agent cannot decide it.`);
59
60
 
60
- const card = JSON.parse(readFileSync(join(dir, "products/shop-app.json"), "utf8"));
61
+ const card = JSON.parse(readFileSync(join(ventureFile(dir, "products/shop-app.json")), "utf8"));
61
62
  const changed = { ...card, description: "Payroll first, then bookings and stock, for small service shops" };
62
63
  const w1 = await tool("mentura_write", { venture, route: "purpose/product/shop-app", record: changed, agent_id: "builder", note: "reposition around payroll" });
63
64
  expect(w1.error && /locked until decision reposition-shop-app/.test(w1.body.error), `first write: ${JSON.stringify(w1.body)}`);
@@ -78,9 +79,9 @@ try {
78
79
  const w2 = await tool("mentura_write", { venture, route: "purpose/product/shop-app", record: changed, agent_id: "builder", decision: "reposition-shop-app", note: "reposition around payroll" });
79
80
  expect(!w2.error, `second write: ${JSON.stringify(w2.body)}`);
80
81
  await say("agent", `Resumed: wrote ${w2.body.written}.`);
81
- const log = readFileSync(join(dir, "provenance.jsonl"), "utf8").trim().split("\n").slice(-3).map((l) => JSON.parse(l));
82
+ const log = readFileSync(join(ventureFile(dir, "provenance.jsonl")), "utf8").trim().split("\n").slice(-3).map((l) => JSON.parse(l));
82
83
  await say("naive", `Provenance, last three entries:\n${log.map((e) => ` ${e.activity.padEnd(7)} ${e.entity.padEnd(48)} by ${e.agent.kind}${e.agent.id ? ` ${e.agent.id}` : ""} via ${e.agent.via}`).join("\n")}`);
83
- const dec = JSON.parse(readFileSync(join(dir, "products/decisions/shop-app.reposition.decision.json"), "utf8"));
84
+ const dec = JSON.parse(readFileSync(join(ventureFile(dir, "products/decisions/shop-app.reposition.decision.json")), "utf8"));
84
85
  await say("naive", `The decision carries its outcome to test later: "${dec.outcomes[0].projected}" (${dec.outcomes[0].result ?? "pending"}).`);
85
86
  }
86
87
  } finally {