@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,295 @@
1
+ /**
2
+ * policy-catalog.js — COMP-POLICY-CHECK-1: local detection-pattern catalog loader.
3
+ *
4
+ * Reads the `## Detection patterns` fenced-yaml convention out of a Claude Code
5
+ * memory directory (`<memory-dir>/feedback_*.md`) and turns it into data. This
6
+ * mirrors SmartMemory's Python loader
7
+ * (`smart-memory-core/smartmemory/adherence/detection.py`) field-for-field —
8
+ * SmartMemory's `memory_get_violation_patterns` MCP tool is a thin wrapper over
9
+ * that same parse, and Compose's engine is plain Node with no MCP client, so we
10
+ * parse the markdown directly rather than fetching it.
11
+ *
12
+ * Pure parsing + caching. No enforcement, no response inspection: that is
13
+ * `lib/policy-check.js`.
14
+ *
15
+ * Degradation contract:
16
+ * - memory dir absent → empty catalog, silent (opt-in feature)
17
+ * - file without a block → skipped, silent (the section is opt-in)
18
+ * - block present but malformed → skipped with a WARNING (authored intent
19
+ * that produced nothing must be visible — no-silent-degradation rule)
20
+ */
21
+
22
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
23
+ import { createHash } from 'node:crypto';
24
+ import { isAbsolute, join, resolve } from 'node:path';
25
+ import { homedir } from 'node:os';
26
+ import YAML from 'yaml';
27
+
28
+ // Leading `---` ... `---` frontmatter block.
29
+ const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n/;
30
+
31
+ // `## Detection patterns` heading followed by the first fenced code block.
32
+ // The fence language tag (```yaml / ```yml / ```) is optional.
33
+ const DETECTION_RE = /^##[ \t]+Detection patterns[ \t]*\n+```(?:ya?ml)?[ \t]*\n([\s\S]*?)\n```/im;
34
+
35
+ const PATTERN_KEYS = ['regex', 'phrase', 'exclude_regex'];
36
+ const DEFAULT_SCAN_TARGET = 'response';
37
+ const DEFAULT_RECENT_TURN_WINDOW = 1;
38
+ const DEFAULT_RULE_TYPE = 'feedback';
39
+
40
+ /** dir → { key, catalog } — per-process, invalidated on max-mtime/file-count change. */
41
+ const catalogCache = new Map();
42
+
43
+ /**
44
+ * Read `.compose/compose.json` → `policyCheck` block. Uncached direct read,
45
+ * try/catch → {} on missing/malformed (same shape as smartmemory-config.js).
46
+ *
47
+ * An ABSENT block means enabled: the check ships as the Compose default and is
48
+ * structurally inert without a catalog. `enabled: false` is the kill switch.
49
+ *
50
+ * @param {string} cwd
51
+ * @returns {{ enabled?: boolean, memoryDir?: string }}
52
+ */
53
+ export function getPolicyCheckConfig(cwd) {
54
+ try {
55
+ const cfg = JSON.parse(readFileSync(join(cwd, '.compose', 'compose.json'), 'utf-8'));
56
+ const block = cfg.policyCheck;
57
+ return block && typeof block === 'object' && !Array.isArray(block) ? block : {};
58
+ } catch {
59
+ return {};
60
+ }
61
+ }
62
+
63
+ /**
64
+ * @param {string} cwd
65
+ * @param {object} [config] pre-read config block (avoids a second file read)
66
+ * @returns {boolean} false only when the kill switch is explicitly set
67
+ */
68
+ export function isPolicyCheckEnabled(cwd, config = getPolicyCheckConfig(cwd)) {
69
+ return config.enabled !== false;
70
+ }
71
+
72
+ /** Expand a leading `~` against the current user's home directory. */
73
+ function expandHome(p) {
74
+ if (p === '~') return homedir();
75
+ if (p.startsWith('~/')) return join(homedir(), p.slice(2));
76
+ return p;
77
+ }
78
+
79
+ /**
80
+ * Claude Code's project-directory encoding: the absolute project path with `/`
81
+ * and `.` replaced by `-` (so `/Users/x/reg/my/App` → `-Users-x-reg-my-App`).
82
+ *
83
+ * @param {string} cwd
84
+ * @returns {string}
85
+ */
86
+ export function encodeProjectDir(cwd) {
87
+ return resolve(cwd).replace(/[/.]/g, '-');
88
+ }
89
+
90
+ /**
91
+ * Resolve the memory directory for a project: config override (absolute, `~`,
92
+ * or cwd-relative) → the default Claude Code project memory dir.
93
+ *
94
+ * @param {string} cwd
95
+ * @param {object} [config]
96
+ * @returns {string} absolute path (existence not checked)
97
+ */
98
+ export function resolveMemoryDir(cwd, config = getPolicyCheckConfig(cwd)) {
99
+ if (typeof config.memoryDir === 'string' && config.memoryDir.length > 0) {
100
+ const expanded = expandHome(config.memoryDir);
101
+ return isAbsolute(expanded) ? expanded : resolve(cwd, expanded);
102
+ }
103
+ return join(homedir(), '.claude', 'projects', encodeProjectDir(cwd), 'memory');
104
+ }
105
+
106
+ /**
107
+ * Normalize a YAML list into the `{regex|phrase|exclude_regex: string}`
108
+ * contract. Non-list → []. Items are reduced to recognized string-valued keys;
109
+ * an item with no such key is dropped, so garbage never reaches consumers that
110
+ * assume string patterns.
111
+ */
112
+ function validPatternItems(value) {
113
+ if (!Array.isArray(value)) return [];
114
+ const items = [];
115
+ for (const item of value) {
116
+ if (!item || typeof item !== 'object' || Array.isArray(item)) continue;
117
+ const cleaned = {};
118
+ for (const key of PATTERN_KEYS) {
119
+ if (typeof item[key] === 'string') cleaned[key] = item[key];
120
+ }
121
+ if (Object.keys(cleaned).length > 0) items.push(cleaned);
122
+ }
123
+ return items;
124
+ }
125
+
126
+ function parseFrontmatter(text) {
127
+ const m = FRONTMATTER_RE.exec(text);
128
+ if (!m) return {};
129
+ try {
130
+ const data = YAML.parse(m[1]);
131
+ return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
132
+ } catch {
133
+ return {};
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Return the parsed detection block, or null when absent/unusable.
139
+ * Absent → null, silently. Present-but-broken → null plus a WARNING.
140
+ */
141
+ function parseDetectionBlock(text, sourceFile) {
142
+ const m = DETECTION_RE.exec(text);
143
+ if (!m) return null; // no block — normal, not an error
144
+
145
+ let data;
146
+ try {
147
+ data = YAML.parse(m[1]);
148
+ } catch (err) {
149
+ // eslint-disable-next-line no-console
150
+ console.warn(`[policy-catalog] invalid YAML in detection block of ${sourceFile}: ${err.message}`);
151
+ return null;
152
+ }
153
+
154
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
155
+ // eslint-disable-next-line no-console
156
+ console.warn(`[policy-catalog] detection block in ${sourceFile} is not a mapping; skipping`);
157
+ return null;
158
+ }
159
+ if (validPatternItems(data.patterns).length === 0) {
160
+ // eslint-disable-next-line no-console
161
+ console.warn(
162
+ `[policy-catalog] detection block in ${sourceFile} has no valid 'patterns' ` +
163
+ "(need {regex|phrase|exclude_regex: str}); skipping"
164
+ );
165
+ return null;
166
+ }
167
+ return data;
168
+ }
169
+
170
+ function safeInt(value, fallback) {
171
+ const n = Number(value);
172
+ return Number.isFinite(n) ? Math.trunc(n) : fallback;
173
+ }
174
+
175
+ /**
176
+ * Load `## Detection patterns` records from a memory directory.
177
+ *
178
+ * @param {string} memoryDir
179
+ * @param {object} [opts]
180
+ * @param {string} [opts.ruleType='feedback'] filename prefix to scan; also the
181
+ * fallback rule type when a file's frontmatter omits `type`
182
+ * @returns {Array<{name: string, description: string, ruleType: string,
183
+ * patterns: object[], suppressionSignals: object[], scanTarget: string,
184
+ * recentTurnWindow: number, sourceFile: string}>}
185
+ */
186
+ export function loadCatalog(memoryDir, { ruleType = DEFAULT_RULE_TYPE } = {}) {
187
+ let names;
188
+ try {
189
+ names = readdirSync(memoryDir);
190
+ } catch {
191
+ // Absent (or unreadable) memory dir: the feature is opt-in, so an empty
192
+ // catalog here is the expected no-op rather than a degradation.
193
+ return [];
194
+ }
195
+
196
+ const files = names
197
+ .filter(n => n.startsWith(`${ruleType}_`) && n.endsWith('.md'))
198
+ .sort();
199
+
200
+ const out = [];
201
+ for (const name of files) {
202
+ const path = join(memoryDir, name);
203
+ let text;
204
+ try {
205
+ text = readFileSync(path, 'utf-8');
206
+ } catch (err) {
207
+ // eslint-disable-next-line no-console
208
+ console.warn(`[policy-catalog] could not read ${path}: ${err.message}`);
209
+ continue;
210
+ }
211
+
212
+ const block = parseDetectionBlock(text, path);
213
+ if (block === null) continue;
214
+
215
+ try {
216
+ const fm = parseFrontmatter(text);
217
+ const metadata = block.metadata && typeof block.metadata === 'object' && !Array.isArray(block.metadata)
218
+ ? block.metadata
219
+ : {};
220
+ out.push({
221
+ name: String(fm.name || name.replace(/\.md$/, '')),
222
+ description: String(fm.description || ''),
223
+ ruleType: String(fm.type || fm?.metadata?.type || ruleType),
224
+ patterns: validPatternItems(block.patterns),
225
+ suppressionSignals: validPatternItems(block.suppression_signals),
226
+ scanTarget: String(metadata.scan_target || DEFAULT_SCAN_TARGET),
227
+ recentTurnWindow: safeInt(metadata.recent_turn_window, DEFAULT_RECENT_TURN_WINDOW),
228
+ sourceFile: path,
229
+ });
230
+ } catch (err) {
231
+ // Defensive: one bad file must not sink the batch.
232
+ // eslint-disable-next-line no-console
233
+ console.warn(`[policy-catalog] failed to build record for ${path}: ${err.message}`);
234
+ }
235
+ }
236
+ return out;
237
+ }
238
+
239
+ /**
240
+ * Cache key for a directory: a digest over the sorted (path, mtimeMs, size) of
241
+ * every rule file. Per-file rather than count + max-mtime, because that older
242
+ * key could not see an edit to an OLDER file while a newer one sat untouched —
243
+ * the max mtime never moved, so a real rule change went unnoticed for the life
244
+ * of the process.
245
+ */
246
+ function cacheKey(memoryDir, ruleType) {
247
+ let names;
248
+ try {
249
+ names = readdirSync(memoryDir);
250
+ } catch {
251
+ return 'missing';
252
+ }
253
+ const parts = [];
254
+ for (const name of names.filter(n => n.startsWith(`${ruleType}_`) && n.endsWith('.md')).sort()) {
255
+ try {
256
+ const { mtimeMs, size } = statSync(join(memoryDir, name));
257
+ parts.push(`${name}:${mtimeMs}:${size}`);
258
+ } catch {
259
+ // Raced deletion between readdir and stat — record the absence so the key
260
+ // still changes rather than silently matching the previous digest.
261
+ parts.push(`${name}:gone`);
262
+ }
263
+ }
264
+ return createHash('sha1').update(parts.join('\n')).digest('hex');
265
+ }
266
+
267
+ /**
268
+ * Resolve + load the catalog for a project, cached per process and invalidated
269
+ * when any rule file changes. Returns [] when the kill switch is set or no
270
+ * catalog exists.
271
+ *
272
+ * @param {string} cwd
273
+ * @param {object} [opts]
274
+ * @param {string} [opts.memoryDir] explicit override (skips config resolution)
275
+ * @param {string} [opts.ruleType='feedback']
276
+ * @returns {Array<object>} catalog records (see loadCatalog)
277
+ */
278
+ export function getCatalog(cwd, { memoryDir, ruleType = DEFAULT_RULE_TYPE } = {}) {
279
+ const config = getPolicyCheckConfig(cwd);
280
+ if (!isPolicyCheckEnabled(cwd, config)) return [];
281
+
282
+ const dir = memoryDir ?? resolveMemoryDir(cwd, config);
283
+ const key = `${ruleType}|${cacheKey(dir, ruleType)}`;
284
+ const cached = catalogCache.get(dir);
285
+ if (cached && cached.key === key) return cached.catalog;
286
+
287
+ const catalog = loadCatalog(dir, { ruleType });
288
+ catalogCache.set(dir, { key, catalog });
289
+ return catalog;
290
+ }
291
+
292
+ /** Test hook: drop the per-process catalog cache. */
293
+ export function _clearCatalogCache() {
294
+ catalogCache.clear();
295
+ }
Binary file
@@ -0,0 +1,98 @@
1
+ /**
2
+ * process-termination.js — graceful teardown for a spawned agent child process.
3
+ *
4
+ * Compose spawns the Claude Code CLI itself (see `lib/local-claude-connector.js`)
5
+ * so that a cancelled run tears down the WHOLE process tree, not just the leader.
6
+ * The child is spawned `detached: true`, which makes it a process-group leader;
7
+ * signalling `-pid` reaches every descendant.
8
+ *
9
+ * Only what compose uses lives here. The teardown contract is:
10
+ * SIGTERM the group → wait up to the grace period → SIGKILL anything still
11
+ * alive → await leader close and group disappearance (bounded at 2 seconds).
12
+ *
13
+ * Grace period: `COMPOSE_CANCEL_GRACE_MS` (default 5000ms).
14
+ */
15
+
16
+ const DEFAULT_GRACE_MS = 5000;
17
+
18
+ /** Read the configured cancellation grace period. */
19
+ function graceMsFromEnv(env = process.env) {
20
+ const raw = env.COMPOSE_CANCEL_GRACE_MS;
21
+ if (raw === undefined || raw === '') return DEFAULT_GRACE_MS;
22
+ const value = Number(raw);
23
+ if (!Number.isFinite(value) || value < 0) {
24
+ throw new Error('COMPOSE_CANCEL_GRACE_MS must be a nonnegative number');
25
+ }
26
+ return value;
27
+ }
28
+
29
+ /**
30
+ * Own the teardown of a spawned child, separately from `child.kill()`, so SDK
31
+ * cleanup cannot bypass the grace period.
32
+ *
33
+ * @param {import('node:child_process').ChildProcess} child
34
+ * @param {boolean} group — signal the whole process group (child was detached)
35
+ * @param {number} [graceMs]
36
+ * @returns {{ close: Promise<void>, terminate: () => Promise<void>, finish: () => Promise<void> }}
37
+ */
38
+ export function processTermination(child, group, graceMs = graceMsFromEnv(), reapTimeoutMs = 2000) {
39
+ let closed = false;
40
+ const close = new Promise((resolve) => child.once('close', () => { closed = true; resolve(); }));
41
+ let teardown;
42
+
43
+ const send = (signal) => {
44
+ if (group && child.pid) {
45
+ try {
46
+ process.kill(-child.pid, signal);
47
+ } catch (error) {
48
+ if (error.code !== 'ESRCH') throw error;
49
+ }
50
+ } else if (!closed) {
51
+ child.kill(signal);
52
+ }
53
+ };
54
+
55
+ // The group outlives the leader: `close` firing does not mean the descendants
56
+ // are gone, so liveness is probed with signal 0 against the group.
57
+ const alive = () => {
58
+ if (!group || !child.pid) return !closed;
59
+ try {
60
+ process.kill(-child.pid, 0);
61
+ return true;
62
+ } catch (error) {
63
+ if (error.code === 'ESRCH') return false;
64
+ throw error;
65
+ }
66
+ };
67
+
68
+ const terminate = () => {
69
+ teardown ??= (async () => {
70
+ send('SIGTERM');
71
+ let timer;
72
+ try {
73
+ await Promise.race([
74
+ new Promise((resolve) => { timer = setTimeout(resolve, graceMs); }),
75
+ // A leader that closed while its group lives must still wait out the
76
+ // grace period, so that branch parks on a never-settling promise.
77
+ close.then(() => (alive() ? new Promise(() => {}) : undefined)),
78
+ ]);
79
+ if (alive()) send('SIGKILL');
80
+ await close;
81
+ const deadline = Date.now() + reapTimeoutMs;
82
+ while (group && alive()) {
83
+ if (Date.now() >= deadline) throw Object.assign(new Error(`Process group still exists after ${reapTimeoutMs}ms reap deadline`), { code: 'CANCELLATION_TEARDOWN_TIMEOUT' });
84
+ await new Promise(resolve => setTimeout(resolve, 10));
85
+ }
86
+ } catch (error) {
87
+ if (error.code !== 'CANCELLATION_TEARDOWN_TIMEOUT') error.code = 'CANCELLATION_UNCONFIRMED';
88
+ throw error;
89
+ } finally {
90
+ if (timer) clearTimeout(timer);
91
+ }
92
+ })();
93
+ void teardown.catch(() => {});
94
+ return teardown;
95
+ };
96
+
97
+ return { close, terminate, finish: () => teardown ?? Promise.resolve() };
98
+ }
@@ -1,7 +1,11 @@
1
1
  /**
2
2
  * resolve-workspace.js — single resolver chain for compose workspaces.
3
3
  *
4
- * Precedence:
4
+ * MCP effective precedence: explicit workspaceId > session binding > COMPOSE_TARGET
5
+ * > discovery. compose-mcp promotes its session binding to hint.workspaceId before
6
+ * calling this resolver, so an established session wins over the process default.
7
+ *
8
+ * Generic resolver precedence (including CLI callers):
5
9
  * 1. explicit hint.workspaceId (cheap upward walk first; falls back to discovery)
6
10
  * 2. COMPOSE_TARGET env (absolute path bypasses discovery; id routes through it)
7
11
  * 3. hint.getBinding() (MCP binding)