@smartmemory/compose 0.3.6-beta → 0.3.8

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 (269) hide show
  1. package/.claude/skills/compose/SKILL.md +42 -88
  2. package/.compose-deps.json +1 -13
  3. package/README.md +72 -5
  4. package/bin/compose.js +754 -347
  5. package/bin/git-hooks/pre-push.template +29 -0
  6. package/bin/judgment-import.js +7 -0
  7. package/bin/judgment-migrate.js +387 -0
  8. package/contracts/comp-obs-contract.schema.json +9 -3
  9. package/contracts/feature-json.schema.json +5 -0
  10. package/contracts/fluid-record.schema.json +209 -0
  11. package/contracts/judgment-record.schema.json +425 -4
  12. package/contracts/lifecycle-backfill.schema.json +322 -0
  13. package/dist/assets/App-Z4MU-H_F.js +916 -0
  14. package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
  15. package/dist/assets/arc-DY26UIVo.js +1 -0
  16. package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
  17. package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
  18. package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
  19. package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
  20. package/dist/assets/channel-SnZzzh7k.js +1 -0
  21. package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
  22. package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
  23. package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
  24. package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
  25. package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
  26. package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
  27. package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
  28. package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
  29. package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
  30. package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
  31. package/dist/assets/clone-DgklGjHm.js +1 -0
  32. package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
  33. package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
  34. package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
  35. package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
  36. package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
  37. package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
  38. package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
  39. package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
  40. package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
  41. package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
  42. package/dist/assets/graph-BNzKGvoy.js +1 -0
  43. package/dist/assets/graph-CI_1htl0.js +331 -0
  44. package/dist/assets/index-BEfrNBp8.js +123 -0
  45. package/dist/assets/index-yyrA5OZd.css +1 -0
  46. package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
  47. package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
  48. package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
  49. package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
  50. package/dist/assets/katex-DkKDou_j.js +257 -0
  51. package/dist/assets/layout-BI8cXFPI.js +1 -0
  52. package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
  53. package/dist/assets/min-vPHfnXcC.js +1 -0
  54. package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
  55. package/dist/assets/mobile-B7m9EO9D.js +17 -0
  56. package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
  57. package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
  58. package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
  59. package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
  60. package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
  61. package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
  62. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
  63. package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
  64. package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
  65. package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
  66. package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
  67. package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
  68. package/dist/index.html +3 -3
  69. package/lib/agent-string.js +7 -5
  70. package/lib/append-integrity.js +81 -0
  71. package/lib/backfill-evidence.js +109 -0
  72. package/lib/bug-escalation.js +39 -4
  73. package/lib/build-stream-schema.js +3 -1
  74. package/lib/build-stream-writer.js +25 -0
  75. package/lib/build.js +1624 -195
  76. package/lib/canon-guard.js +245 -0
  77. package/lib/canon-override.js +196 -0
  78. package/lib/canon-registry.js +291 -0
  79. package/lib/cli-commands.js +144 -0
  80. package/lib/codex-preflight.js +50 -15
  81. package/lib/colleague/context.js +215 -0
  82. package/lib/colleague/writeback.js +95 -0
  83. package/lib/completion-gate.js +1421 -0
  84. package/lib/completion-writer.js +47 -47
  85. package/lib/consumer-fanout.js +105 -11
  86. package/lib/coverage-gate.js +200 -0
  87. package/lib/dir-lock.js +170 -0
  88. package/lib/dispatch-ledger.js +301 -0
  89. package/lib/dispatch-metrics.js +236 -0
  90. package/lib/experiment-judge.js +6 -1
  91. package/lib/feature-json.js +1 -1
  92. package/lib/feature-reconciler.js +8 -0
  93. package/lib/feature-validator.js +64 -1
  94. package/lib/feature-writer.js +66 -2
  95. package/lib/fluid/factory.js +167 -0
  96. package/lib/fluid/ideabox-dates.js +73 -0
  97. package/lib/fluid/ideabox-migrate.js +154 -0
  98. package/lib/fluid/ideabox-ops.js +585 -0
  99. package/lib/fluid/ideabox-view.js +146 -0
  100. package/lib/fluid/import-ideabox.js +186 -0
  101. package/lib/fluid/local-provider.js +606 -0
  102. package/lib/fluid/provider.js +684 -0
  103. package/lib/fluid/record-shape.js +214 -0
  104. package/lib/fluid/record-store.js +328 -0
  105. package/lib/fluid/render-ideabox.js +261 -0
  106. package/lib/fluid/schema.js +40 -0
  107. package/lib/fluid/smartmemory-provider.js +1695 -0
  108. package/lib/gsd.js +63 -14
  109. package/lib/guard-cli.js +175 -0
  110. package/lib/guard-custody.js +141 -0
  111. package/lib/guard-descriptors.js +530 -0
  112. package/lib/guard-enrol.js +254 -0
  113. package/lib/health-score.js +1 -1
  114. package/lib/hooks-status.js +32 -3
  115. package/lib/ideabox-cli.js +315 -0
  116. package/lib/ideabox.js +121 -21
  117. package/lib/judgment/store/index.js +166 -0
  118. package/lib/judgment/store/records.js +184 -25
  119. package/lib/judgment/trace.js +380 -0
  120. package/lib/judgment-attest.js +259 -0
  121. package/lib/judgment-decision-write.js +277 -0
  122. package/lib/judgment-decisions.js +466 -0
  123. package/lib/judgment-gen.js +375 -22
  124. package/lib/judgment-verify.js +153 -0
  125. package/lib/judgment-writer.js +2842 -262
  126. package/lib/lane-gate.js +2 -0
  127. package/lib/lifecycle-modes.js +4 -4
  128. package/lib/lineage.js +400 -0
  129. package/lib/local-claude-connector.js +250 -54
  130. package/lib/maya-client.js +302 -0
  131. package/lib/maya-config.js +53 -0
  132. package/lib/maya-identity.js +283 -0
  133. package/lib/mcp-enforcement.js +21 -35
  134. package/lib/migrate-anon.js +5 -0
  135. package/lib/migrate-roadmap.js +15 -0
  136. package/lib/new.js +13 -1
  137. package/lib/pipeline-compat.js +104 -0
  138. package/lib/policy-catalog.js +295 -0
  139. package/lib/policy-check.js +0 -0
  140. package/lib/process-termination.js +98 -0
  141. package/lib/resolve-workspace.js +5 -1
  142. package/lib/result-normalizer.js +428 -153
  143. package/lib/review-normalize.js +4 -0
  144. package/lib/roadmap-errors.js +65 -0
  145. package/lib/roadmap-preservers.js +24 -4
  146. package/lib/roadmap-residue.js +299 -0
  147. package/lib/smartmemory-client.js +614 -78
  148. package/lib/smartmemory-config.js +54 -0
  149. package/lib/smartmemory-ingest.js +19 -2
  150. package/lib/step-prompt.js +7 -6
  151. package/lib/stratum-engine.js +53 -4
  152. package/lib/stratum-mcp-client.js +391 -31
  153. package/lib/test-bootstrap.js +31 -0
  154. package/lib/tool-inventory.js +122 -0
  155. package/lib/version-check.js +91 -19
  156. package/lib/vision-writer.js +88 -1
  157. package/package.json +7 -6
  158. package/pipelines/bug-fix.stratum.yaml +205 -211
  159. package/pipelines/build-quick.profiles.json +12 -0
  160. package/pipelines/build-quick.stratum.yaml +263 -350
  161. package/pipelines/content.stratum.yaml +81 -77
  162. package/pipelines/coverage-sweep.stratum.yaml +49 -30
  163. package/pipelines/plan.stratum.yaml +76 -86
  164. package/pipelines/refactor.stratum.yaml +125 -125
  165. package/pipelines/research.stratum.yaml +56 -58
  166. package/pipelines/review-fix.profiles.json +6 -0
  167. package/pipelines/review-fix.stratum.yaml +110 -83
  168. package/presets/team-feature.profiles.json +6 -0
  169. package/presets/team-feature.stratum.yaml +93 -66
  170. package/presets/team-research.profiles.json +6 -0
  171. package/presets/team-research.stratum.yaml +89 -80
  172. package/presets/team-review.profiles.json +8 -0
  173. package/presets/team-review.stratum.yaml +98 -80
  174. package/scripts/cost-census.mjs +70 -0
  175. package/scripts/guard-sign/compose-guard-sign.sh +62 -0
  176. package/server/agent-health.js +22 -0
  177. package/server/agent-hooks.js +14 -1
  178. package/server/agent-server.js +5 -248
  179. package/server/agent-spawn.js +3 -4
  180. package/server/agent-workspace.js +294 -0
  181. package/server/build-routes.js +6 -5
  182. package/server/build-stream-bridge.js +53 -0
  183. package/server/cc-session-watcher.js +4 -1
  184. package/server/coalescing-buffer.js +7 -1
  185. package/server/completion-projection.js +228 -0
  186. package/server/compose-mcp-tools.js +124 -24
  187. package/server/compose-mcp.js +91 -790
  188. package/server/decision-event-emit.js +41 -2
  189. package/server/decision-event-id.js +17 -0
  190. package/server/decision-events-snapshot.js +3 -0
  191. package/server/design-routes.js +14 -8
  192. package/server/feature-scan.js +76 -2
  193. package/server/file-watcher.js +170 -21
  194. package/server/ideabox-routes.js +166 -224
  195. package/server/index.js +70 -100
  196. package/server/lifecycle-guard.js +240 -10
  197. package/server/lifecycle-phase-history.js +276 -0
  198. package/server/maya-routes.js +507 -0
  199. package/server/mcp-tool-defs.js +940 -0
  200. package/server/mcp-tool-policy.js +35 -3
  201. package/server/model-tiers.js +22 -5
  202. package/server/pipeline-routes.js +21 -11
  203. package/server/project-root.js +58 -19
  204. package/server/remote-utils.js +3 -1
  205. package/server/schema-validator.js +7 -1
  206. package/server/session-manager.js +5 -6
  207. package/server/session-routes.js +3 -1
  208. package/server/stratum-client.js +57 -10
  209. package/server/stratum-sync.js +6 -3
  210. package/server/summarizer.js +3 -4
  211. package/server/supervisor.js +0 -1
  212. package/server/vision-routes.js +208 -98
  213. package/server/vision-server.js +86 -23
  214. package/server/vision-store.js +60 -6
  215. package/server/vision-utils.js +3 -4
  216. package/server/workspace-activity.js +18 -0
  217. package/server/workspace-middleware.js +2 -2
  218. package/server/workspace-runtime.js +243 -0
  219. package/server/worktree-gc.js +1 -0
  220. package/dist/assets/App-BG3ngu8H.js +0 -896
  221. package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
  222. package/dist/assets/arc-_v4hR_uD.js +0 -1
  223. package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
  224. package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
  225. package/dist/assets/channel-B3U5wFAT.js +0 -1
  226. package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
  227. package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
  228. package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
  229. package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
  230. package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
  231. package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
  232. package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
  233. package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
  234. package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
  235. package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
  236. package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
  237. package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
  238. package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
  239. package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
  240. package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
  241. package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
  242. package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
  243. package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
  244. package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
  245. package/dist/assets/graph-BXPQrYYB.js +0 -331
  246. package/dist/assets/graph-C9eacEi8.js +0 -1
  247. package/dist/assets/index-3ZH5eMcZ.js +0 -119
  248. package/dist/assets/index-LIwREYgH.css +0 -1
  249. package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
  250. package/dist/assets/katex-C5jXJg4s.js +0 -257
  251. package/dist/assets/layout-DEXfKzaS.js +0 -1
  252. package/dist/assets/map-Czzmt4hB.js +0 -1
  253. package/dist/assets/mobile-CaoXUwAr.js +0 -17
  254. package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
  255. package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
  256. package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
  257. package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
  258. package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
  259. package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
  260. package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
  261. package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
  262. package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
  263. package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
  264. package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
  265. package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
  266. package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
  267. package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
  268. package/lib/staleness.js +0 -87
  269. package/server/ideabox-cache.js +0 -77
