@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.
- package/.claude/skills/compose/SKILL.md +42 -88
- package/.compose-deps.json +1 -13
- package/README.md +72 -5
- package/bin/compose.js +754 -347
- package/bin/git-hooks/pre-push.template +29 -0
- package/bin/judgment-import.js +7 -0
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/feature-json.schema.json +5 -0
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/judgment-record.schema.json +425 -4
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
- package/dist/assets/arc-DY26UIVo.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
- package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
- package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
- package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
- package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
- package/dist/assets/clone-DgklGjHm.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
- package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
- package/dist/assets/graph-BNzKGvoy.js +1 -0
- package/dist/assets/graph-CI_1htl0.js +331 -0
- package/dist/assets/index-BEfrNBp8.js +123 -0
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
- package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-BI8cXFPI.js +1 -0
- package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/min-vPHfnXcC.js +1 -0
- package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
- package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
- package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
- package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
- package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
- package/dist/index.html +3 -3
- package/lib/agent-string.js +7 -5
- package/lib/append-integrity.js +81 -0
- package/lib/backfill-evidence.js +109 -0
- package/lib/bug-escalation.js +39 -4
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +1624 -195
- package/lib/canon-guard.js +245 -0
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +291 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +50 -15
- package/lib/colleague/context.js +215 -0
- package/lib/colleague/writeback.js +95 -0
- package/lib/completion-gate.js +1421 -0
- package/lib/completion-writer.js +47 -47
- package/lib/consumer-fanout.js +105 -11
- package/lib/coverage-gate.js +200 -0
- package/lib/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +301 -0
- package/lib/dispatch-metrics.js +236 -0
- package/lib/experiment-judge.js +6 -1
- package/lib/feature-json.js +1 -1
- package/lib/feature-reconciler.js +8 -0
- package/lib/feature-validator.js +64 -1
- package/lib/feature-writer.js +66 -2
- package/lib/fluid/factory.js +167 -0
- package/lib/fluid/ideabox-dates.js +73 -0
- package/lib/fluid/ideabox-migrate.js +154 -0
- package/lib/fluid/ideabox-ops.js +585 -0
- package/lib/fluid/ideabox-view.js +146 -0
- package/lib/fluid/import-ideabox.js +186 -0
- package/lib/fluid/local-provider.js +606 -0
- package/lib/fluid/provider.js +684 -0
- package/lib/fluid/record-shape.js +214 -0
- package/lib/fluid/record-store.js +328 -0
- package/lib/fluid/render-ideabox.js +261 -0
- package/lib/fluid/schema.js +40 -0
- package/lib/fluid/smartmemory-provider.js +1695 -0
- package/lib/gsd.js +63 -14
- package/lib/guard-cli.js +175 -0
- package/lib/guard-custody.js +141 -0
- package/lib/guard-descriptors.js +530 -0
- package/lib/guard-enrol.js +254 -0
- package/lib/health-score.js +1 -1
- package/lib/hooks-status.js +32 -3
- package/lib/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +166 -0
- package/lib/judgment/store/records.js +184 -25
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-attest.js +259 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +375 -22
- package/lib/judgment-verify.js +153 -0
- package/lib/judgment-writer.js +2842 -262
- package/lib/lane-gate.js +2 -0
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +250 -54
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- package/lib/mcp-enforcement.js +21 -35
- package/lib/migrate-anon.js +5 -0
- package/lib/migrate-roadmap.js +15 -0
- package/lib/new.js +13 -1
- package/lib/pipeline-compat.js +104 -0
- package/lib/policy-catalog.js +295 -0
- package/lib/policy-check.js +0 -0
- package/lib/process-termination.js +98 -0
- package/lib/resolve-workspace.js +5 -1
- package/lib/result-normalizer.js +428 -153
- package/lib/review-normalize.js +4 -0
- package/lib/roadmap-errors.js +65 -0
- package/lib/roadmap-preservers.js +24 -4
- package/lib/roadmap-residue.js +299 -0
- package/lib/smartmemory-client.js +614 -78
- package/lib/smartmemory-config.js +54 -0
- package/lib/smartmemory-ingest.js +19 -2
- package/lib/step-prompt.js +7 -6
- package/lib/stratum-engine.js +53 -4
- package/lib/stratum-mcp-client.js +391 -31
- package/lib/test-bootstrap.js +31 -0
- package/lib/tool-inventory.js +122 -0
- package/lib/version-check.js +91 -19
- package/lib/vision-writer.js +88 -1
- package/package.json +7 -6
- package/pipelines/bug-fix.stratum.yaml +205 -211
- package/pipelines/build-quick.profiles.json +12 -0
- package/pipelines/build-quick.stratum.yaml +263 -350
- package/pipelines/content.stratum.yaml +81 -77
- package/pipelines/coverage-sweep.stratum.yaml +49 -30
- package/pipelines/plan.stratum.yaml +76 -86
- package/pipelines/refactor.stratum.yaml +125 -125
- package/pipelines/research.stratum.yaml +56 -58
- package/pipelines/review-fix.profiles.json +6 -0
- package/pipelines/review-fix.stratum.yaml +110 -83
- package/presets/team-feature.profiles.json +6 -0
- package/presets/team-feature.stratum.yaml +93 -66
- package/presets/team-research.profiles.json +6 -0
- package/presets/team-research.stratum.yaml +89 -80
- package/presets/team-review.profiles.json +8 -0
- package/presets/team-review.stratum.yaml +98 -80
- package/scripts/cost-census.mjs +70 -0
- package/scripts/guard-sign/compose-guard-sign.sh +62 -0
- package/server/agent-health.js +22 -0
- package/server/agent-hooks.js +14 -1
- package/server/agent-server.js +5 -248
- package/server/agent-spawn.js +3 -4
- package/server/agent-workspace.js +294 -0
- package/server/build-routes.js +6 -5
- package/server/build-stream-bridge.js +53 -0
- package/server/cc-session-watcher.js +4 -1
- package/server/coalescing-buffer.js +7 -1
- package/server/completion-projection.js +228 -0
- package/server/compose-mcp-tools.js +124 -24
- package/server/compose-mcp.js +91 -790
- package/server/decision-event-emit.js +41 -2
- package/server/decision-event-id.js +17 -0
- package/server/decision-events-snapshot.js +3 -0
- package/server/design-routes.js +14 -8
- package/server/feature-scan.js +76 -2
- package/server/file-watcher.js +170 -21
- package/server/ideabox-routes.js +166 -224
- package/server/index.js +70 -100
- package/server/lifecycle-guard.js +240 -10
- package/server/lifecycle-phase-history.js +276 -0
- package/server/maya-routes.js +507 -0
- package/server/mcp-tool-defs.js +940 -0
- package/server/mcp-tool-policy.js +35 -3
- package/server/model-tiers.js +22 -5
- package/server/pipeline-routes.js +21 -11
- package/server/project-root.js +58 -19
- package/server/remote-utils.js +3 -1
- package/server/schema-validator.js +7 -1
- package/server/session-manager.js +5 -6
- package/server/session-routes.js +3 -1
- package/server/stratum-client.js +57 -10
- package/server/stratum-sync.js +6 -3
- package/server/summarizer.js +3 -4
- package/server/supervisor.js +0 -1
- package/server/vision-routes.js +208 -98
- package/server/vision-server.js +86 -23
- package/server/vision-store.js +60 -6
- package/server/vision-utils.js +3 -4
- package/server/workspace-activity.js +18 -0
- package/server/workspace-middleware.js +2 -2
- package/server/workspace-runtime.js +243 -0
- package/server/worktree-gc.js +1 -0
- package/dist/assets/App-BG3ngu8H.js +0 -896
- package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
- package/dist/assets/arc-_v4hR_uD.js +0 -1
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
- package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
- package/dist/assets/channel-B3U5wFAT.js +0 -1
- package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
- package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
- package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
- package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
- package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
- package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
- package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
- package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
- package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
- package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
- package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
- package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
- package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
- package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
- package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
- package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
- package/dist/assets/graph-BXPQrYYB.js +0 -331
- package/dist/assets/graph-C9eacEi8.js +0 -1
- package/dist/assets/index-3ZH5eMcZ.js +0 -119
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DEXfKzaS.js +0 -1
- package/dist/assets/map-Czzmt4hB.js +0 -1
- package/dist/assets/mobile-CaoXUwAr.js +0 -17
- package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
- package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
- package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
- package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
- package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
- package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
- package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
- package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
- package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
- package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
- package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
- package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
- package/lib/staleness.js +0 -87
- 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
|
+
}
|
package/lib/resolve-workspace.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* resolve-workspace.js — single resolver chain for compose workspaces.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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)
|