naive-framework 0.1.0 → 0.2.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 (254) hide show
  1. package/AGENTS.md +2 -1
  2. package/docs/decisions/decisions.json +27 -0
  3. package/docs/reference/ROUTES.md +36 -37
  4. package/governance/layer.json +2 -1
  5. package/governance/venture-template/roles/venture-roles.json +62 -0
  6. package/interface/acting.mjs +83 -0
  7. package/interface/adapters/claude-code.json +16 -0
  8. package/interface/adapters/codex.json +14 -0
  9. package/interface/adapters/cursor.json +16 -0
  10. package/interface/adapters.mjs +182 -0
  11. package/interface/api/server.mjs +4 -2
  12. package/interface/boundaries.mjs +218 -0
  13. package/interface/brand.mjs +214 -0
  14. package/interface/cli/freshness.mjs +59 -0
  15. package/interface/cli/guard.mjs +6 -0
  16. package/interface/cli/mentura.mjs +78 -8
  17. package/interface/cli/naive.mjs +33 -8
  18. package/interface/context.mjs +524 -0
  19. package/interface/demo/board.mjs +1 -1
  20. package/interface/dev/setup.mjs +84 -0
  21. package/interface/gate-hooks.mjs +36 -0
  22. package/interface/harness/evals.mjs +3 -3
  23. package/interface/harness/run.mjs +4 -4
  24. package/interface/instrumentation.mjs +86 -0
  25. package/interface/kit-boundary.mjs +73 -0
  26. package/interface/layer.json +8 -2
  27. package/interface/mcp/server.mjs +47 -7
  28. package/interface/package-routes.mjs +196 -0
  29. package/interface/provenance.mjs +24 -1
  30. package/interface/reads.mjs +173 -0
  31. package/interface/release-card.mjs +42 -0
  32. package/interface/router.mjs +101 -28
  33. package/interface/skills/naive-setup/SKILL.md +71 -0
  34. package/interface/skills/naive-setup/artifact.html +83 -0
  35. package/interface/vitals.mjs +115 -0
  36. package/interface/writes.mjs +190 -0
  37. package/knowledge/freshness.json +85 -0
  38. package/knowledge/layer.json +11 -3
  39. package/knowledge/templates/glossary.md +23 -0
  40. package/operations/layer.json +10 -3
  41. package/operations/templates/marketing-brand.md +46 -0
  42. package/operations/templates/public-assets.md +39 -0
  43. package/package.json +3 -1
  44. package/packages/catalog.json +8 -0
  45. package/packages/customers/package.json +21 -1
  46. package/packages/deals/package.json +27 -0
  47. package/packages/decisions/package.json +12 -1
  48. package/packages/finance/frames/fund-without-ceding-control.frame.json +177 -0
  49. package/packages/foundation/package.json +8 -1
  50. package/packages/fund/package.json +48 -0
  51. package/packages/marketing/package.json +10 -4
  52. package/packages/money/package.json +24 -3
  53. package/packages/validation/package.json +13 -0
  54. package/platform/README.md +6 -0
  55. package/platform/adapter.json +15 -0
  56. package/platform/integrations/remote-image.mjs +79 -0
  57. package/platform/integrations/rho.mjs +7 -6
  58. package/platform/kits/README.md +8 -1
  59. package/platform/kits/agent-upgrade/kit.json +15 -0
  60. package/platform/kits/agent-upgrade/run.mjs +82 -0
  61. package/platform/kits/agent-upgrade/stand-in-agent.mjs +32 -0
  62. package/platform/kits/validation-kickoff/kit.json +29 -0
  63. package/platform/webhooks/server.mjs +70 -0
  64. package/purpose/frameworks/idea-map/ideamap.mjs +4 -2
  65. package/schema/README.md +1 -0
  66. package/schema/access.mjs +57 -0
  67. package/schema/adapter.schema.json +33 -0
  68. package/schema/assets.mjs +212 -0
  69. package/schema/brand.mjs +373 -0
  70. package/schema/build-release.mjs +61 -0
  71. package/schema/caching.mjs +505 -0
  72. package/schema/check-assets.mjs +24 -0
  73. package/schema/check-brand.mjs +24 -0
  74. package/schema/check-config.mjs +110 -0
  75. package/schema/check-nextjs-status.mjs +3 -1
  76. package/schema/check-people.mjs +70 -0
  77. package/schema/check-sources.mjs +9 -1
  78. package/schema/data-model.json +334 -8
  79. package/schema/deal.schema.json +80 -0
  80. package/schema/decisions.mjs +7 -1
  81. package/schema/examples/frames/judge.mjs +10 -0
  82. package/schema/examples/venture-type/jev.mjs +12 -0
  83. package/schema/examples/workspace/ventures/acme-labs/AGENTS.md +17 -0
  84. package/schema/examples/workspace/ventures/acme-labs/accounts/.gitkeep +0 -0
  85. package/schema/examples/workspace/ventures/acme-labs/decisions/discovery-goal.decision.json +23 -0
  86. package/schema/examples/workspace/ventures/acme-labs/decisions/statement-icp.decision.json +22 -0
  87. package/schema/examples/workspace/ventures/acme-labs/decisions/statement-problem.decision.json +22 -0
  88. package/schema/examples/workspace/ventures/acme-labs/decisions/venture.team.decision.json +37 -0
  89. package/schema/examples/workspace/ventures/acme-labs/knowledge/README.md +3 -0
  90. package/schema/examples/workspace/ventures/acme-labs/knowledge/sources.json +46 -0
  91. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000001.json +12 -0
  92. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000002.json +12 -0
  93. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000003.json +12 -0
  94. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000004.json +12 -0
  95. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000005.json +12 -0
  96. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000006.json +12 -0
  97. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000007.json +12 -0
  98. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000008.json +12 -0
  99. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000009.json +12 -0
  100. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000010.json +12 -0
  101. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000011.json +12 -0
  102. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000012.json +12 -0
  103. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000013.json +12 -0
  104. package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000014.json +12 -0
  105. package/schema/examples/workspace/ventures/acme-labs/money/_streams/transactions/stream.json +5 -0
  106. package/schema/examples/workspace/ventures/acme-labs/money/_summary.json +30 -0
  107. package/schema/examples/workspace/ventures/acme-labs/operations/departments/customer-service/README.md +5 -0
  108. package/schema/examples/workspace/ventures/acme-labs/operations/departments/customer-service/department.json +5 -0
  109. package/schema/examples/workspace/ventures/acme-labs/operations/departments/finance/README.md +5 -0
  110. package/schema/examples/workspace/ventures/acme-labs/operations/departments/finance/department.json +5 -0
  111. package/schema/examples/workspace/ventures/acme-labs/operations/departments/human-capital/README.md +5 -0
  112. package/schema/examples/workspace/ventures/acme-labs/operations/departments/human-capital/department.json +5 -0
  113. package/schema/examples/workspace/ventures/acme-labs/operations/departments/it/README.md +5 -0
  114. package/schema/examples/workspace/ventures/acme-labs/operations/departments/it/department.json +5 -0
  115. package/schema/examples/workspace/ventures/acme-labs/operations/departments/marketing-and-sales/README.md +5 -0
  116. package/schema/examples/workspace/ventures/acme-labs/operations/departments/marketing-and-sales/department.json +5 -0
  117. package/schema/examples/workspace/ventures/acme-labs/operations/departments/products/README.md +5 -0
  118. package/schema/examples/workspace/ventures/acme-labs/operations/departments/products/department.json +5 -0
  119. package/schema/examples/workspace/ventures/acme-labs/operations/departments/strategy/README.md +5 -0
  120. package/schema/examples/workspace/ventures/acme-labs/operations/departments/strategy/department.json +5 -0
  121. package/schema/examples/workspace/ventures/acme-labs/people/founders/maya-chen.json +15 -0
  122. package/schema/examples/workspace/ventures/acme-labs/people/people.json +22 -0
  123. package/schema/examples/workspace/ventures/acme-labs/pillars/brand.md +5 -0
  124. package/schema/examples/workspace/ventures/acme-labs/pillars/business.md +5 -0
  125. package/schema/examples/workspace/ventures/acme-labs/pillars/product.md +5 -0
  126. package/schema/examples/workspace/ventures/acme-labs/products/.gitkeep +0 -0
  127. package/schema/examples/workspace/ventures/acme-labs/products/decisions/product.core-product.decision.json +28 -0
  128. package/schema/examples/workspace/ventures/acme-labs/products/decisions/product.exists.decision.json +38 -0
  129. package/schema/examples/workspace/ventures/acme-labs/products/decisions/product.flagship.decision.json +27 -0
  130. package/schema/examples/workspace/ventures/acme-labs/profile.md +12 -0
  131. package/schema/examples/workspace/ventures/acme-labs/provenance.jsonl +12 -0
  132. package/schema/examples/workspace/ventures/acme-labs/raw/dump.md +6 -0
  133. package/schema/examples/workspace/ventures/acme-labs/venture.json +27 -0
  134. package/schema/examples/workspace/ventures/northstar/AGENTS.md +17 -0
  135. package/schema/examples/workspace/ventures/northstar/accounts/.gitkeep +0 -0
  136. package/schema/examples/workspace/ventures/northstar/deals/ductline.json +8 -0
  137. package/schema/examples/workspace/ventures/northstar/deals/kilnworks.json +14 -0
  138. package/schema/examples/workspace/ventures/northstar/decisions/venture.team.decision.json +31 -0
  139. package/schema/examples/workspace/ventures/northstar/evidence/README.md +3 -0
  140. package/schema/examples/workspace/ventures/northstar/fund/decisions/terms.decision.json +27 -0
  141. package/schema/examples/workspace/ventures/northstar/fund/decisions/terms.first-close.decision.json +27 -0
  142. package/schema/examples/workspace/ventures/northstar/fund/decisions/thesis.decision.json +26 -0
  143. package/schema/examples/workspace/ventures/northstar/fund/terms.json +22 -0
  144. package/schema/examples/workspace/ventures/northstar/fund/thesis.json +22 -0
  145. package/schema/examples/workspace/ventures/northstar/governance/README.md +3 -0
  146. package/schema/examples/workspace/ventures/northstar/governance/departments/external-relationships/charter.md +5 -0
  147. package/schema/examples/workspace/ventures/northstar/governance/departments/external-relationships/department.json +5 -0
  148. package/schema/examples/workspace/ventures/northstar/governance/departments/finance/charter.md +5 -0
  149. package/schema/examples/workspace/ventures/northstar/governance/departments/finance/department.json +5 -0
  150. package/schema/examples/workspace/ventures/northstar/governance/departments/human-capital/charter.md +5 -0
  151. package/schema/examples/workspace/ventures/northstar/governance/departments/human-capital/department.json +5 -0
  152. package/schema/examples/workspace/ventures/northstar/governance/departments/risk-and-compliance/charter.md +5 -0
  153. package/schema/examples/workspace/ventures/northstar/governance/departments/risk-and-compliance/department.json +5 -0
  154. package/schema/examples/workspace/ventures/northstar/governance/departments/strategy/charter.md +5 -0
  155. package/schema/examples/workspace/ventures/northstar/governance/departments/strategy/department.json +5 -0
  156. package/schema/examples/workspace/ventures/northstar/knowledge/README.md +3 -0
  157. package/schema/examples/workspace/ventures/northstar/operations/README.md +3 -0
  158. package/schema/examples/workspace/ventures/northstar/operations/departments/external-relationships/README.md +5 -0
  159. package/schema/examples/workspace/ventures/northstar/operations/departments/external-relationships/department.json +5 -0
  160. package/schema/examples/workspace/ventures/northstar/operations/departments/finance/README.md +5 -0
  161. package/schema/examples/workspace/ventures/northstar/operations/departments/finance/department.json +5 -0
  162. package/schema/examples/workspace/ventures/northstar/operations/departments/human-capital/README.md +5 -0
  163. package/schema/examples/workspace/ventures/northstar/operations/departments/human-capital/department.json +5 -0
  164. package/schema/examples/workspace/ventures/northstar/operations/departments/risk-and-compliance/README.md +5 -0
  165. package/schema/examples/workspace/ventures/northstar/operations/departments/risk-and-compliance/department.json +5 -0
  166. package/schema/examples/workspace/ventures/northstar/operations/departments/strategy/README.md +5 -0
  167. package/schema/examples/workspace/ventures/northstar/operations/departments/strategy/department.json +5 -0
  168. package/schema/examples/workspace/ventures/northstar/people/founders/ana-ruiz.json +13 -0
  169. package/schema/examples/workspace/ventures/northstar/people/founders/ben-okafor.json +7 -0
  170. package/schema/examples/workspace/ventures/northstar/people/people.json +27 -0
  171. package/schema/examples/workspace/ventures/northstar/pillars/brand.md +5 -0
  172. package/schema/examples/workspace/ventures/northstar/pillars/business.md +5 -0
  173. package/schema/examples/workspace/ventures/northstar/pillars/product.md +5 -0
  174. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/AGENTS.md +17 -0
  175. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/accounts/.gitkeep +0 -0
  176. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/decisions/venture.team.decision.json +31 -0
  177. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/knowledge/README.md +3 -0
  178. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/customer-service/README.md +5 -0
  179. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/customer-service/department.json +5 -0
  180. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/finance/README.md +5 -0
  181. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/finance/department.json +5 -0
  182. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/human-capital/README.md +5 -0
  183. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/human-capital/department.json +5 -0
  184. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/it/README.md +5 -0
  185. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/it/department.json +5 -0
  186. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/marketing-and-sales/README.md +5 -0
  187. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/marketing-and-sales/department.json +5 -0
  188. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/products/README.md +5 -0
  189. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/products/department.json +5 -0
  190. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/strategy/README.md +5 -0
  191. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/strategy/department.json +5 -0
  192. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/pillars/brand.md +5 -0
  193. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/pillars/business.md +5 -0
  194. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/pillars/product.md +5 -0
  195. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/.gitkeep +0 -0
  196. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/decisions/product.core-product.decision.json +28 -0
  197. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/decisions/product.exists.decision.json +32 -0
  198. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/decisions/product.flagship.decision.json +27 -0
  199. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/profile.md +5 -0
  200. package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/venture.json +27 -0
  201. package/schema/examples/workspace/ventures/northstar/products/.gitkeep +0 -0
  202. package/schema/examples/workspace/ventures/northstar/profile.md +5 -0
  203. package/schema/examples/workspace/ventures/northstar/provenance.jsonl +3 -0
  204. package/schema/examples/workspace/ventures/northstar/purpose/README.md +3 -0
  205. package/schema/examples/workspace/ventures/northstar/venture.json +49 -0
  206. package/schema/founder.schema.json +72 -0
  207. package/schema/frame.schema.json +240 -0
  208. package/schema/frames.mjs +228 -0
  209. package/schema/freshness-log.schema.json +40 -0
  210. package/schema/freshness-profiles.schema.json +44 -0
  211. package/schema/freshness.mjs +473 -0
  212. package/schema/fund-terms.schema.json +107 -0
  213. package/schema/fund-thesis.schema.json +87 -0
  214. package/schema/gate-drafts.mjs +130 -0
  215. package/schema/gate.mjs +105 -11
  216. package/schema/glossary.schema.json +25 -0
  217. package/schema/init-venture.mjs +38 -8
  218. package/schema/judgment.schema.json +38 -0
  219. package/schema/kit.schema.json +47 -0
  220. package/schema/kits.mjs +24 -0
  221. package/schema/knowledge-sources.schema.json +145 -15
  222. package/schema/layer.json +16 -3
  223. package/schema/marketing-brand.schema.json +116 -0
  224. package/schema/migrations/kit-to-package.mjs +94 -0
  225. package/schema/migrations/package-layout.mjs +86 -0
  226. package/schema/model-steps.mjs +717 -0
  227. package/schema/money-summary.schema.json +138 -0
  228. package/schema/naive-config.mjs +165 -0
  229. package/schema/naive-config.schema.json +268 -0
  230. package/schema/need-now.json +171 -0
  231. package/schema/need-now.mjs +3 -2
  232. package/schema/packages.mjs +10 -5
  233. package/schema/people.schema.json +86 -0
  234. package/schema/public-assets.schema.json +76 -0
  235. package/schema/release-read.mjs +111 -0
  236. package/schema/releases.mjs +497 -0
  237. package/schema/setup.mjs +164 -0
  238. package/schema/stale.mjs +111 -0
  239. package/schema/start-release.mjs +41 -0
  240. package/schema/structure.mjs +186 -0
  241. package/schema/systems-catalog.json +126 -0
  242. package/schema/systems-catalog.schema.json +78 -0
  243. package/schema/trigger.schema.json +44 -0
  244. package/schema/triggers.mjs +237 -0
  245. package/schema/upgrade-registry.schema.json +60 -0
  246. package/schema/upgrade-venture.mjs +96 -118
  247. package/schema/upgrades.json +69 -0
  248. package/schema/upgrades.mjs +385 -0
  249. package/schema/validate.py +19 -10
  250. package/schema/venture-role.schema.json +64 -0
  251. package/schema/venture-type.mjs +62 -0
  252. package/schema/venture-types.json +346 -0
  253. package/schema/venture-types.schema.json +129 -0
  254. package/schema/venture.schema.json +53 -0