@@ -0,0 +1,215 @@
1
+ /**
2
+ * lib/colleague/context.js — COMP-FOH FOH-6 S2: the per-turn channel_context
3
+ * composer, and the FIRST PRODUCTION CONSUMER of the FOH-3/4/5 capabilities
4
+ * (`challengeIdea`, `convictionOf`, `contradictionsOf` had zero production
5
+ * call sites until here — an acceptance criterion, not a side effect).
6
+ *
7
+ * Compose does the memory reasoning; Maya gets the findings as per-turn
8
+ * `channel_context: [{author, text}]` blocks with explicit provenance authors
9
+ * (`compose:idea IDEA-42`, `compose:conviction`, …). Contracts (design §3):
10
+ *
11
+ * - Sections are DECLARED-capability-derived (`provider.has(CAP.X)`).
12
+ * Absence is structural (the panel's capability strip owns it), not an
13
+ * omission.
14
+ * - Findings are fetched CONCURRENTLY with a per-capability deadline; a
15
+ * section that fails is omitted AND NAMED — never a turn failure, and
16
+ * never a silent hole.
17
+ * - Maya token-caps the block upstream (CHANNEL_CONTEXT_MAX_TOKENS), so the
18
+ * composer enforces its own priority BEFORE sending: contradictions >
19
+ * conviction > challenge > record body (truncated to headline first) >
20
+ * discussion (dropped first). Every drop is named in `omissions`, so the
21
+ * owner never mistakes a truncated turn for a clean one.
22
+ */
23
+
24
+ import {
25
+ findIdea, challengeIdea, convictionOf, contradictionsOf, IdeaboxNotFound,
26
+ } from '../fluid/ideabox-ops.js';
27
+ import { CAP, KIND } from '../fluid/provider.js';
28
+
29
+ /** Sum small enough that the priority sections survive Maya's own token cap. */
30
+ export const DEFAULT_BYTE_BUDGET = 8000;
31
+ /** Safety net over the client-level deadlines (challenge's LLM path caps at 30s). */
32
+ export const CAPABILITY_TIMEOUT_MS = 35000;
33
+
34
+ const MAX_CONTRADICTIONS = 5;
35
+ const MAX_CONFLICTS = 5;
36
+ const MAX_DISCUSSION = 5;
37
+ const MAX_HISTORY = 3;
38
+ const MAX_CORPUS_IDEAS = 10;
39
+
40
+ const bytes = (block) => Buffer.byteLength(block.author) + Buffer.byteLength(block.text);
41
+
42
+ function withDeadline(promise, ms, label) {
43
+ let timer;
44
+ return Promise.race([
45
+ promise,
46
+ new Promise((_, reject) => {
47
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
48
+ timer.unref?.();
49
+ }),
50
+ ]).finally(() => clearTimeout(timer));
51
+ }
52
+
53
+ function headline(idea) {
54
+ const bits = [`status: ${idea.status ?? 'open'}`];
55
+ if (idea.priority) bits.push(`priority: ${idea.priority}`);
56
+ return `[${idea.handle}] ${idea.title} (${bits.join(', ')})`;
57
+ }
58
+
59
+ function recordText(idea) {
60
+ const body = String(idea.body ?? '').trim();
61
+ return body ? `${headline(idea)}\n${body}` : headline(idea);
62
+ }
63
+
64
+ function convictionText(idea, c) {
65
+ const recent = (c.history ?? []).slice(-MAX_HISTORY).map((h) =>
66
+ `"${h.conflictingFact ?? 'unrecorded fact'}" (${h.oldConfidence}→${h.newConfidence}, ${h.reason})`,
67
+ );
68
+ const head = `Conviction for ${idea.handle}: confidence ${c.confidence}`
69
+ + ` (challenged ${c.challengeCount}x${c.lastChallengedAt ? `, last ${c.lastChallengedAt}` : ''})`;
70
+ return recent.length ? `${head}. Recent decays: ${recent.join('; ')}` : head;
71
+ }
72
+
73
+ function contradictionsText(idea, hits) {
74
+ const shown = hits.slice(0, MAX_CONTRADICTIONS);
75
+ const lines = shown.map((h) => `[${h.handle}] ${h.record?.title ?? ''} — ${String(h.record?.body ?? '').slice(0, 200)}`);
76
+ return `Records contradicting ${idea.handle} (${hits.length}):\n${lines.join('\n')}`;
77
+ }
78
+
79
+ function challengeText(idea, result) {
80
+ if (!result.conflicts?.length) return `Challenge scan for ${idea.handle}: no conflicts detected.`;
81
+ const shown = result.conflicts.slice(0, MAX_CONFLICTS);
82
+ const lines = shown.map((c, i) =>
83
+ `${i + 1}. [${c.handle}] (${c.conflictType}, confidence ${c.confidence}): ${c.explanation}`
84
+ + ` — suggests ${c.suggestedResolution}`,
85
+ );
86
+ return `Challenge scan for ${idea.handle}: ${result.conflicts.length} conflict(s).\n${lines.join('\n')}`;
87
+ }
88
+
89
+ function discussionText(idea) {
90
+ const recent = (idea.discussion ?? []).slice(-MAX_DISCUSSION);
91
+ if (!recent.length) return null;
92
+ const lines = recent.map((d) => `- [${d.author ?? 'unattributed'}${d.ts ? ` @ ${d.ts}` : ''}] ${d.text}`);
93
+ return `Recent discussion on ${idea.handle}:\n${lines.join('\n')}`;
94
+ }
95
+
96
+ /**
97
+ * Compose the per-turn context for a colleague turn.
98
+ *
99
+ * @param {object} ctx an ideabox ops context ({provider, cwd, ideaboxPath, …})
100
+ * @param {{focusId?: string|null, byteBudget?: number, capabilityTimeoutMs?: number}} [opts]
101
+ * @returns {Promise<{blocks: Array<{author: string, text: string}>, omissions: string[]}>}
102
+ * @throws {IdeaboxNotFound} when `focusId` names no idea — the relay maps this
103
+ * to the context funnel; it must not silently fall through to plain chat.
104
+ */
105
+ export async function composeColleagueContext(ctx, {
106
+ focusId = null,
107
+ byteBudget = DEFAULT_BYTE_BUDGET,
108
+ capabilityTimeoutMs = CAPABILITY_TIMEOUT_MS,
109
+ } = {}) {
110
+ const omissions = [];
111
+
112
+ // ── corpus-level context: no record in focus ─────────────────────────────
113
+ if (!focusId) {
114
+ const ideas = await ctx.provider.listRecords({ kind: KIND.IDEA });
115
+ const recent = [...ideas]
116
+ .sort((a, b) => String(b.updated_at ?? '').localeCompare(String(a.updated_at ?? '')))
117
+ .slice(0, MAX_CORPUS_IDEAS);
118
+ if (recent.length < ideas.length) {
119
+ omissions.push(`ideas list truncated to ${recent.length} of ${ideas.length}`);
120
+ }
121
+ const lines = recent.map((i) => headline(i));
122
+ return {
123
+ blocks: [{
124
+ author: 'compose:ideabox',
125
+ text: `Recent ideas in this project's ideabox:\n${lines.join('\n')}`,
126
+ }],
127
+ omissions,
128
+ };
129
+ }
130
+
131
+ // ── focused context ──────────────────────────────────────────────────────
132
+ const idea = await findIdea(ctx.provider, focusId);
133
+ if (!idea) throw new IdeaboxNotFound(focusId);
134
+
135
+ /** Fetch one findings section; a failure becomes a named omission. */
136
+ async function section(name, capability, fetch) {
137
+ if (!ctx.provider.has(capability)) return null;
138
+ try {
139
+ return await withDeadline(fetch(), capabilityTimeoutMs, name);
140
+ } catch (err) {
141
+ omissions.push(`${name} unavailable: ${err?.message?.slice(0, 160) ?? 'unknown error'}`);
142
+ return null;
143
+ }
144
+ }
145
+
146
+ const [conviction, contradictions, challenge] = await Promise.all([
147
+ section('conviction', CAP.CONVICTION, () => convictionOf(ctx, idea.handle)),
148
+ section('contradictions', CAP.CONTRADICTION, () => contradictionsOf(ctx, idea.handle)),
149
+ section('challenge', CAP.CHALLENGE, () => challengeIdea(ctx, idea.handle)),
150
+ ]);
151
+
152
+ if (contradictions && contradictions.length > MAX_CONTRADICTIONS) {
153
+ omissions.push(`contradictions truncated to ${MAX_CONTRADICTIONS} of ${contradictions.length}`);
154
+ }
155
+ if (challenge && (challenge.conflicts?.length ?? 0) > MAX_CONFLICTS) {
156
+ omissions.push(`challenge conflicts truncated to ${MAX_CONFLICTS} of ${challenge.conflicts.length}`);
157
+ }
158
+
159
+ // Assembly order is presentation; SURVIVAL order under budget is the
160
+ // design contract, expressed by `drop` rank below (higher = dropped sooner).
161
+ const sections = [];
162
+ sections.push({
163
+ name: 'record body', drop: 4, truncateTo: () => headline(idea),
164
+ block: { author: `compose:idea ${idea.handle}`, text: recordText(idea) },
165
+ });
166
+ if (conviction) {
167
+ sections.push({
168
+ name: 'conviction', drop: 2,
169
+ block: { author: 'compose:conviction', text: convictionText(idea, conviction) },
170
+ });
171
+ }
172
+ if (contradictions?.length) {
173
+ sections.push({
174
+ name: 'contradictions', drop: 1,
175
+ block: { author: 'compose:contradiction', text: contradictionsText(idea, contradictions) },
176
+ });
177
+ }
178
+ if (challenge) {
179
+ sections.push({
180
+ name: 'challenge', drop: 3,
181
+ block: { author: 'compose:challenge', text: challengeText(idea, challenge) },
182
+ });
183
+ }
184
+ const discussion = discussionText(idea);
185
+ if (discussion) {
186
+ sections.push({
187
+ name: 'discussion', drop: 5,
188
+ block: { author: 'compose:discussion', text: discussion },
189
+ });
190
+ }
191
+
192
+ // ── budget enforcement: drop (or truncate) in drop-rank order ────────────
193
+ const total = () => sections.reduce((s, x) => s + bytes(x.block), 0);
194
+ for (const rank of [5, 4, 3, 2, 1]) {
195
+ if (total() <= byteBudget) break;
196
+ const i = sections.findIndex((s) => s.drop === rank);
197
+ if (i === -1) continue;
198
+ const s = sections[i];
199
+ if (s.truncateTo) {
200
+ // The record body truncates to its headline before anything with a
201
+ // lower drop rank is touched; if even the headline doesn't fit, the
202
+ // next pass of the loop cannot shrink it further, so it stays — the
203
+ // budget is a priority ordering, not a hard wire limit (Maya caps
204
+ // upstream regardless).
205
+ s.block = { author: s.block.author, text: s.truncateTo() };
206
+ delete s.truncateTo;
207
+ omissions.push(`record body truncated to headline, over budget`);
208
+ } else {
209
+ sections.splice(i, 1);
210
+ omissions.push(`${s.name} omitted, over budget`);
211
+ }
212
+ }
213
+
214
+ return { blocks: sections.map((s) => s.block), omissions };
215
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * lib/colleague/writeback.js — COMP-FOH FOH-6 S4: Maya joins the discussion
3
+ * trail, idempotently.
4
+ *
5
+ * When a colleague turn was about a record in focus, her reply is appended to
6
+ * that idea's discussion as `author: 'maya'` through the same
7
+ * `addDiscussion` op every other surface uses (migration gate + durable
8
+ * record + projection render, in that order).
9
+ *
10
+ * THE CONTRACT (design §5):
11
+ * - The chat result is authoritative; a write-back failure must never
12
+ * surface as a failed turn. Callers get an OUTCOME, never a throw:
13
+ * 'ok' — appended (or already present: see idempotency)
14
+ * 'landed-unrendered' — the durable record mutation succeeded but the
15
+ * projection render failed; the ideabox file is
16
+ * stale until repaired (`compose ideabox render` /
17
+ * POST /api/ideabox/render)
18
+ * 'failed' — no append landed (as far as we can tell)
19
+ * - Idempotent, keyed on Maya's `message_id`. A 'failed' outcome does not
20
+ * prove the append didn't land (the durable write can succeed and the
21
+ * call still fail afterward), and the trail is append-only — a blind
22
+ * retry would duplicate her reply. Every entry embeds a marker comment,
23
+ * and every attempt is RECONCILE-THEN-APPEND: scan the record's
24
+ * discussion for the marker first, append only if absent.
25
+ *
26
+ * Marker encoding: a trailing HTML comment — invisible in rendered markdown,
27
+ * greppable, zero schema change.
28
+ */
29
+
30
+ import { findIdea, addDiscussion, IdeaboxRenderFailed } from '../fluid/ideabox-ops.js';
31
+
32
+ /** The idempotency marker embedded in every write-back entry. */
33
+ export function markerFor(messageId) {
34
+ return `<!-- maya:msg_${messageId} -->`;
35
+ }
36
+
37
+ /**
38
+ * Serialization per (project, focus, message): the reconcile (marker scan) and
39
+ * the append are two provider calls, and the provider's lock covers only the
40
+ * append — two concurrent retries could both observe absence and both append
41
+ * (Codex r1 P2). All write-backs flow through this one server process, so an
42
+ * in-process promise chain closes the race; a cross-PROCESS writer of maya
43
+ * markers does not exist (the CLI never writes them). If one ever does, this
44
+ * needs a provider-level lease instead.
45
+ */
46
+ const _writebackChain = new Map(); // key -> tail Promise
47
+
48
+ /**
49
+ * One reconcile-then-append attempt, serialized per (project, focus, message).
50
+ * Never throws — the outcome IS the API.
51
+ *
52
+ * @param {object} ctx an ideabox ops context
53
+ * @param {{focusId: string, messageId: string, text: string}} args
54
+ * @returns {Promise<{outcome: 'ok'|'landed-unrendered'|'failed', focusId: string,
55
+ * deduped?: boolean, reason?: string}>}
56
+ */
57
+ export async function writebackReply(ctx, args) {
58
+ const key = `${ctx.cwd}|${String(args.focusId ?? '').toUpperCase()}|${args.messageId}`;
59
+ const tail = _writebackChain.get(key) ?? Promise.resolve();
60
+ const run = tail.catch(() => {}).then(() => writebackReplyUnserialized(ctx, args));
61
+ _writebackChain.set(key, run);
62
+ try {
63
+ return await run;
64
+ } finally {
65
+ if (_writebackChain.get(key) === run) _writebackChain.delete(key);
66
+ }
67
+ }
68
+
69
+ async function writebackReplyUnserialized(ctx, { focusId, messageId, text }) {
70
+ try {
71
+ const idea = await findIdea(ctx.provider, focusId);
72
+ if (!idea) return { outcome: 'failed', focusId, reason: `idea not found: ${focusId}` };
73
+
74
+ const marker = markerFor(messageId);
75
+ const already = (idea.discussion ?? []).some(
76
+ (d) => typeof d?.text === 'string' && d.text.includes(marker),
77
+ );
78
+ // Reconcile: the entry already landed (a prior attempt whose response was
79
+ // lost, or a retry racing a success). Appending again would duplicate her
80
+ // reply in an append-only trail.
81
+ if (already) return { outcome: 'ok', focusId, deduped: true };
82
+
83
+ await addDiscussion(ctx, idea.handle, { author: 'maya', text: `${text}\n\n${marker}` });
84
+ return { outcome: 'ok', focusId };
85
+ } catch (err) {
86
+ if (err instanceof IdeaboxRenderFailed) {
87
+ // The record IS durable — only the generated file is stale. Telling the
88
+ // caller 'failed' here would invite a retry that dedups into a no-op
89
+ // while the projection stays silently stale; the distinct outcome is
90
+ // what makes the repair affordance render.
91
+ return { outcome: 'landed-unrendered', focusId };
92
+ }
93
+ return { outcome: 'failed', focusId, reason: err?.message?.slice(0, 300) ?? 'unknown error' };
94
+ }
95
+ }