rcf-lite 0.0.1 → 0.8.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 (349) hide show
  1. package/CHANGELOG.md +344 -0
  2. package/LICENSE +202 -0
  3. package/README.md +92 -4
  4. package/bin/rcf-verify.js +122 -0
  5. package/bin/rcf.js +174 -0
  6. package/bin/view-supervisor-child.mjs +14 -0
  7. package/fixtures/canary-manifest.json +103 -0
  8. package/guidance/README.md +25 -0
  9. package/guidance/build-cycle-playbook.md +492 -0
  10. package/guidance/build-cycle.md +40 -0
  11. package/guidance/document-model.md +45 -0
  12. package/guidance/elicitation-playbook.md +442 -0
  13. package/guidance/harness-template.md +246 -0
  14. package/guidance/managed/README.md +63 -0
  15. package/guidance/managed/agent-instructions-block.hash +1 -0
  16. package/guidance/managed/agent-instructions-block.md +220 -0
  17. package/guidance/managed/legacy-fragment-hashes.json +22 -0
  18. package/guidance/manifest.json +21 -0
  19. package/guidance/overview.md +48 -0
  20. package/guidance/persona-programme.md +44 -0
  21. package/package.json +71 -8
  22. package/rcf/adrs/adr-001.json +25 -0
  23. package/rcf/adrs/adr-002.json +25 -0
  24. package/rcf/adrs/adr-003.json +25 -0
  25. package/rcf/adrs/adr-004.json +20 -0
  26. package/rcf/adrs/adr-005.json +20 -0
  27. package/rcf/adrs/adr-006.json +25 -0
  28. package/rcf/adrs/adr-007.json +25 -0
  29. package/rcf/adrs/adr-008.json +25 -0
  30. package/rcf/adrs/adr-009.json +25 -0
  31. package/rcf/build-sequence.json +11 -0
  32. package/rcf/code-nodes/cn-001.json +14 -0
  33. package/rcf/code-nodes/cn-002.json +14 -0
  34. package/rcf/code-nodes/cn-003.json +15 -0
  35. package/rcf/code-nodes/cn-004.json +17 -0
  36. package/rcf/code-nodes/cn-005.json +19 -0
  37. package/rcf/code-nodes/cn-006.json +17 -0
  38. package/rcf/code-nodes/cn-007.json +16 -0
  39. package/rcf/code-nodes/cn-008.json +19 -0
  40. package/rcf/code-nodes/cn-009.json +16 -0
  41. package/rcf/code-nodes/cn-010.json +18 -0
  42. package/rcf/code-nodes/cn-011.json +18 -0
  43. package/rcf/code-nodes/cn-012.json +18 -0
  44. package/rcf/code-nodes/cn-013.json +19 -0
  45. package/rcf/code-nodes/cn-014.json +16 -0
  46. package/rcf/code-nodes/cn-015.json +16 -0
  47. package/rcf/code-nodes/cn-016.json +16 -0
  48. package/rcf/code-nodes/cn-017.json +16 -0
  49. package/rcf/code-nodes/cn-018.json +15 -0
  50. package/rcf/code-nodes/cn-019.json +16 -0
  51. package/rcf/code-nodes/cn-020.json +16 -0
  52. package/rcf/code-nodes/cn-021.json +16 -0
  53. package/rcf/code-nodes/cn-022.json +16 -0
  54. package/rcf/code-nodes/cn-023.json +16 -0
  55. package/rcf/code-nodes/cn-024.json +16 -0
  56. package/rcf/code-nodes/cn-025.json +16 -0
  57. package/rcf/code-nodes/cn-026.json +16 -0
  58. package/rcf/code-nodes/cn-027.json +16 -0
  59. package/rcf/code-nodes/cn-028.json +16 -0
  60. package/rcf/code-nodes/cn-029.json +19 -0
  61. package/rcf/code-nodes/cn-030.json +14 -0
  62. package/rcf/code-nodes/cn-031.json +14 -0
  63. package/rcf/code-nodes/cn-032.json +14 -0
  64. package/rcf/code-nodes/cn-033.json +14 -0
  65. package/rcf/code-nodes/cn-034.json +14 -0
  66. package/rcf/code-nodes/cn-035.json +14 -0
  67. package/rcf/code-nodes/cn-036.json +14 -0
  68. package/rcf/code-nodes/cn-037.json +14 -0
  69. package/rcf/code-nodes/cn-038.json +14 -0
  70. package/rcf/code-nodes/cn-039.json +14 -0
  71. package/rcf/code-nodes/cn-040.json +14 -0
  72. package/rcf/code-nodes/cn-041.json +14 -0
  73. package/rcf/code-nodes/cn-042.json +14 -0
  74. package/rcf/code-nodes/cn-043.json +14 -0
  75. package/rcf/code-nodes/cn-044.json +14 -0
  76. package/rcf/code-nodes/cn-045.json +14 -0
  77. package/rcf/code-nodes/cn-046.json +14 -0
  78. package/rcf/code-nodes/cn-047.json +14 -0
  79. package/rcf/code-nodes/cn-048.json +14 -0
  80. package/rcf/code-nodes/cn-049.json +14 -0
  81. package/rcf/code-nodes/cn-050.json +14 -0
  82. package/rcf/code-nodes/cn-051.json +14 -0
  83. package/rcf/code-nodes/cn-052.json +14 -0
  84. package/rcf/code-nodes/cn-053.json +14 -0
  85. package/rcf/code-nodes/cn-054.json +14 -0
  86. package/rcf/code-nodes/cn-055.json +14 -0
  87. package/rcf/code-nodes/cn-056.json +14 -0
  88. package/rcf/code-nodes/cn-057.json +14 -0
  89. package/rcf/fbs/fbs-001.json +49 -0
  90. package/rcf/fbs/fbs-002.json +42 -0
  91. package/rcf/fbs/fbs-003.json +37 -0
  92. package/rcf/fbs/fbs-004.json +39 -0
  93. package/rcf/fbs/fbs-005.json +38 -0
  94. package/rcf/fbs/fbs-006.json +48 -0
  95. package/rcf/fbs/fbs-007.json +39 -0
  96. package/rcf/fbs/fbs-008.json +40 -0
  97. package/rcf/fbs/fbs-009.json +36 -0
  98. package/rcf/fbs/fbs-010.json +41 -0
  99. package/rcf/fbs/fbs-011.json +36 -0
  100. package/rcf/fbs/fbs-012.json +46 -0
  101. package/rcf/fbs/fbs-013.json +42 -0
  102. package/rcf/fbs/fbs-014.json +49 -0
  103. package/rcf/fbs/fbs-015.json +32 -0
  104. package/rcf/manifest.json +17 -0
  105. package/rcf/prd.json +47 -0
  106. package/rcf/requirements/req-001.json +19 -0
  107. package/rcf/requirements/req-002.json +19 -0
  108. package/rcf/requirements/req-003.json +19 -0
  109. package/rcf/requirements/req-004.json +19 -0
  110. package/rcf/requirements/req-005.json +19 -0
  111. package/rcf/requirements/req-006.json +19 -0
  112. package/rcf/requirements/req-007.json +19 -0
  113. package/rcf/requirements/req-008.json +19 -0
  114. package/rcf/requirements/req-009.json +19 -0
  115. package/rcf/tacs/tac-001.json +45 -0
  116. package/rcf/tacs/tac-002.json +109 -0
  117. package/rcf/tacs/tac-003.json +40 -0
  118. package/rcf/tacs/tac-004.json +51 -0
  119. package/rcf/tacs/tac-005.json +52 -0
  120. package/rcf/tacs/tac-006.json +104 -0
  121. package/rcf/tacs/tac-007.json +38 -0
  122. package/rcf/tacs/tac-008.json +51 -0
  123. package/rcf/tad.json +51 -0
  124. package/rcf/test-suites/PENDING.md +23 -0
  125. package/rcf/test-suites/ts-001.json +38 -0
  126. package/rcf/test-suites/ts-002.json +38 -0
  127. package/rcf/test-suites/ts-003.json +43 -0
  128. package/rcf/test-suites/ts-004.json +44 -0
  129. package/rcf/test-suites/ts-005.json +30 -0
  130. package/rcf/test-suites/ts-006.json +36 -0
  131. package/rcf/test-suites/ts-007.json +43 -0
  132. package/rcf/test-suites/ts-008.json +37 -0
  133. package/rcf/test-suites/ts-009.json +38 -0
  134. package/rcf/test-suites/ts-010.json +38 -0
  135. package/rcf/test-suites/ts-011.json +44 -0
  136. package/rcf/test-suites/ts-012.json +36 -0
  137. package/rcf/test-suites/ts-013.json +38 -0
  138. package/rcf/test-suites/ts-014.json +38 -0
  139. package/rcf/test-suites/ts-015.json +38 -0
  140. package/rcf/test-suites/ts-016.json +37 -0
  141. package/rcf/test-suites/ts-017.json +38 -0
  142. package/rcf/test-suites/ts-018.json +38 -0
  143. package/rcf/test-suites/ts-019.json +37 -0
  144. package/rcf/test-suites/ts-020.json +46 -0
  145. package/rcf/test-suites/ts-021.json +46 -0
  146. package/rcf/test-suites/ts-022.json +46 -0
  147. package/rcf/test-suites/ts-023.json +46 -0
  148. package/rcf/test-suites/ts-024.json +46 -0
  149. package/rcf/test-suites/ts-025.json +52 -0
  150. package/rcf/user-stories/us-101.json +40 -0
  151. package/rcf/user-stories/us-102.json +40 -0
  152. package/rcf/user-stories/us-201.json +40 -0
  153. package/rcf/user-stories/us-202.json +40 -0
  154. package/rcf/user-stories/us-203.json +32 -0
  155. package/rcf/user-stories/us-301.json +40 -0
  156. package/rcf/user-stories/us-302.json +40 -0
  157. package/rcf/user-stories/us-303.json +40 -0
  158. package/rcf/user-stories/us-304.json +40 -0
  159. package/rcf/user-stories/us-401.json +40 -0
  160. package/rcf/user-stories/us-402.json +40 -0
  161. package/rcf/user-stories/us-403.json +40 -0
  162. package/rcf/user-stories/us-501.json +40 -0
  163. package/rcf/user-stories/us-502.json +40 -0
  164. package/rcf/user-stories/us-503.json +40 -0
  165. package/rcf/user-stories/us-601.json +40 -0
  166. package/rcf/user-stories/us-602.json +40 -0
  167. package/rcf/user-stories/us-701.json +40 -0
  168. package/rcf/user-stories/us-702.json +40 -0
  169. package/rcf/user-stories/us-801.json +49 -0
  170. package/rcf/user-stories/us-802.json +49 -0
  171. package/rcf/user-stories/us-803.json +49 -0
  172. package/rcf/user-stories/us-804.json +49 -0
  173. package/rcf/user-stories/us-805.json +49 -0
  174. package/rcf/user-stories/us-901.json +40 -0
  175. package/src/.gitkeep +0 -0
  176. package/src/admissibility/enforce.js +142 -0
  177. package/src/admissibility/index.js +8 -0
  178. package/src/admissibility/markers.js +104 -0
  179. package/src/admissibility/scope-lint.js +163 -0
  180. package/src/browser-verify/auth-smoke.js +109 -0
  181. package/src/browser-verify/index.js +29 -0
  182. package/src/browser-verify/invariants.js +336 -0
  183. package/src/browser-verify/manifest-writer.js +189 -0
  184. package/src/browser-verify/runner.js +171 -0
  185. package/src/build/bundle.js +198 -0
  186. package/src/build/formatters/json.js +18 -0
  187. package/src/build/formatters/markdown.js +406 -0
  188. package/src/build/index.js +14 -0
  189. package/src/build/mark.js +177 -0
  190. package/src/build/queue.js +285 -0
  191. package/src/cli/browser-verify.js +231 -0
  192. package/src/cli/build.js +584 -0
  193. package/src/cli/coverage.js +219 -0
  194. package/src/cli/create.js +358 -0
  195. package/src/cli/delete.js +127 -0
  196. package/src/cli/design.js +353 -0
  197. package/src/cli/doctor.js +548 -0
  198. package/src/cli/fbs.js +171 -0
  199. package/src/cli/finalise.js +377 -0
  200. package/src/cli/guidance.js +206 -0
  201. package/src/cli/help.js +156 -0
  202. package/src/cli/impact.js +119 -0
  203. package/src/cli/init.js +282 -0
  204. package/src/cli/intake.js +153 -0
  205. package/src/cli/link.js +128 -0
  206. package/src/cli/mcp.js +160 -0
  207. package/src/cli/preflight.js +220 -0
  208. package/src/cli/read.js +168 -0
  209. package/src/cli/req-baseline.js +269 -0
  210. package/src/cli/req-classify.js +135 -0
  211. package/src/cli/review.js +295 -0
  212. package/src/cli/test-suite.js +221 -0
  213. package/src/cli/trace.js +172 -0
  214. package/src/cli/ui-baseline.js +292 -0
  215. package/src/cli/ui-classify.js +108 -0
  216. package/src/cli/update.js +197 -0
  217. package/src/cli/validate.js +168 -0
  218. package/src/cli/view.js +452 -0
  219. package/src/core/baseline-catalog/data/auth.json +42 -0
  220. package/src/core/baseline-catalog/data/http-api.json +42 -0
  221. package/src/core/baseline-catalog/data/notifications.json +33 -0
  222. package/src/core/baseline-catalog/data/persistence.json +33 -0
  223. package/src/core/baseline-catalog/data/web-ui.json +60 -0
  224. package/src/core/baseline-catalog/index.js +121 -0
  225. package/src/core/errors/index.js +167 -0
  226. package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
  227. package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
  228. package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
  229. package/src/core/isolation/index.js +60 -0
  230. package/src/core/mcp/framing.js +103 -0
  231. package/src/core/mcp/index.js +8 -0
  232. package/src/core/mcp/server.js +228 -0
  233. package/src/core/patterns/register-canary.js +209 -0
  234. package/src/core/patterns/req-shapes.js +158 -0
  235. package/src/core/patterns/services.js +358 -0
  236. package/src/core/patterns/ui-shapes.js +166 -0
  237. package/src/core/store/cn-resolve.js +134 -0
  238. package/src/core/store/derive-deps.js +93 -0
  239. package/src/core/store/ids.js +78 -0
  240. package/src/core/store/index.js +20 -0
  241. package/src/core/store/init.js +255 -0
  242. package/src/core/store/loader.js +214 -0
  243. package/src/core/store/tp-resolve.js +176 -0
  244. package/src/core/store/validator.js +191 -0
  245. package/src/core/store/walker.js +944 -0
  246. package/src/core/store/writer.js +1879 -0
  247. package/src/design/index.js +11 -0
  248. package/src/design/writer.js +271 -0
  249. package/src/finalise/detect.js +151 -0
  250. package/src/finalise/index.js +31 -0
  251. package/src/finalise/ingest.js +160 -0
  252. package/src/finalise/install.js +119 -0
  253. package/src/finalise/ship-without-verified.js +131 -0
  254. package/src/finalise/spawn.js +84 -0
  255. package/src/intake/fidelity.js +105 -0
  256. package/src/intake/index.js +6 -0
  257. package/src/intake/manifest-writer.js +100 -0
  258. package/src/intake/orchestrator.js +138 -0
  259. package/src/intake/validate.js +80 -0
  260. package/src/mcp/map-errors.js +131 -0
  261. package/src/mcp/prompts.js +49 -0
  262. package/src/mcp/resources.js +244 -0
  263. package/src/mcp/tools.js +1212 -0
  264. package/src/preflight/design-shapes.js +185 -0
  265. package/src/preflight/index.js +90 -0
  266. package/src/preflight/manifest-writer.js +264 -0
  267. package/src/preflight/scanner.js +206 -0
  268. package/src/preflight/secrets.js +134 -0
  269. package/src/preflight/session.js +246 -0
  270. package/src/query/attestation.js +285 -0
  271. package/src/query/coverage.js +308 -0
  272. package/src/query/formatters/json.js +21 -0
  273. package/src/query/formatters/mermaid.js +209 -0
  274. package/src/query/formatters/table.js +203 -0
  275. package/src/query/impact.js +173 -0
  276. package/src/query/index.js +13 -0
  277. package/src/query/refuse-on-admissibility.js +73 -0
  278. package/src/query/trace.js +345 -0
  279. package/src/register-canary/fixture-loader.js +87 -0
  280. package/src/register-canary/index.js +10 -0
  281. package/src/register-canary/record-writer.js +132 -0
  282. package/src/register-canary/runner.js +156 -0
  283. package/src/req-baseline/gate.js +86 -0
  284. package/src/req-baseline/index.js +27 -0
  285. package/src/req-baseline/open-candidates.js +143 -0
  286. package/src/req-baseline/opt-out.js +195 -0
  287. package/src/req-baseline/sweep.js +230 -0
  288. package/src/req-detection/classifier.js +181 -0
  289. package/src/req-detection/index.js +9 -0
  290. package/src/req-detection/persist.js +55 -0
  291. package/src/review/index.js +325 -0
  292. package/src/review/mutation.js +117 -0
  293. package/src/review/ui-baseline-drift.js +138 -0
  294. package/src/ruleset/index.js +140 -0
  295. package/src/ruleset/ruleset.json +146 -0
  296. package/src/server/index.js +178 -0
  297. package/src/server/routes.js +110 -0
  298. package/src/server/sse.js +118 -0
  299. package/src/setup/agent-setup.js +362 -0
  300. package/src/setup/identity-seed.js +104 -0
  301. package/src/setup/knowledge-seed.js +123 -0
  302. package/src/setup/managed-block.js +193 -0
  303. package/src/setup/managed-gitignore.js +166 -0
  304. package/src/setup/managed-markers.js +49 -0
  305. package/src/ui-baseline/defaults.js +119 -0
  306. package/src/ui-baseline/index.js +25 -0
  307. package/src/ui-baseline/manifest-writer.js +282 -0
  308. package/src/ui-baseline/session.js +178 -0
  309. package/src/ui-detection/classifier.js +192 -0
  310. package/src/verify/chain/index.js +221 -0
  311. package/src/verify/cli/cleanup.js +61 -0
  312. package/src/verify/cli/help.js +56 -0
  313. package/src/verify/cli/mcp.js +98 -0
  314. package/src/verify/cli/provision.js +71 -0
  315. package/src/verify/cli/report.js +71 -0
  316. package/src/verify/cli/run.js +155 -0
  317. package/src/verify/engine/brief.js +87 -0
  318. package/src/verify/engine/index.js +177 -0
  319. package/src/verify/engine/launcher.js +307 -0
  320. package/src/verify/mcp/tools.js +107 -0
  321. package/src/verify/profile/index.js +146 -0
  322. package/src/verify/provision/index.js +256 -0
  323. package/src/verify/report/index.js +139 -0
  324. package/src/verify/report/renderer.js +118 -0
  325. package/src/verify/verdict/index.js +313 -0
  326. package/src/view/doc-renderers/adr.js +44 -0
  327. package/src/view/doc-renderers/build-sequence.js +40 -0
  328. package/src/view/doc-renderers/fbs.js +128 -0
  329. package/src/view/doc-renderers/helpers.js +159 -0
  330. package/src/view/doc-renderers/index.js +12 -0
  331. package/src/view/doc-renderers/prd.js +45 -0
  332. package/src/view/doc-renderers/req.js +43 -0
  333. package/src/view/doc-renderers/tac.js +38 -0
  334. package/src/view/doc-renderers/tad.js +74 -0
  335. package/src/view/doc-renderers/test-suite.js +45 -0
  336. package/src/view/doc-renderers/user-story.js +63 -0
  337. package/src/view/html-page.js +462 -0
  338. package/src/view/index.js +63 -0
  339. package/src/view/live-client.js +338 -0
  340. package/src/view/mermaid-diagram.js +178 -0
  341. package/src/view/style.css +735 -0
  342. package/src/view/tree-model.js +152 -0
  343. package/src/view/vendored/mermaid.min.js +2607 -0
  344. package/src/view-supervisor/index.js +26 -0
  345. package/src/view-supervisor/logs.js +32 -0
  346. package/src/view-supervisor/manifest-writer.js +178 -0
  347. package/src/view-supervisor/persist-until.js +85 -0
  348. package/src/view-supervisor/supervisor.js +276 -0
  349. package/src/watch/index.js +152 -0