@@ -0,0 +1,524 @@
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
+
50
+ const here = dirname(new URL(import.meta.url).pathname);
51
+ const repoRoot = join(here, "..");
52
+ const load = (p) => JSON.parse(readFileSync(p, "utf8"));
53
+ const sha = (x) => createHash("sha256").update(typeof x === "string" ? x : JSON.stringify(x)).digest("hex").slice(0, 16);
54
+
55
+ export const DEFAULTS = { budget: 6000, prefetch_budget: 1500, large_collection: 20, full_max: 800, template_ms: 3000 };
56
+ // Keys that hold computed values: they belong in the template, never in a layout (F-6).
57
+ export const COMPUTED = ["need_now", "need-now", "next", "ranking", "rank", "open_decisions", "decisions", "status", "todo", "due"];
58
+
59
+ /** The venture's context settings: the defaults, overridden by naive.config.json context. */
60
+ export function settings(dir) {
61
+ const f = dir && join(dir, "naive.config.json");
62
+ let c = {};
63
+ try { if (f && existsSync(f)) c = load(f).context ?? {}; } catch {}
64
+ return { ...DEFAULTS, ...c };
65
+ }
66
+
67
+ /** A route's context budget in tokens: the first matching glob in context.routes, else the default. */
68
+ export function budgetFor(set, route) {
69
+ const hit = Object.entries(set.routes ?? {}).find(([g]) => glob(g).test(route));
70
+ return hit ? hit[1] : set.budget;
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------------------------
74
+ // Header first (O-4)
75
+
76
+ /** The state of one venture file: missing, draft waiting, agent-written, confirmed, baseline or unlogged. */
77
+ export function stateOf(dir, file, draft) {
78
+ if (draft && existsSync(join(dir, draft))) return { state: "draft waiting", draft };
79
+ if (!file || !existsSync(join(dir, file))) return { state: "missing" };
80
+ let state = "unlogged", last = null;
81
+ for (const e of provEntries(dir).filter((x) => x.entity === file)) {
82
+ last = e;
83
+ if (e.activity === "baseline") state = "baseline";
84
+ else if (e.activity === "accept" || e.activity === "confirm" || e.agent?.kind === "person") state = "confirmed";
85
+ else if (e.agent?.kind === "agent") state = "agent-written";
86
+ }
87
+ return { state, ...(last ? { updated: last.at, by: `${last.agent?.kind ?? "unknown"}${last.agent?.id ? ` ${last.agent.id}` : ""}` } : {}) };
88
+ }
89
+
90
+ const TITLE = ["name", "title", "statement", "claim", "end", "question", "promise", "summary", "description", "id"];
91
+ /** A one-line summary of a record: its first naming field, cut to 140 characters. */
92
+ export function titleOf(rec) {
93
+ if (rec === null || rec === undefined) return null;
94
+ if (typeof rec === "string") return rec.split("\n").find((l) => l.trim())?.replace(/^#+\s*/, "").slice(0, 140) ?? null;
95
+ if (typeof rec !== "object" || Array.isArray(rec)) return null;
96
+ for (const k of TITLE) if (typeof rec[k] === "string" && rec[k].trim()) return rec[k].slice(0, 140);
97
+ for (const v of Object.values(rec)) if (v && typeof v === "object" && !Array.isArray(v)) { const t = titleOf(v); if (t) return t; }
98
+ const s = Object.values(rec).find((v) => typeof v === "string" && v.trim());
99
+ return s ? s.slice(0, 140) : null;
100
+ }
101
+
102
+ function ownerOf(r, dir, route) {
103
+ const g = r.kind === "record" ? gateOf(r, dir) : null;
104
+ if (g?.owner) return g.owner;
105
+ const plan = [join(dir, "people/team-plan.json"), join(dir, "operations/team-plan.json")].find((p) => existsSync(p));
106
+ if (!plan) return null;
107
+ try { return load(plan).roles.find((x) => (x.routes?.write ?? []).some((gl) => glob(gl).test(route)))?.id ?? null; } catch { return null; }
108
+ }
109
+
110
+ function fileOfRead(res) {
111
+ const a = res?.source?.address;
112
+ return a ? a.replace(/^[a-z0-9-]+:\/\//, "") : null;
113
+ }
114
+
115
+ /** The header of a read: what it is, its state, its owner and what reading it costs, without its body. */
116
+ export function headerOf({ r, route, dir, venture, workspace, res }) {
117
+ const h = { route, record: r.record ?? null, kind: r.kind, ...(r.collection ? { collection: r.collection } : {}) };
118
+ if (r.kind === "command") return { ...h, state: "computed", note: r.note };
119
+ if (r.collection === "list") {
120
+ const n = res?.result ? Object.keys(res.result).length : 0;
121
+ return { ...h, state: n ? "collection" : "missing", count: n, owner: ownerOf(r, dir, route), tokens: tokens(res?.result) };
122
+ }
123
+ const d = draftOf(route, venture, workspace);
124
+ const file = (res?.result !== null && res?.result !== undefined && (fileOfRead(res) ?? d?.file)) || d?.file;
125
+ const st = stateOf(dir, res?.result === null ? null : file, d?.draft);
126
+ if (st.state === "missing" && res?.result !== null && res?.result !== undefined) st.state = "present";
127
+ 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),
128
+ ...(res?.source ? { source: res.source.source, version: res.source.version } : {}) };
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------------------------
132
+ // Layout parts, sessions and scoped instructions (L-2, N-7, S-14)
133
+
134
+ /** Every AGENTS.md from the venture root down the route's segments, nearest last. */
135
+ export function chainOf(dir, route) {
136
+ const out = [];
137
+ const add = (rel) => { const f = join(dir, rel); if (existsSync(f)) out.push({ path: rel, text: readFileSync(f, "utf8") }); };
138
+ add("AGENTS.md");
139
+ let acc = "";
140
+ for (const p of route.split("/").filter(Boolean)) { acc = acc ? `${acc}/${p}` : p; add(`${acc}/AGENTS.md`); }
141
+ return out;
142
+ }
143
+
144
+ const PLAIN = /^([a-z][a-z0-9_-]*)\s*:\s*(.+)$/;
145
+ const SCOPED = /^\[([^\]]+)\]\s*([a-z][a-z0-9_-]*)\s*:\s*(.+)$/;
146
+ /** The merged key: value instructions for a route; a scoped line applies only where its glob matches. Nearest wins. */
147
+ export function instructionsFor(chain, route) {
148
+ const out = {};
149
+ for (const f of chain) for (const line of f.text.split("\n")) {
150
+ const s = line.match(SCOPED);
151
+ if (s) { if (s[1].split(",").map((x) => x.trim()).some((g) => glob(g).test(route))) out[s[2]] = s[3].trim(); continue; }
152
+ const m = line.match(PLAIN);
153
+ if (m) out[m[1]] = m[2].trim();
154
+ }
155
+ return out;
156
+ }
157
+
158
+ /** The stable layout of a route, as named parts. Nothing computed is in it (F-6). */
159
+ export function layoutParts(r, dir) {
160
+ const base = routerLayout(r, null); // no venture: no need-now spawn, no chain; both come from here instead
161
+ const chain = chainOf(dir, r.route.replace(/\/\[id\]$/, ""));
162
+ // One part per layer or package in the stack, so routes in the same layer share theirs.
163
+ const parts = Object.fromEntries(base.stack.map((k) => [`stack:${k.path}`, k]));
164
+ for (const f of chain) parts[`chain:${f.path}`] = f.text;
165
+ parts.instructions = instructionsFor(chain, r.route);
166
+ parts[`record:${r.route}`] = r.kind === "record"
167
+ ? { schema: r.schema, template: r.template, checks: r.checks, gate: gateOf(r, dir), package: base.package, groups: r.groups, note: r.note }
168
+ : { note: r.note, script: r.script ?? null };
169
+ return parts;
170
+ }
171
+
172
+ const sessionDir = () => process.env.NAIVE_SESSION_DIR ?? join(tmpdir(), "naive-sessions");
173
+ const sessionFile = (dir, session) => join(sessionDir(), `${sha(`${dir}\0${session}`)}.json`);
174
+ /** What a session has been sent: part key to hash, and the routes it visited. */
175
+ export function sessionOf(dir, session) {
176
+ try { return load(sessionFile(dir, session)); } catch { return { session, sent: {}, routes: [] }; }
177
+ }
178
+ function saveSession(dir, s) { mkdirSync(sessionDir(), { recursive: true }); writeFileSync(sessionFile(dir, s.session), JSON.stringify(s)); }
179
+
180
+ /**
181
+ * The layout to send: every part without a session; with one, only the parts this session has not
182
+ * seen or that changed since (L-2). A sibling route keeps the shared parts and swaps the record's (N-7).
183
+ */
184
+ export function layoutFor(r, dir, session) {
185
+ const parts = layoutParts(r, dir);
186
+ if (!session) return { parts };
187
+ const s = sessionOf(dir, session);
188
+ const send = {}, kept = [], changed = [];
189
+ for (const [k, v] of Object.entries(parts)) {
190
+ const h = sha(v);
191
+ if (s.sent[k] === h) kept.push(k);
192
+ else { send[k] = v; if (s.sent[k]) changed.push(k); s.sent[k] = h; }
193
+ }
194
+ const first = s.routes.length === 0;
195
+ s.routes = [...s.routes.filter((x) => x !== r.route), r.route].slice(-50);
196
+ saveSession(dir, s);
197
+ return { session, first, parts: send, kept, ...(changed.length ? { changed } : {}) };
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------------------------
201
+ // Template: recomputed on every action, with a time limit (P-12, F-6)
202
+
203
+ function runJson(script, args, ms) {
204
+ return new Promise((done) => {
205
+ const t0 = Date.now();
206
+ execFile(process.execPath, [join(repoRoot, script), ...args], { timeout: ms, maxBuffer: 64 * 1024 * 1024 }, (err, stdout) => {
207
+ if (err?.killed) return done({ ok: false, timed_out: true, ms: Date.now() - t0 });
208
+ 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 }); }
209
+ });
210
+ });
211
+ }
212
+
213
+ /** The template for a route: the need-now ranking and open decisions, computed now. Parts that run past template_ms come back pending. */
214
+ export async function templateFor(r, dir, set = settings(dir)) {
215
+ 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)]);
216
+ const out = { computed_at: new Date().toISOString() };
217
+ const pending = [];
218
+ if (nn.ok) {
219
+ const row = nn.json.rows.find((x) => x.type === r.record);
220
+ out.need_now = { phase: nn.json.phase, ...(row ? { this: { status: row.status, now: row.now, why: row.why } } : {}),
221
+ 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) };
222
+ } else pending.push(`need_now (${nn.timed_out ? `over ${set.template_ms}ms` : nn.error})`);
223
+ if (ds.ok) {
224
+ const open = (ds.json.decisions ?? []).filter((d) => ["now", "overdue"].includes(d.render) && d.source !== "root");
225
+ 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 })) };
226
+ } else pending.push(`decisions (${ds.timed_out ? `over ${set.template_ms}ms` : ds.error})`);
227
+ 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" });
228
+ return out;
229
+ }
230
+
231
+ // ---------------------------------------------------------------------------------------------
232
+ // Prefetch (N-3, N-4, N-12, L-12, C-16)
233
+
234
+ const MODEL = () => load(join(repoRoot, "schema/data-model.json"));
235
+ const AFTER = () => { try { return load(join(repoRoot, "packages/catalog.json")).records ?? {}; } catch { return {}; } };
236
+ 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();
237
+ const ROUTEISH = /^[a-z][a-z0-9-]*(\/[a-z0-9.\-[\]]+)+$/;
238
+ function strings(o, out = [], depth = 0) {
239
+ if (out.length > 300 || depth > 6) return out;
240
+ if (typeof o === "string") out.push(o);
241
+ else if (Array.isArray(o)) for (const x of o) strings(x, out, depth + 1);
242
+ else if (o && typeof o === "object") for (const x of Object.values(o)) strings(x, out, depth + 1);
243
+ return out;
244
+ }
245
+ /** The route a record type is read at: its list route for a collection, else its one route. */
246
+ export function routeOfType(type, id) {
247
+ const rs = routes().filter((x) => x.kind === "record" && x.record === type);
248
+ const item = rs.find((x) => x.collection === "item"), list = rs.find((x) => x.collection === "list"), one = rs.find((x) => !x.collection);
249
+ if (item && typeof id === "string" && /^[a-z0-9][a-z0-9-]*$/.test(id)) return item.route.replace("[id]", id);
250
+ return (list ?? one)?.route ?? null;
251
+ }
252
+
253
+ /** The routes an agent is likely to need after this one, most likely first, each with why. */
254
+ export function candidates(r, record, template, { hover = [] } = {}) {
255
+ const out = [];
256
+ const add = (route, why) => { if (route && route !== r.route && !out.some((x) => x.route === route)) out.push({ route, why }); };
257
+ for (const h of hover) add(h, "asked");
258
+ if (r.kind === "record" && record && typeof record === "object") {
259
+ const def = MODEL().records.find((x) => x.id === r.record);
260
+ for (const ref of def?.refs ?? []) {
261
+ const vals = valuesAt(record, ref.field).filter((v) => typeof v === "string");
262
+ for (const v of vals) add(routeOfType(ref.to, v), `ref ${ref.field}`);
263
+ }
264
+ // Links: a string in the record that is itself a route (foundation/icp), Naive's <Link>.
265
+ for (const s of strings(record).filter((x) => ROUTEISH.test(x))) { const { r: hit } = resolve(s); if (hit?.kind === "record") add(s, "link"); }
266
+ }
267
+ const after = AFTER();
268
+ for (const t of after[r.record]?.after ?? []) add(routeOfType(t), "reads first");
269
+ const next = template?.need_now?.suggest_now?.find((t) => t !== r.record);
270
+ if (next) add(routeOfType(next), "next step");
271
+ for (const [t, v] of Object.entries(after)) if ((v.after ?? []).includes(r.record)) add(routeOfType(t), "waits on this");
272
+ return out;
273
+ }
274
+
275
+ /** Prefetches the candidates within the prefetch budget: full, summary, state or count, by what each is and what is left. */
276
+ export async function prefetch({ r, record, template, venture, dir, workspace, actor_id, task = "", hover = [], set }) {
277
+ const cands = candidates(r, record, template, { hover });
278
+ const named = (route) => hover.includes(route) || (task && (task.includes(route) || task.includes(route.split("/").at(-1))));
279
+ const entries = [], dropped = [];
280
+ let spent = 0;
281
+ for (const c of cands) {
282
+ const { r: cr } = resolve(c.route);
283
+ if (!cr) continue;
284
+ const res = await safeCall({ venture, route: c.route, action: "read", workspace, actor_id, via: "prefetch" });
285
+ let options; // most to least, each an entry the agent could receive
286
+ if (!res.ok) options = [{ ...c, state: "unreadable" }];
287
+ else if (cr.collection === "list") {
288
+ const items = res.result ? Object.entries(res.result) : [];
289
+ const counted = { ...c, state: items.length ? "collection" : "missing", count: items.length };
290
+ if (items.length > set.large_collection && !named(c.route)) options = [counted];
291
+ else options = [{ ...counted, items: items.map(([id, v]) => ({ id, title: titleOf(v) })) }, counted];
292
+ } else {
293
+ const h = headerOf({ r: cr, route: c.route, dir, venture, workspace, res });
294
+ const st = { ...c, state: h.state };
295
+ const summary = { ...st, title: h.title, tokens: h.tokens };
296
+ const stable = h.state === "confirmed";
297
+ if (h.state === "missing") options = [st];
298
+ else if ((stable || named(c.route)) && h.tokens <= set.full_max) options = [{ ...summary, full: res.result }, summary, st];
299
+ else if (stable || named(c.route)) options = [summary, st];
300
+ else options = [st]; // changing (agent-written, a draft, unlogged): only its state (N-4)
301
+ }
302
+ const fit = options.find((o) => spent + tokens(o) <= set.prefetch_budget);
303
+ if (!fit) { dropped.push(c.route); continue; }
304
+ spent += tokens(fit);
305
+ entries.push({ ...fit, cost: tokens(fit) });
306
+ }
307
+ return { entries, spent, budget: set.prefetch_budget, ...(dropped.length ? { dropped } : {}) };
308
+ }
309
+
310
+ // ---------------------------------------------------------------------------------------------
311
+ // Navigate: the whole answer, streamed part by part (N-5, N-9)
312
+
313
+ // A router call that answers a throw as an error, so one broken route never stops a navigation or a report.
314
+ const safeCall = async (args) => { try { return await call(args); } catch (e) { return { ok: false, error: String(e.message ?? e).split("\n")[0] }; } };
315
+
316
+ const runCacheFile = (dir, route) => join(sessionDir(), "runs", `${sha(`${dir}\0${route}`)}.json`);
317
+
318
+ /**
319
+ * Reads a route the AI-native way. Options: session (layout once per session), task (what the agent
320
+ * is doing; a route it names is prefetched in full), prefetch (false to turn it off, or a list of
321
+ * routes to prefetch in full, like hover), header_only (stop after the header), onPart (called with
322
+ * { part, data } as each part is ready). Returns { ok, result: { header, budget, layout, template,
323
+ * result, prefetch } } in that order.
324
+ */
325
+ 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 }) {
326
+ const { r, params } = resolve(route);
327
+ if (!r) return { ok: false, error: `no route ${route}` };
328
+ const dir = ventureDir(venture, workspace);
329
+ if (!dir) return { ok: false, error: `no venture ${venture}` };
330
+ const concrete = r.collection === "item" ? r.route.replace("[id]", params.id) : r.route;
331
+ const set = settings(dir);
332
+ const send = (part, data) => { try { onPart?.({ part, data: data === undefined ? data : JSON.parse(JSON.stringify(data)) }); } catch {} };
333
+ const out = {};
334
+
335
+ if (r.kind === "command") {
336
+ // A computed route answers at once with its state and the last result, then streams the new one (N-9).
337
+ let last = null;
338
+ try { last = load(runCacheFile(dir, concrete)); } catch {}
339
+ out.header = { ...headerOf({ r, route: concrete, dir, venture, workspace }), state: "computing", ...(last ? { last_at: last.at } : {}) };
340
+ send("header", out.header);
341
+ if (last) { out.stale_result = last.result; send("stale_result", { as_of: last.at, result: last.result }); }
342
+ if (header_only) return { ok: true, result: out };
343
+ out.layout = layoutFor(r, dir, session); send("layout", out.layout);
344
+ const res = await safeCall({ venture, route: concrete, action: "run", workspace, actor, actor_id, via });
345
+ if (!res.ok) return res;
346
+ out.header.state = res.needs_input ? "needs input" : "computed";
347
+ out.result = res.result ?? null;
348
+ if (res.needs_input) out.needs_input = res.needs_input;
349
+ else { mkdirSync(dirname(runCacheFile(dir, concrete)), { recursive: true }); writeFileSync(runCacheFile(dir, concrete), JSON.stringify({ at: new Date().toISOString(), result: res.result })); }
350
+ send("result", out.result);
351
+ out.budget = budgetOf(out, budgetFor(set, r.route));
352
+ send("budget", out.budget);
353
+ return { ok: true, result: order(out) };
354
+ }
355
+
356
+ // The template starts first, in other processes, so it runs while the record is read.
357
+ const templating = header_only ? null : _template ? Promise.resolve(_template) : templateFor(r, dir, set);
358
+ const res = await safeCall({ venture, route: concrete, action: "read", workspace, actor, actor_id, via });
359
+ if (!res.ok) return res;
360
+ out.header = headerOf({ r, route: concrete, dir, venture, workspace, res });
361
+ send("header", out.header);
362
+ if (header_only) { out.budget = budgetOf(out, budgetFor(set, r.route)); return { ok: true, result: order(out) }; }
363
+ out.layout = layoutFor(r, dir, session);
364
+ send("layout", out.layout);
365
+ out.template = await templating;
366
+ send("template", out.template);
367
+ out.result = res.result;
368
+ for (const k of ["not_found", "pending_draft", "pending_pull_requests", "source", "sources", "deprecated"]) if (res[k]) out[k] = res[k];
369
+ send("result", out.result);
370
+ if (pf !== false) {
371
+ out.prefetch = await prefetch({ r, record: res.result, template: out.template, venture, dir, workspace, actor_id, task, hover: Array.isArray(pf) ? pf : [], set });
372
+ // Over the route's budget: prefetch is the first thing given back (C-16).
373
+ const limit = budgetFor(set, r.route);
374
+ while (out.prefetch.entries.length && budgetOf(out, limit).over) out.prefetch.dropped = [...(out.prefetch.dropped ?? []), out.prefetch.entries.pop().route];
375
+ out.prefetch.spent = out.prefetch.entries.reduce((n, e) => n + e.cost, 0);
376
+ send("prefetch", out.prefetch);
377
+ }
378
+ out.budget = budgetOf(out, budgetFor(set, r.route));
379
+ send("budget", out.budget);
380
+ return { ok: true, result: order(out) };
381
+ }
382
+
383
+ const PARTS = ["header", "layout", "template", "stale_result", "result", "prefetch"];
384
+ function budgetOf(out, limit) {
385
+ const parts = Object.fromEntries(PARTS.filter((k) => out[k] !== undefined).map((k) => [k, tokens(out[k])]));
386
+ const used = Object.values(parts).reduce((a, b) => a + b, 0);
387
+ return { limit, used, over: used > limit, parts };
388
+ }
389
+ // Header first, then the budget, then the rest in the order an agent needs it.
390
+ 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]]));
391
+
392
+ /** Runs a command route in a child process and reports progress while it runs (N-5). Gates apply: it goes through the router. */
393
+ export function runStreaming({ venture, route, input, workspace, actor_id }, onProgress = () => {}, every = 250) {
394
+ return new Promise((done) => {
395
+ const args = [join(repoRoot, "interface/cli/mentura.mjs"), route, "run", "--venture", venture, "--json",
396
+ ...(input?.value !== undefined ? ["--value", String(input.value)] : []), ...(input?.as_is ? ["--as-is"] : []), ...(workspace ? ["--workspace", workspace] : [])];
397
+ const env = { ...process.env, ...(actor_id ? { MENTURA_ACTOR_ID: actor_id } : {}) };
398
+ const p = spawn(process.execPath, args, { env });
399
+ let stdout = "", stderr = "", n = 0;
400
+ const t0 = Date.now();
401
+ onProgress({ progress: 0, message: `started ${route}` });
402
+ const timer = setInterval(() => onProgress({ progress: ++n, message: `running ${route}: ${((Date.now() - t0) / 1000).toFixed(1)}s` }), every);
403
+ p.stdout.on("data", (d) => { stdout += d; });
404
+ p.stderr.on("data", (d) => {
405
+ stderr += d;
406
+ 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] }); }
407
+ });
408
+ p.on("close", (code) => {
409
+ clearInterval(timer);
410
+ onProgress({ progress: ++n, message: `finished ${route} in ${((Date.now() - t0) / 1000).toFixed(1)}s` });
411
+ if (code !== 0) return done({ ok: false, error: (stderr || stdout).trim() });
412
+ try { const j = JSON.parse(stdout); done(j.ok !== undefined ? j : { ok: true, result: j }); } catch { done({ ok: true, result: stdout.trim() }); }
413
+ });
414
+ });
415
+ }
416
+
417
+ /** Runs calls one after another (a batch of reads, checks or runs) and reports each as it finishes (N-5). */
418
+ export async function batch({ venture, workspace, calls = [], actor_id, via = "router" }, onProgress = () => {}) {
419
+ const results = [];
420
+ for (const [i, c] of calls.entries()) {
421
+ const r = await safeCall({ venture, workspace, actor_id, via, ...c });
422
+ results.push({ route: c.route, action: c.action, ok: r.ok, ...(r.ok ? { result: r.result } : { error: r.error }) });
423
+ onProgress({ progress: i + 1, total: calls.length, message: `${c.action} ${c.route}: ${r.ok ? "ok" : "failed"}` });
424
+ }
425
+ return { ok: results.every((x) => x.ok), result: results };
426
+ }
427
+
428
+ // ---------------------------------------------------------------------------------------------
429
+ // The context budget report (N-13, S-14, F-6)
430
+
431
+ const ADVICE = {
432
+ 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)",
433
+ template: "the template is computed every time: keep it to rankings and counts",
434
+ result: "the record is large: read its header first (head) and split it if agents rarely need all of it",
435
+ prefetch: "lower context.prefetch_budget for this route, or turn prefetch off where the refs are rarely followed",
436
+ header: "the header should be small: check the record's title field",
437
+ };
438
+
439
+ /** Every route of a venture against its context budget, with its largest part, plus the layout rules it breaks. */
440
+ export async function budget(dir) {
441
+ // The report's own reads are measurement, not an agent's work: they stay out of the events log and vitals.
442
+ const was = process.env.NAIVE_INSTRUMENT;
443
+ process.env.NAIVE_INSTRUMENT = "0";
444
+ try { return await measureBudget(dir); } finally { if (was === undefined) delete process.env.NAIVE_INSTRUMENT; else process.env.NAIVE_INSTRUMENT = was; }
445
+ }
446
+
447
+ async function measureBudget(dir) {
448
+ const v = load(join(dir, "venture.json"));
449
+ const workspace = dirname(dir);
450
+ const set = settings(dir);
451
+ const rows = [];
452
+ let tmpl = null;
453
+ for (const r of routes().filter((x) => x.kind === "record" && x.collection !== "item")) {
454
+ tmpl ??= await templateFor(r, dir, set);
455
+ const t = { ...tmpl, need_now: tmpl.need_now ? { ...tmpl.need_now, this: undefined } : undefined };
456
+ const n = await navigate({ venture: v.id, route: r.route, workspace, _template: t, via: "budget" });
457
+ if (!n.ok) { rows.push({ route: r.route, error: n.error }); continue; }
458
+ const b = n.result.budget;
459
+ const largest = Object.entries(b.parts).sort((a, c) => c[1] - a[1])[0]?.[0];
460
+ rows.push({ route: r.route, limit: b.limit, used: b.used, over: b.over, largest, ...(b.over ? { advice: ADVICE[largest] } : {}) });
461
+ }
462
+ // Layout rules, over every AGENTS.md in the venture.
463
+ const rules = [];
464
+ const agentsFiles = (d, rel = "") => readdirSync(join(d, rel), { withFileTypes: true }).flatMap((e) => e.name.startsWith(".") || e.name.startsWith("_") ? []
465
+ : 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"] : []);
466
+ for (const f of agentsFiles(dir)) {
467
+ const folder = dirname(f) === "." ? "" : dirname(f);
468
+ const text = readFileSync(join(dir, f), "utf8");
469
+ for (const line of text.split("\n")) {
470
+ const s = line.match(SCOPED), m = s ? [null, s[2], s[3]] : line.match(PLAIN);
471
+ if (!m) continue;
472
+ // F-6: a computed value in a layout blocks or goes stale; it belongs in the template.
473
+ 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}` });
474
+ // S-14: a scoped line whose routes all sit under one folder belongs in that folder's AGENTS.md.
475
+ if (s) {
476
+ const fixed = s[1].split(",").map((g) => g.trim().split("/").filter((x) => !x.includes("*")).join("/"));
477
+ 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("/"); });
478
+ 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` });
479
+ }
480
+ }
481
+ const t = tokens(text);
482
+ 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}` });
483
+ }
484
+ const over = rows.filter((x) => x.over);
485
+ 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 };
486
+ }
487
+
488
+ // ---------------------------------------------------------------------------------------------
489
+ // Command line: the router's command route system/context, and the CLI hook for navigate, head and batch.
490
+
491
+ /** mentura <route> navigate|head --venture <id> [--session s] [--task text] [--prefetch a,b | --no-prefetch] [--stream]; mentura batch --venture <id> --input calls.json [--stream]. */
492
+ export async function cli(argv, { workspace } = {}) {
493
+ const opt = (n) => { const i = argv.indexOf(`--${n}`); return i >= 0 ? argv[i + 1] : undefined; };
494
+ const flag = (n) => argv.includes(`--${n}`);
495
+ 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))));
496
+ const venture = opt("venture");
497
+ const stream = flag("stream");
498
+ const line = (x) => process.stdout.write(JSON.stringify(x) + "\n");
499
+ if (route === "batch") {
500
+ const calls = JSON.parse(readFileSync(opt("input") === "-" ? 0 : opt("input"), "utf8"));
501
+ const r = await batch({ venture, workspace, calls: calls.calls ?? calls, via: "cli" }, stream ? (p) => line({ progress: p }) : undefined);
502
+ if (stream) line({ part: "result", data: r.result }); else console.log(JSON.stringify(r.result, null, 2));
503
+ return r.ok ? 0 : 1;
504
+ }
505
+ // Through the router, so the navigation is one instrumented call with its reads under it.
506
+ 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"),
507
+ prefetch: flag("no-prefetch") ? false : opt("prefetch") ? opt("prefetch").split(",").filter(Boolean) : true, onPart: stream ? line : undefined } });
508
+ if (!r.ok) { console.error(r.error); return 1; }
509
+ if (!stream) console.log(JSON.stringify(r.result, null, 2));
510
+ return 0;
511
+ }
512
+
513
+ if (import.meta.url === `file://${process.argv[1]}`) {
514
+ const [dir, what] = process.argv.slice(2);
515
+ if (!dir || !existsSync(join(dir, "venture.json")) || what !== "budget") { console.error("usage: node interface/context.mjs <venture dir> budget [--check] [--json]"); process.exit(2); }
516
+ const r = await budget(dir);
517
+ if (process.argv.includes("--json")) console.log(JSON.stringify(r, null, 2));
518
+ else {
519
+ console.log(`${r.venture}: ${r.routes.length} routes, budget ${r.budget} tokens (prefetch ${r.prefetch_budget})`);
520
+ 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}`}`);
521
+ for (const x of r.rules) console.log(` ${x.rule.padEnd(5)} ${x.file}: ${x.fix}`);
522
+ }
523
+ 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); }
524
+ }
@@ -38,7 +38,7 @@ function state(id) {
38
38
  const rows = nn.status === 0 ? JSON.parse(nn.stdout).rows : [];
39
39
  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
40
  // 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;
41
+ const money = existsSync(join(dir, "money/_summary.json")) ? JSON.parse(readFileSync(join(dir, "money/_summary.json"), "utf8")) : null;
42
42
  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
43
  }
44
44
 
@@ -0,0 +1,84 @@
1
+ // The setup page (EE-083): what `naive dev` serves at /setup, the way next dev serves a first page. It
2
+ // walks a founder through establishing the venture's structure, in order: the venture and its people,
3
+ // where each function keeps its truth, the strategic foundation and the first decisions (the decision
4
+ // docket), and a goal for the discovery phase. Every answer is written as the code would write it:
5
+ // a decision a person approves (schema/decisions.mjs), or knowledge/sources.json through the router, with
6
+ // provenance. When the docket's first decisions are settled, the page says the structure is established
7
+ // and what comes next.
8
+ //
9
+ // handle(req, res, dir) -> true when it answered a /setup request
10
+
11
+ import { decide, point, state } from "../../schema/setup.mjs";
12
+
13
+ const json = (res, code, body) => { res.writeHead(code, { "content-type": "application/json" }); res.end(JSON.stringify(body)); };
14
+ const body = (req) => new Promise((ok) => { let b = ""; req.on("data", (c) => (b += c)); req.on("end", () => { try { ok(JSON.parse(b || "{}")); } catch { ok({}); } }); });
15
+ const GOAL = "discovery-goal";
16
+
17
+ export async function handle(req, res, dir) {
18
+ const u = new URL(req.url, "http://x");
19
+ if (!u.pathname.startsWith("/setup")) return false;
20
+ if (u.pathname === "/setup" || u.pathname === "/setup/") { res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); res.end(PAGE); return true; }
21
+ if (u.pathname === "/setup/state") { json(res, 200, await state(dir)); return true; }
22
+ if (req.method === "POST" && u.pathname === "/setup/decide") { const r = await decide(dir, await body(req)); json(res, r.ok ? 200 : 400, r); return true; }
23
+ if (req.method === "POST" && u.pathname === "/setup/point") { const r = await point(dir, await body(req)); json(res, r.ok ? 200 : 400, r); return true; }
24
+ json(res, 404, { error: "no such setup route" });
25
+ return true;
26
+ }
27
+
28
+ const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
29
+ <title>Set up your venture</title>
30
+ <style>
31
+ :root { --bg:#f6f7f4; --panel:#fff; --fg:#1d2320; --muted:#5d675f; --line:#d9ded8; --accent:#2f6b4f; --accent-soft:#e3efe8; --warn:#8a5a00; --warn-soft:#f6ecd8;
32
+ --body: "Source Sans 3", system-ui, -apple-system, "Segoe UI", sans-serif; --mono: ui-monospace, "SF Mono", Menlo, monospace; color-scheme: light; }
33
+ @media (prefers-color-scheme: dark) { :root { --bg:#121614; --panel:#1a201d; --fg:#e4e9e5; --muted:#9aa59d; --line:#2b332e; --accent:#86c4a3; --accent-soft:#1d2b24; --warn:#e3b25c; --warn-soft:#2c2414; color-scheme: dark; } }
34
+ * { box-sizing: border-box } body { margin:0; background:var(--bg); color:var(--fg); font:16px/1.55 var(--body); }
35
+ .wrap { max-width: 860px; margin: 0 auto; padding: 32px max(16px, env(safe-area-inset-left)) 80px; display: grid; gap: 28px; }
36
+ header { display:grid; gap:6px } h1 { margin:0; font-size:1.9rem; letter-spacing:-.01em } h2 { margin:0 0 4px; font-size:1.2rem }
37
+ .muted { color: var(--muted) } .mono { font-family: var(--mono); font-size: .88em }
38
+ .progress { display:flex; gap:6px; flex-wrap:wrap } .step { font:600 .78rem/1 var(--mono); padding:7px 10px; border-radius:999px; border:1px solid var(--line); color:var(--muted) }
39
+ .step.ok { background: var(--accent-soft); color: var(--accent); border-color: transparent }
40
+ section { background: var(--panel); border:1px solid var(--line); border-radius:10px; padding:20px; display:grid; gap:14px }
41
+ table { width:100%; border-collapse: collapse; font-size:.95rem } td, th { text-align:left; padding:8px 6px; border-bottom:1px solid var(--line); vertical-align: top } th { font: 600 .74rem/1.2 var(--mono); text-transform: uppercase; letter-spacing:.05em; color: var(--muted) }
42
+ .pill { display:inline-block; font:600 .72rem/1 var(--mono); padding:4px 7px; border-radius:999px; background: var(--accent-soft); color: var(--accent); margin: 0 4px 4px 0 } .pill.pointed { background: var(--line); color: var(--fg) } .pill.none { background: var(--warn-soft); color: var(--warn) }
43
+ .card { border:1px solid var(--line); border-radius:8px; padding:14px; display:grid; gap:10px } .card.done { opacity:.75 }
44
+ .card .q { font-weight:600 } textarea, input, select { font: inherit; color: inherit; background: var(--bg); border:1px solid var(--line); border-radius:6px; padding:8px; width:100% }
45
+ .row { display:flex; gap:8px; flex-wrap:wrap; align-items:center } .row > * { flex: 1 1 140px }
46
+ button { font: 600 .92rem var(--body); background: var(--accent); color: var(--bg); border: 0; border-radius: 6px; padding: 9px 14px; cursor: pointer; flex: 0 0 auto }
47
+ button.ghost { background: transparent; color: var(--accent); border:1px solid var(--accent) } button:focus-visible, textarea:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px }
48
+ .err { color: var(--warn); font-size: .9rem } .celebrate { text-align:center; padding: 32px 20px; display:grid; gap:10px; background: var(--accent-soft); border-color: transparent }
49
+ .celebrate .big { font-size: 2.2rem; font-weight: 700; color: var(--accent) } ol { margin:0; padding-left: 1.2em; display:grid; gap:6px; text-align:left }
50
+ @media (prefers-reduced-motion: no-preference) { .celebrate { animation: pop .5s ease-out } @keyframes pop { from { transform: scale(.97); opacity:.4 } to { transform: none; opacity:1 } } }
51
+ </style></head><body><div class="wrap" id="app"><p class="muted">Loading your venture…</p></div>
52
+ <script>
53
+ const $ = (s) => document.querySelector(s);
54
+ const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;" })[c]);
55
+ const post = (u, b) => fetch(u, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(b) }).then((r) => r.json());
56
+ let S = null;
57
+ async function load() { S = await fetch("/setup/state").then((r) => r.json()); render(); }
58
+ function render() {
59
+ const v = S.venture, st = S.steps;
60
+ const label = (k, t) => '<span class="step ' + (st[k] ? "ok" : "") + '">' + (st[k] ? "✓ " : "") + t + "</span>";
61
+ const people = S.people ? '<table><tr><th>Person</th><th>Role</th><th>Hats</th></tr>' + S.people.people.map((p) => "<tr><td>" + esc(p.name) + "</td><td class=mono>" + esc(p.role) + "</td><td>" + (p.hats.map((h) => '<span class="pill pointed">' + esc(h) + "</span>").join("") || '<span class="muted">none</span>') + "</td></tr>").join("") + "</table>" : '<p class="err">No people/people.json yet: create the venture with create-venture.</p>';
62
+ const fns = [...new Set([...Object.keys(S.catalog.functions).filter((f) => f === "legal" || (S.people?.people ?? []).some((p) => p.hats.includes(f))), ...Object.keys(S.functions)])];
63
+ const truth = '<table><tr><th>Function</th><th>Source of truth</th></tr>' + fns.map((f) => { const x = S.functions[f]; const sys = x?.systems?.length ? x.systems.map((s) => '<span class="pill ' + (s.status === "connected" ? "" : "pointed") + '">' + esc(S.catalog.systems[s.system]?.label ?? s.system) + (s.address ? " · " + esc(s.address) : "") + " · " + s.status + (s.fixture ? " (fixture)" : "") + "</span>").join("") : '<span class="pill none">none yet</span>' + (x?.note ? ' <span class="muted">' + esc(x.note) + "</span>" : ""); return "<tr><td>" + esc(f === "legal" ? "legal documents" : f) + "</td><td>" + sys + "</td></tr>"; }).join("") + "</table>"
64
+ + '<div class="row"><select id="fn">' + fns.map((f) => "<option>" + f + "</option>").join("") + '</select><select id="sys"></select><input id="addr" placeholder="address, such as acme/web"><button id="point">Point</button></div><div class="err" id="perr"></div>';
65
+ const cards = S.docket.map((d) => '<div class="card ' + (d.status === "decided" ? "done" : "") + '" data-id="' + esc(d.id) + '"><div class="q">' + esc(d.question) + "</div>" + (d.status === "decided" ? '<div>✓ <span class="muted">' + esc(d.answer) + "</span></div>" : (d.options.length ? '<div class="row">' + d.options.map((o) => '<button class="ghost opt">' + esc(o) + "</button>").join("") + "</div>" : "") + '<textarea rows="2" placeholder="In your own words"></textarea><div class="row"><button class="ans">Save</button><span class="err"></span></div>') + "</div>").join("");
66
+ const goal = S.goal?.status === "decided" ? '<p>✓ ' + esc(S.goal.answer) + "</p>" : '<textarea id="goal" rows="2" placeholder="For example: ten clinic owners confirm the scheduling problem in interviews by December"></textarea><div class="row"><button id="setgoal">Set the goal</button><span class="err" id="gerr"></span></div>';
67
+ const done = S.done ? '<section class="celebrate"><div class="big">Your venture is structured.</div><p>' + esc(v.name) + " has its people and their hats, a source of truth for its functions, a settled foundation, and a goal for discovery. Every decision from here routes through the docket.</p><ol><li>Run <span class=mono>npm run dev</span> and keep the board open: decisions waiting on you show there first.</li><li>Orient each next decision around the discovery goal; a sprint or campaign is a set of decisions toward it.</li><li>To operate the venture with your team and agents (tasks, documents, money and people in one workspace), connect it to Mentura.</li></ol>" + (S.later ? '<p class="muted">' + S.later + " more decisions come later, as the venture needs them.</p>" : "") + "</section>" : "";
68
+ $("#app").innerHTML = '<header><span class="muted mono">' + esc(v.type?.label ?? "venture") + " · " + esc(v.phase) + '</span><h1>Set up ' + esc(v.name) + '</h1><p class="muted">Establish the structure of your venture. Each answer is saved as you go.</p><div class="progress">' + label("people", "People and hats") + label("truth", "Sources of truth") + label("docket", "Foundation and first decisions " + S.counts.settled + "/" + S.counts.of) + label("goal", "Discovery goal") + "</div></header>" + done
69
+ + "<section><h2>1. People and hats</h2>" + people + "</section>"
70
+ + '<section><h2>2. Sources of truth</h2><p class="muted">Point Naive at where each function keeps its truth. Naive keeps pointers, never copies; Rho is read through its API.</p>' + truth + "</section>"
71
+ + '<section><h2>3. Foundation and first decisions</h2><p class="muted">The decision docket: what the venture must settle first, in order.</p>' + cards + "</section>"
72
+ + '<section><h2>4. A goal for discovery</h2><p class="muted">What must be true to leave discovery? Every decision after this one works toward it.</p>' + goal + "</section>";
73
+ const sysFor = () => { $("#sys").innerHTML = (S.catalog.functions[$("#fn").value] ?? []).map((s) => '<option value="' + s + '">' + esc(S.catalog.systems[s].label) + "</option>").join(""); $("#addr").placeholder = S.catalog.systems[$("#sys").value]?.address ?? "address"; };
74
+ $("#fn").onchange = sysFor; sysFor(); $("#sys").onchange = () => ($("#addr").placeholder = S.catalog.systems[$("#sys").value]?.address ?? "address");
75
+ $("#point").onclick = async () => { const r = await post("/setup/point", { fn: $("#fn").value, system: $("#sys").value, address: $("#addr").value.trim() || undefined }); if (!r.ok) $("#perr").textContent = r.error; else load(); };
76
+ document.querySelectorAll(".card").forEach((c) => {
77
+ const id = c.dataset.id, ta = c.querySelector("textarea");
78
+ c.querySelectorAll(".opt").forEach((b) => (b.onclick = () => { ta.value = b.textContent; }));
79
+ const save = c.querySelector(".ans"); if (save) save.onclick = async () => { const r = await post("/setup/decide", { id, answer: ta.value }); if (!r.ok) c.querySelector(".err").textContent = r.error; else load(); };
80
+ });
81
+ const g = $("#setgoal"); if (g) g.onclick = async () => { const r = await post("/setup/decide", { id: "${GOAL}", answer: $("#goal").value }); if (!r.ok) $("#gerr").textContent = r.error; else load(); };
82
+ }
83
+ load();
84
+ </script></body></html>`;