@@ -0,0 +1,118 @@
1
+ // SSE (Server-Sent Events) connection manager. Tracks the open long-lived
2
+ // `/events` responses, broadcasts payload events to all of them, keeps a
3
+ // heartbeat loop, and drains gracefully on server close (D16).
4
+ //
5
+ // D5: private surface between the server and its own browser client.
6
+ // Neither payload shape nor route path is a stable contract; downstream
7
+ // consumers (e.g. `rcf query`) get their own wire when the time comes.
8
+
9
+ /**
10
+ * @typedef {Object} SseHubOptions
11
+ * @property {number} [heartbeatMs=30000] - heartbeat interval in ms
12
+ * @property {(msg: string) => void} [log] - optional stderr sink
13
+ */
14
+
15
+ /**
16
+ * @param {SseHubOptions} [opts]
17
+ * @returns {{
18
+ * handle: (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse, current: { version: number, contentHtml: string } | null) => void,
19
+ * broadcast: (event: string, data: object) => void,
20
+ * heartbeatOnce: () => void,
21
+ * drain: (event?: string) => Promise<void>,
22
+ * size: () => number,
23
+ * close: () => void,
24
+ * }}
25
+ */
26
+ export function createSseHub(opts = {}) {
27
+ const heartbeatMs = typeof opts.heartbeatMs === 'number' && opts.heartbeatMs > 0 ? opts.heartbeatMs : 30000;
28
+ const log = typeof opts.log === 'function' ? opts.log : () => {};
29
+ /** @type {Set<import('node:http').ServerResponse>} */
30
+ const clients = new Set();
31
+ let heartbeatTimer = null;
32
+ let closed = false;
33
+
34
+ function frame(event, data) {
35
+ const payload = typeof data === 'string' ? data : JSON.stringify(data ?? {});
36
+ return `event: ${event}\ndata: ${payload}\n\n`;
37
+ }
38
+
39
+ function writeTo(res, event, data) {
40
+ try {
41
+ res.write(frame(event, data));
42
+ } catch (err) {
43
+ log(`[sse] write failed: ${/** @type {Error} */ (err).message}`);
44
+ try { res.end(); } catch { /* swallow */ }
45
+ clients.delete(res);
46
+ }
47
+ }
48
+
49
+ function startHeartbeat() {
50
+ if (heartbeatTimer || closed) return;
51
+ heartbeatTimer = setInterval(() => {
52
+ if (clients.size === 0) return;
53
+ const ts = new Date().toISOString();
54
+ for (const res of clients) writeTo(res, 'heartbeat', { ts });
55
+ }, heartbeatMs);
56
+ if (typeof heartbeatTimer.unref === 'function') heartbeatTimer.unref();
57
+ }
58
+
59
+ function stopHeartbeat() {
60
+ if (!heartbeatTimer) return;
61
+ clearInterval(heartbeatTimer);
62
+ heartbeatTimer = null;
63
+ }
64
+
65
+ return {
66
+ handle(req, res, current) {
67
+ if (closed) {
68
+ res.writeHead(503, { 'content-type': 'text/plain; charset=utf-8' });
69
+ res.end('server shutting down\n');
70
+ return;
71
+ }
72
+ res.writeHead(200, {
73
+ 'content-type': 'text/event-stream; charset=utf-8',
74
+ 'cache-control': 'no-cache, no-transform',
75
+ 'connection': 'keep-alive',
76
+ 'x-accel-buffering': 'no',
77
+ });
78
+ // Prime the stream so proxies don't buffer the initial event.
79
+ try { res.write(': ok\n\n'); } catch { /* swallow */ }
80
+ clients.add(res);
81
+ req.on('close', () => {
82
+ clients.delete(res);
83
+ });
84
+ // First event on connect carries the current state so the client
85
+ // is in sync without a separate resume protocol (D12).
86
+ if (current) writeTo(res, 'tree-update', current);
87
+ startHeartbeat();
88
+ },
89
+ broadcast(event, data) {
90
+ if (closed) return;
91
+ for (const res of clients) writeTo(res, event, data);
92
+ },
93
+ heartbeatOnce() {
94
+ if (clients.size === 0) return;
95
+ const ts = new Date().toISOString();
96
+ for (const res of clients) writeTo(res, 'heartbeat', { ts });
97
+ },
98
+ async drain(event = 'shutdown') {
99
+ if (closed) return;
100
+ closed = true;
101
+ stopHeartbeat();
102
+ for (const res of clients) {
103
+ writeTo(res, event, {});
104
+ try { res.end(); } catch { /* swallow */ }
105
+ }
106
+ clients.clear();
107
+ },
108
+ size() { return clients.size; },
109
+ close() {
110
+ closed = true;
111
+ stopHeartbeat();
112
+ for (const res of clients) {
113
+ try { res.end(); } catch { /* swallow */ }
114
+ }
115
+ clients.clear();
116
+ },
117
+ };
118
+ }
@@ -0,0 +1,362 @@
1
+ // Pre-session agent bootstrap (Theme 1, E2E matrix 2026-07-06-003;
2
+ // updated for 0.6.0 init-hygiene). `rcf init` is the single golden path
3
+ // that leaves a project fully wired BEFORE the agent session starts:
4
+ // rcf/ tree + project-root .mcp.json (rcf server entry) + the managed
5
+ // canonical block inside marker comments in the agent-instructions
6
+ // file. Anything that detects incomplete setup funnels back here: run
7
+ // `npx rcf init`, then restart the agent session.
8
+ //
9
+ // 0.6.0 changes:
10
+ // - Marker constants moved to `managed-markers.js` and imported here
11
+ // (D-7); the strand-1 legacy migration recognises both generations.
12
+ // - Canonical block content is sourced from
13
+ // `guidance/managed/agent-instructions-block.md`, not from the
14
+ // `guidance/harness-template.md` fenced fragment. The two remain
15
+ // byte-identical after `scripts/gen-managed-artefacts.mjs` runs
16
+ // (AC-1.14); harness-template.md stays as the manual-paste-in doc.
17
+ // - `hasAgentMarker` recognises BOTH the new managed markers and the
18
+ // pre-0.6.0 legacy markers so the MCP setup funnel does not spam
19
+ // legacy-inited repos with "Setup incomplete" notices (§2.5 / AC-1.13).
20
+
21
+ import { readFile, writeFile } from 'node:fs/promises';
22
+ import { dirname, join, resolve } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ import { rcfError } from '#core/errors';
26
+
27
+ import {
28
+ MARKER_BEGIN,
29
+ MARKER_END,
30
+ LEGACY_MARKER_BEGIN,
31
+ markerRegex,
32
+ } from './managed-markers.js';
33
+
34
+ const here = dirname(fileURLToPath(import.meta.url));
35
+ const PACKAGE_ROOT = resolve(here, '..', '..');
36
+ const MANAGED_BLOCK_PATH = join(PACKAGE_ROOT, 'guidance', 'managed', 'agent-instructions-block.md');
37
+ const MANAGED_HASH_PATH = join(PACKAGE_ROOT, 'guidance', 'managed', 'agent-instructions-block.hash');
38
+ const LEGACY_FRAGMENT_HASHES_PATH = join(PACKAGE_ROOT, 'guidance', 'managed', 'legacy-fragment-hashes.json');
39
+
40
+ // Re-export the marker constants at the module boundary so callers that
41
+ // already import from `agent-setup.js` (setup funnel, existing tests)
42
+ // keep working without touching every import site.
43
+ export { MARKER_BEGIN, MARKER_END, LEGACY_MARKER_BEGIN } from './managed-markers.js';
44
+
45
+ /** Absolute path of this package's rcf bin - what .mcp.json points at. */
46
+ export function rcfBinPath() {
47
+ return join(PACKAGE_ROOT, 'bin', 'rcf.js');
48
+ }
49
+
50
+ /** Absolute path of the canonical managed block; test-visible for the byte-match AC. */
51
+ export function managedBlockPath() {
52
+ return MANAGED_BLOCK_PATH;
53
+ }
54
+
55
+ /** Absolute path of the canonical managed-block hash file. */
56
+ export function managedBlockHashPath() {
57
+ return MANAGED_HASH_PATH;
58
+ }
59
+
60
+ /**
61
+ * Absolute path of the pre-0.6.0 canonical-fragment hash whitelist that
62
+ * §7.3's fail-safe hand-edit detector consults. Test-visible.
63
+ */
64
+ export function legacyFragmentHashesPath() {
65
+ return LEGACY_FRAGMENT_HASHES_PATH;
66
+ }
67
+
68
+ async function fileExists(path) {
69
+ try {
70
+ await readFile(path, 'utf8');
71
+ return true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ async function readIfExists(path) {
78
+ try {
79
+ return await readFile(path, 'utf8');
80
+ } catch (err) {
81
+ if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') return null;
82
+ throw err;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Read the canonical managed block text (§2.4 verbatim). Returns the
88
+ * text with a trailing newline; callers wrap it in the markers. Fails
89
+ * with an RcfError if the shipped asset is missing (bad tarball, files
90
+ * whitelist regression) - doctor surfaces this rather than pretending
91
+ * clean (§12 risk).
92
+ *
93
+ * @returns {Promise<string | import('#core/errors').RcfError>}
94
+ */
95
+ export async function loadManagedBlock() {
96
+ const text = await readIfExists(MANAGED_BLOCK_PATH);
97
+ if (text === null) {
98
+ return rcfError({ kind: 'missingFile', message: `managed block not found: ${MANAGED_BLOCK_PATH}`, filePath: MANAGED_BLOCK_PATH });
99
+ }
100
+ return text.endsWith('\n') ? text : `${text}\n`;
101
+ }
102
+
103
+ /**
104
+ * Read the SHA-256 hash the package shipped for the managed block. One
105
+ * line, no trailing whitespace; on read failure, returns an RcfError
106
+ * with kind `hashFileMissing` so doctor emits a distinct error rather
107
+ * than reporting spurious clean.
108
+ *
109
+ * @returns {Promise<string | import('#core/errors').RcfError>}
110
+ */
111
+ export async function loadManagedBlockHash() {
112
+ const text = await readIfExists(MANAGED_HASH_PATH);
113
+ if (text === null) {
114
+ return rcfError({ kind: 'hashFileMissing', message: `managed-block hash not found: ${MANAGED_HASH_PATH}`, filePath: MANAGED_HASH_PATH });
115
+ }
116
+ return text.trim();
117
+ }
118
+
119
+ /**
120
+ * Read the SHA-256 whitelist of pre-0.6.0 canonical fragments (§7.3).
121
+ * Doctor's `detectLegacyHandEdits` hashes the extracted legacy inner
122
+ * content (trimmed) and treats any hash NOT in the returned set as
123
+ * hand-edited. Fail-safe: a missing / malformed whitelist surfaces as
124
+ * an RcfError rather than silently allowing overwrite.
125
+ *
126
+ * @returns {Promise<Set<string> | import('#core/errors').RcfError>}
127
+ */
128
+ export async function loadLegacyFragmentHashes() {
129
+ const text = await readIfExists(LEGACY_FRAGMENT_HASHES_PATH);
130
+ if (text === null) {
131
+ return rcfError({
132
+ kind: 'legacyFragmentHashesMissing',
133
+ message: `legacy fragment hash whitelist not found: ${LEGACY_FRAGMENT_HASHES_PATH}`,
134
+ filePath: LEGACY_FRAGMENT_HASHES_PATH,
135
+ });
136
+ }
137
+ let parsed;
138
+ try {
139
+ parsed = JSON.parse(text);
140
+ } catch (err) {
141
+ return rcfError({
142
+ kind: 'legacyFragmentHashesInvalid',
143
+ message: `legacy fragment hash whitelist is not valid JSON: ${err.message}`,
144
+ filePath: LEGACY_FRAGMENT_HASHES_PATH,
145
+ });
146
+ }
147
+ if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.hashes)) {
148
+ return rcfError({
149
+ kind: 'legacyFragmentHashesInvalid',
150
+ message: `legacy fragment hash whitelist missing hashes[] array`,
151
+ filePath: LEGACY_FRAGMENT_HASHES_PATH,
152
+ });
153
+ }
154
+ const set = new Set();
155
+ for (const entry of parsed.hashes) {
156
+ if (entry && typeof entry.hash === 'string' && /^[0-9a-f]{64}$/i.test(entry.hash)) {
157
+ set.add(entry.hash.toLowerCase());
158
+ }
159
+ }
160
+ return set;
161
+ }
162
+
163
+ /**
164
+ * Extract the paste-in fragment from guidance/harness-template.md (the
165
+ * first ```markdown fence). Preserved for the paste-in doc use case,
166
+ * but the fragment is regenerated at package-build time from the
167
+ * managed block canonical source (AC-1.14) so the two are byte-identical.
168
+ *
169
+ * @param {object} [opts]
170
+ * @param {string} [opts.templatePath] - test override
171
+ * @returns {Promise<string | import('#core/errors').RcfError>}
172
+ */
173
+ export async function loadHarnessFragment({ templatePath } = {}) {
174
+ const path = templatePath ?? join(PACKAGE_ROOT, 'guidance', 'harness-template.md');
175
+ const text = await readIfExists(path);
176
+ if (text === null) {
177
+ return rcfError({ kind: 'missingFile', message: `harness template not found: ${path}`, filePath: path });
178
+ }
179
+ const m = /```markdown\n([\s\S]*?)```/.exec(text);
180
+ if (!m) {
181
+ return rcfError({ kind: 'parseFailure', message: `no \`\`\`markdown fragment fence in ${path}`, filePath: path });
182
+ }
183
+ return m[1].trim();
184
+ }
185
+
186
+ /**
187
+ * Write or merge the project-root .mcp.json with the rcf server entry
188
+ * (the exact registration shape docs/install.md documents). MERGE
189
+ * discipline: other servers and unknown top-level keys are preserved
190
+ * verbatim; an existing `rcf` entry is left alone.
191
+ *
192
+ * @param {object} args
193
+ * @param {string} args.projectRoot
194
+ * @param {string} [args.binPath] - test override
195
+ * @returns {Promise<{ file: string, action: 'created'|'merged'|'kept' } | import('#core/errors').RcfError>}
196
+ */
197
+ export async function writeMcpConfig({ projectRoot, binPath = rcfBinPath() }) {
198
+ const file = join(projectRoot, '.mcp.json');
199
+ const raw = await readIfExists(file);
200
+ let config = {};
201
+ let action = 'created';
202
+ if (raw !== null) {
203
+ try {
204
+ config = JSON.parse(raw);
205
+ } catch (err) {
206
+ return rcfError({
207
+ kind: 'parseFailure',
208
+ message: `.mcp.json exists but is not valid JSON (${err.message}); refusing to modify it. Fix it by hand, or add the rcf entry manually - see docs/install.md, section 7.`,
209
+ filePath: '.mcp.json',
210
+ });
211
+ }
212
+ if (config === null || typeof config !== 'object' || Array.isArray(config)) {
213
+ return rcfError({
214
+ kind: 'parseFailure',
215
+ message: '.mcp.json exists but is not a JSON object; refusing to modify it.',
216
+ filePath: '.mcp.json',
217
+ });
218
+ }
219
+ action = 'merged';
220
+ }
221
+ const servers = (config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers))
222
+ ? config.mcpServers
223
+ : {};
224
+ if (servers.rcf) {
225
+ return { file: '.mcp.json', action: 'kept' };
226
+ }
227
+ const next = {
228
+ ...config,
229
+ mcpServers: {
230
+ ...servers,
231
+ rcf: { command: 'node', args: [binPath, 'mcp'] },
232
+ },
233
+ };
234
+ await writeFile(file, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
235
+ return { file: '.mcp.json', action };
236
+ }
237
+
238
+ /**
239
+ * Write the composed managed block into one agent-instructions file.
240
+ * Idempotent: an existing new-marker block is replaced in place, never
241
+ * duplicated; a file without one gets the block appended; a missing
242
+ * file is created. Init does not touch legacy markers - that migration
243
+ * is doctor's `--fix` path, so an existing pre-0.6.0 file whose only
244
+ * marker pair is the legacy one gets a NEW managed block appended
245
+ * alongside the legacy one, and the operator resolves via `rcf doctor
246
+ * --fix` afterwards. This preserves init's "leave operator content
247
+ * alone" discipline even for a file that also happens to carry the old
248
+ * managed convention.
249
+ *
250
+ * @param {string} target - absolute path
251
+ * @param {string} file - display name (CLAUDE.md / AGENTS.md)
252
+ * @param {string} block - the marked fragment block
253
+ * @returns {Promise<{ file: string, action: 'created'|'appended'|'replaced' }>}
254
+ */
255
+ async function writeFragmentToFile(target, file, block) {
256
+ const existing = await readIfExists(target);
257
+ if (existing === null) {
258
+ await writeFile(target, `${block}\n`, 'utf8');
259
+ return { file, action: 'created' };
260
+ }
261
+ const markerRe = markerRegex();
262
+ if (markerRe.test(existing)) {
263
+ await writeFile(target, existing.replace(markerRe, block), 'utf8');
264
+ return { file, action: 'replaced' };
265
+ }
266
+ const sep = existing.endsWith('\n') ? '\n' : '\n\n';
267
+ await writeFile(target, `${existing}${sep}${block}\n`, 'utf8');
268
+ return { file, action: 'appended' };
269
+ }
270
+
271
+ /**
272
+ * Write the canonical managed block into the project's agent-instructions
273
+ * file(s) inside the rcf managed markers. Routing:
274
+ * - An existing instructions file is refreshed in place (CLAUDE.md
275
+ * preferred as the write target, else an existing AGENTS.md). We
276
+ * never invent the other convention's file when one already exists.
277
+ * - A fresh repo (neither present) gets BOTH CLAUDE.md and AGENTS.md,
278
+ * so the wiring is vendor-neutral by default (operator ruling
279
+ * 2026-07-16). The same marked block goes into each.
280
+ * Idempotent throughout: re-running replaces the marked block in place,
281
+ * never duplicating it, in whichever file(s) are touched.
282
+ *
283
+ * @param {object} args
284
+ * @param {string} args.projectRoot
285
+ * @param {string} args.fragment - canonical block inner content (no markers).
286
+ * @returns {Promise<{ writes: Array<{ file: string, action: 'created'|'appended'|'replaced' }> }>}
287
+ */
288
+ export async function writeAgentInstructions({ projectRoot, fragment }) {
289
+ const claudePath = join(projectRoot, 'CLAUDE.md');
290
+ const agentsPath = join(projectRoot, 'AGENTS.md');
291
+ const claudeExists = await fileExists(claudePath);
292
+ const agentsExists = await fileExists(agentsPath);
293
+ const trimmed = fragment.trim();
294
+ const block = `${MARKER_BEGIN}\n${trimmed}\n${MARKER_END}`;
295
+ const writes = [];
296
+
297
+ if (claudeExists) {
298
+ // Existing CLAUDE.md wins as the target; refresh it in place.
299
+ writes.push(await writeFragmentToFile(claudePath, 'CLAUDE.md', block));
300
+ } else if (agentsExists) {
301
+ // No CLAUDE.md, but an AGENTS.md is present: keep that routing.
302
+ writes.push(await writeFragmentToFile(agentsPath, 'AGENTS.md', block));
303
+ } else {
304
+ // Fresh repo: write both, vendor-neutral by default.
305
+ writes.push(await writeFragmentToFile(claudePath, 'CLAUDE.md', block));
306
+ writes.push(await writeFragmentToFile(agentsPath, 'AGENTS.md', block));
307
+ }
308
+ return { writes };
309
+ }
310
+
311
+ /**
312
+ * Does the project's agent-instructions file carry ANY generation of
313
+ * the rcf marker block? The MCP setup funnel uses this: marker absent
314
+ * means the session started without the init bootstrap.
315
+ *
316
+ * Recognises BOTH the 0.6.0+ managed marker (MARKER_BEGIN) and the
317
+ * pre-0.6.0 legacy marker (LEGACY_MARKER_BEGIN). Either presence
318
+ * satisfies the funnel so a legacy-inited repo is not spammed with
319
+ * setup notices for the process lifetime while it waits to migrate;
320
+ * the correct signal for "you should migrate" is doctor's
321
+ * `legacy-markers` drift item, which the operator sees on their next
322
+ * diagnostic run. §2.5, AC-1.13.
323
+ *
324
+ * @param {string} projectRoot
325
+ * @returns {Promise<boolean>}
326
+ */
327
+ export async function hasAgentMarker(projectRoot) {
328
+ for (const name of ['CLAUDE.md', 'AGENTS.md']) {
329
+ const text = await readIfExists(join(projectRoot, name));
330
+ if (text === null) continue;
331
+ if (text.includes(MARKER_BEGIN)) return true;
332
+ if (text.includes(LEGACY_MARKER_BEGIN)) return true;
333
+ }
334
+ return false;
335
+ }
336
+
337
+ /**
338
+ * The single funnel instruction every incomplete-setup touchpoint
339
+ * repeats. One golden path; no degraded mid-session fallback.
340
+ */
341
+ export const SETUP_FUNNEL_INSTRUCTION = 'Setup incomplete. Run `npx rcf init` to finish wiring '
342
+ + '(tree + .mcp.json + agent instructions), then tell the user to exit and restart their '
343
+ + 'agent session before continuing.';
344
+
345
+ /**
346
+ * Manual instructions printed by `rcf init --no-agent-setup`.
347
+ *
348
+ * @param {string} [binPath]
349
+ * @returns {string}
350
+ */
351
+ export function manualSetupInstructions(binPath = rcfBinPath()) {
352
+ return [
353
+ 'Agent setup skipped (--no-agent-setup). To wire the harness manually:',
354
+ ' 1. Register the MCP server in your project-root .mcp.json:',
355
+ ' { "mcpServers": { "rcf": { "command": "node",',
356
+ ` "args": ["${binPath}", "mcp"] } } }`,
357
+ ' 2. Paste the fragment from `rcf guidance harness-template` (the',
358
+ ' first ```markdown fence) into your project\'s CLAUDE.md or',
359
+ ' AGENTS.md.',
360
+ ' 3. Restart your agent session so it picks both up.',
361
+ ].join('\n');
362
+ }
@@ -0,0 +1,104 @@
1
+ // Strand 3: seed writer for `rcf/.identity/profile.md` and the entry
2
+ // constant the managed-gitignore aggregator consumes to keep the
3
+ // directory ignored by default (0.6.0 spec §5, §4.1).
4
+ //
5
+ // Two exports:
6
+ // - `identityEntry`: the aggregator's registered entry; imported by
7
+ // managed-gitignore.js and inserted into `managedGitignoreEntries()`.
8
+ // - `writeIdentityTemplate`: init calls this after the tree scaffold.
9
+ // Idempotent: an existing profile.md is left byte-identical (§5.4).
10
+ //
11
+ // The template is a one-shot seed; doctor does not maintain its content
12
+ // (§5.5 — the profile is entirely operator-owned once written). Doctor
13
+ // cares only about presence and effective-ignore, both warn-only.
14
+
15
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
16
+ import { dirname, join } from 'node:path';
17
+
18
+ /**
19
+ * Entry constant consumed by `managed-gitignore.js`'s aggregator. Adding
20
+ * this to `managedGitignoreEntries()` keeps `rcf/.identity/` out of the
21
+ * shared repo by default (§4.1, §4.3, §5.1). The 0.7.0 credentials
22
+ * side-file will follow the same pattern: import an entry constant from
23
+ * its owning module, insert into the array, done.
24
+ */
25
+ export const identityEntry = Object.freeze({
26
+ path: 'rcf/.identity/',
27
+ owner: 'rcf init: per-clone operator profile',
28
+ since: '0.6.0',
29
+ });
30
+
31
+ /**
32
+ * Canonical template written verbatim on fresh init (spec §5.4). British
33
+ * English, no em-dashes, banned-tells baseline honoured; the same lint
34
+ * fires against this constant per AC-4.7.
35
+ */
36
+ export const IDENTITY_TEMPLATE = `# Operator profile
37
+
38
+ This file describes you (the operator) to any agent working on this
39
+ repo. It lives in \`rcf/.identity/\` and is gitignored by default, so
40
+ it is per-clone: another developer's clone of the same repo has their
41
+ own profile, not yours.
42
+
43
+ Fill in what is useful. Leave the rest. Freeform prose is fine; a
44
+ bulleted list is fine. Nothing here is enforced by tooling.
45
+
46
+ ## Name
47
+
48
+ _(who you are; how you want the agent to address you)_
49
+
50
+ ## Role
51
+
52
+ _(what you do; what perspective you bring to this project)_
53
+
54
+ ## Working preferences
55
+
56
+ _(how you like to work: casual or formal register, verbose or terse
57
+ responses, willingness to be pushed back on, anything the agent should
58
+ know before choosing its default posture)_
59
+
60
+ ## Project-scoped notes
61
+
62
+ _(anything specific to this project that would be useful for the agent
63
+ to know but does not belong in the shared requirements tree: a local
64
+ dev workflow quirk, an in-flight side-experiment, a "do not touch this
65
+ directory yet" flag)_
66
+
67
+ ---
68
+
69
+ If you want to share your profile with the team, remove the
70
+ \`rcf/.identity/\` line from the managed block in \`.gitignore\` (or \`git
71
+ add -f rcf/.identity/profile.md\` for one-off sharing). The default is
72
+ per-clone. Sharing is deliberate.
73
+ `;
74
+
75
+ /**
76
+ * Absolute path of the profile file for a given project root.
77
+ *
78
+ * @param {string} projectRoot
79
+ * @returns {string}
80
+ */
81
+ export function identityProfilePath(projectRoot) {
82
+ return join(projectRoot, 'rcf', '.identity', 'profile.md');
83
+ }
84
+
85
+ /**
86
+ * Seed the identity template if not already present. Idempotent - an
87
+ * existing file (any content) is left byte-identical, per AC-4.2.
88
+ *
89
+ * @param {object} args
90
+ * @param {string} args.projectRoot
91
+ * @returns {Promise<{ file: string, action: 'created' | 'kept' }>}
92
+ */
93
+ export async function writeIdentityTemplate({ projectRoot }) {
94
+ const path = identityProfilePath(projectRoot);
95
+ try {
96
+ await readFile(path, 'utf8');
97
+ return { file: 'rcf/.identity/profile.md', action: 'kept' };
98
+ } catch (err) {
99
+ if (/** @type {NodeJS.ErrnoException} */ (err).code !== 'ENOENT') throw err;
100
+ }
101
+ await mkdir(dirname(path), { recursive: true });
102
+ await writeFile(path, IDENTITY_TEMPLATE, 'utf8');
103
+ return { file: 'rcf/.identity/profile.md', action: 'created' };
104
+ }