@smartmemory/compose 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/compose/SKILL.md +12 -3
- package/.compose-deps.json +51 -25
- package/README.md +79 -7
- package/bin/compose.js +495 -360
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/{_baseUniq-Bo837sRJ.js → _baseUniq-ClWoCPFl.js} +1 -1
- package/dist/assets/{arc-BafGpyqE.js → arc-DY26UIVo.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BOBfUsqL.js → architectureDiagram-Q4EWVU46-6Ggq4DqJ.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-Dwodev1a.js → blockDiagram-DXYQGD6D-CH3Ked0l.js} +1 -1
- package/dist/assets/{browser-1ntj1-x_.js → browser-BWkrenen.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-CU_bhYag.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-p8WsDwnO.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-B8h7-eR0.js → chunk-4TB4RGXK-JytR14a9.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DxeEr98s.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-BYt8F151.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DGSOVeie.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-B-QdgYR2.js → chunk-OYMX7WX6-BySQzVxc.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Du5UAZLs.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-C8JbNBSk.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-S5V4N54A-O1ESaqge.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-CPTmFPHw.js → dagre-KV5264BT-Cp3F5KTn.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-B3PNrWs5.js → diagram-5BDNPKRD-DiR6_2q_.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Cscfr6vc.js → diagram-G4DWMVQ6-w0i-p5HX.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-CSfqZ-TM.js → diagram-MMDJMWI5-tIHhwUv3.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Cg4aYS7W.js → diagram-TYMM5635-BAeY3B19.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-_ZqwG5pl.js → erDiagram-SMLLAGMA-Ckx_Knko.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-C83boxFT.js → flowDiagram-DWJPFMVM-DeoNka6J.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-CWnIjuEi.js → ganttDiagram-T4ZO3ILL-BmGnFbEg.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-DrMdxZfH.js → gitGraphDiagram-UUTBAWPF-Dk48IHsx.js} +1 -1
- package/dist/assets/{graph-RE4I7Ty7.js → graph-BNzKGvoy.js} +1 -1
- package/dist/assets/{graph-Bi99_6Yf.js → graph-CI_1htl0.js} +1 -1
- package/dist/assets/{index-Rm2RE-c0.js → index-BEfrNBp8.js} +3 -3
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/{infoDiagram-42DDH7IO-BLmP4Epr.js → infoDiagram-42DDH7IO-BRf827i0.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-yuWWshKN.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-BOfhaJov.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-Bbolde15.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +1 -1
- package/dist/assets/{layout-BSf33zm8.js → layout-BI8cXFPI.js} +1 -1
- package/dist/assets/{linear-AvSTWMqx.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/{min-QBM8H4xN.js → min-vPHfnXcC.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-BuvgtqIc.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +1 -1
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/{pieDiagram-DEJITSTG-DIzF16vh.js → pieDiagram-DEJITSTG-Cno-gETh.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-D-mbUIjS.js → quadrantDiagram-34T5L4WZ-BUQM1Hfm.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-CEs4kCLd.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-DFsnCr9n.js → sankeyDiagram-XADWPNL6-Crynd3_b.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-BEJYdTjQ.js → sequenceDiagram-FGHM5R23-D9fZdCM8.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-BBXs57uY.js → stateDiagram-FHFEXIEX-CW9qVec8.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-BGvLoVAY.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-9LaBTMe0.js → vennDiagram-DHZGUBPP-BfytJcWk.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-P4MEqMTP.js → wardley-RL74JXVD-DLj-IjyB.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-o-tmxnlC.js → wardleyDiagram-NUSXRM2D-Ds0Ue68c.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-Dpn7V6qk.js → xychartDiagram-5P7HB3ND-vjWDXFL6.js} +1 -1
- 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 +9 -0
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +874 -170
- package/lib/canon-guard.js +28 -6
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +104 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +26 -13
- 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/deps.js +164 -7
- package/lib/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +3 -3
- 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 +57 -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 -23
- 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/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +9 -1
- package/lib/judgment/store/records.js +1 -1
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +5 -1
- package/lib/judgment-writer.js +56 -2
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +52 -1
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- 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 +396 -199
- 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 +271 -36
- 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 +109 -23
- package/server/compose-mcp.js +88 -882
- 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 +34 -2
- 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-PkZzHeMj.js +0 -894
- package/dist/assets/channel-qVK_qn4E.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +0 -1
- package/dist/assets/clone-Pu3RyLUh.js +0 -1
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/mobile-BnXEOE3U.js +0 -17
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +0 -1
- package/lib/staleness.js +0 -87
- package/server/ideabox-cache.js +0 -77
package/lib/new.js
CHANGED
|
@@ -13,6 +13,7 @@ import { promptGate } from './gate-prompt.js';
|
|
|
13
13
|
import { VisionWriter } from './vision-writer.js';
|
|
14
14
|
import { readFlowRound } from './flow-state.js';
|
|
15
15
|
import { validateStep } from './step-validator.js';
|
|
16
|
+
import { tsCompatibilityOf, quarantineMessage } from './pipeline-compat.js';
|
|
16
17
|
|
|
17
18
|
const KICKOFF_VALIDATION = new Map([
|
|
18
19
|
['research', {
|
|
@@ -81,7 +82,18 @@ export async function runNew(intent, opts = {}) {
|
|
|
81
82
|
throw new Error(`Kickoff spec not found: ${specPath}. Run 'compose init' to get default pipelines.`);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
// COMP-PIPELINE-QUARANTINE follow-up: `compose new` has its own runner and
|
|
86
|
+
// calls stratum.plan directly, so it never passed through runBuild's
|
|
87
|
+
// compatibility check. An existing workspace pinning an old new.stratum.yaml
|
|
88
|
+
// (init never overwrites one) still got the opaque engine rejection this check
|
|
89
|
+
// exists to replace.
|
|
90
|
+
const specText = readFileSync(specPath, 'utf-8');
|
|
91
|
+
const specCompat = tsCompatibilityOf(specText);
|
|
92
|
+
if (!specCompat.compatible) {
|
|
93
|
+
throw new Error(quarantineMessage(specPath, specCompat));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const spec = YAML.parse(specText);
|
|
85
97
|
if (opts.skipResearch) {
|
|
86
98
|
const research = spec.flows?.new?.steps?.find((step) => step.id === 'research');
|
|
87
99
|
if (research) research.when = 'false';
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TS-engine spec compatibility.
|
|
3
|
+
*
|
|
4
|
+
* COMP-PIPELINE-QUARANTINE: STRAT-PY-RETIRE converted only the two production
|
|
5
|
+
* pipelines (build, gsd) from the v0.3 dialect to TS v1 — see compose commit
|
|
6
|
+
* 9221548, "Both production pipelines (build, gsd) are re-authored as TS v1".
|
|
7
|
+
* Everything else in pipelines/ and presets/ stayed behind — most on v0.3,
|
|
8
|
+
* bug-fix on v0.1 — and the older execution paths were deleted, so those specs
|
|
9
|
+
* cannot run at all: the engine
|
|
10
|
+
* refuses them at `stratum_plan` with a bare `MCP error -32602: spec validation
|
|
11
|
+
* failed`, which tells the user nothing about why.
|
|
12
|
+
*
|
|
13
|
+
* This module is the single place that answers "can the TS engine run this
|
|
14
|
+
* spec". It reads the spec's OWN version stamp rather than consulting a list of
|
|
15
|
+
* pipeline names — so migrating a spec to `version: 1` lifts its quarantine
|
|
16
|
+
* automatically, with nothing here to remember to update.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { parse as parseYaml } from 'yaml';
|
|
20
|
+
|
|
21
|
+
/** The only spec dialect the TS engine accepts. */
|
|
22
|
+
export const TS_SPEC_VERSION = 1;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The specs `compose init` seeds into a workspace's pipelines/ directory.
|
|
26
|
+
*
|
|
27
|
+
* Shared with resolveTemplatePath, which deliberately does NOT fall back to the
|
|
28
|
+
* bundled copy for these: their absence means the workspace was never
|
|
29
|
+
* initialized, and failing loudly with "Lifecycle spec not found" is the honest
|
|
30
|
+
* answer. Silently running Compose's own bundled build pipeline against an
|
|
31
|
+
* uninitialized project would be worse than the error. Every OTHER shipped
|
|
32
|
+
* pipeline (content, coverage-sweep, refactor, research, review-fix) is never
|
|
33
|
+
* copied by init, so the bundled copy is its only source and the fallback is the
|
|
34
|
+
* only way `--template research` can resolve at all.
|
|
35
|
+
*/
|
|
36
|
+
export const INIT_PROVISIONED_SPECS = Object.freeze([
|
|
37
|
+
'build',
|
|
38
|
+
'build-quick',
|
|
39
|
+
'bug-fix',
|
|
40
|
+
'new',
|
|
41
|
+
'plan',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Classify a spec's engine compatibility.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately shallow: a `version: 1` stamp means "authored for this engine",
|
|
48
|
+
* not "valid". Full validation belongs to the engine, which reports precise
|
|
49
|
+
* errors. This exists to turn the common, uninformative failure — a spec from
|
|
50
|
+
* the retired dialect — into a message that names the cause.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} specText Raw spec YAML
|
|
53
|
+
* @returns {{compatible: boolean, version: unknown, reason: string|null}}
|
|
54
|
+
*/
|
|
55
|
+
export function tsCompatibilityOf(specText) {
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = parseYaml(specText);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return { compatible: false, version: null, reason: `spec is not parseable YAML: ${err.message}` };
|
|
61
|
+
}
|
|
62
|
+
const version = parsed?.version ?? null;
|
|
63
|
+
if (version === TS_SPEC_VERSION) return { compatible: true, version, reason: null };
|
|
64
|
+
if (version === null) {
|
|
65
|
+
return { compatible: false, version, reason: 'spec declares no `version`' };
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
compatible: false,
|
|
69
|
+
version,
|
|
70
|
+
reason: `spec declares version ${JSON.stringify(version)}, but the TS engine only runs version ${TS_SPEC_VERSION}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** True iff the TS engine will accept this spec's dialect. */
|
|
75
|
+
export function isTsEngineSpec(specText) {
|
|
76
|
+
return tsCompatibilityOf(specText).compatible;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The message shown when a quarantined pipeline is invoked. Names the spec, the
|
|
81
|
+
* cause, and the only two ways forward — never a raw engine error code.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} specPath Absolute path of the offending spec
|
|
84
|
+
* @param {{reason: string|null}} compat Result of tsCompatibilityOf
|
|
85
|
+
*/
|
|
86
|
+
export function quarantineMessage(specPath, compat) {
|
|
87
|
+
// Only a spec that declares an OLDER dialect gets the migration story. A
|
|
88
|
+
// malformed or version-less spec is far more likely to be a fresh typo, and
|
|
89
|
+
// telling its author it was "left behind by STRAT-PY-RETIRE" is a confident
|
|
90
|
+
// false history that sends them looking in the wrong place.
|
|
91
|
+
const isRetiredDialect = typeof compat.version === 'string' && /^0\./.test(compat.version);
|
|
92
|
+
const lines = [
|
|
93
|
+
`This pipeline cannot run: ${specPath}`,
|
|
94
|
+
` ${compat.reason}.`,
|
|
95
|
+
];
|
|
96
|
+
if (isRetiredDialect) {
|
|
97
|
+
lines.push(
|
|
98
|
+
' It was left on a retired dialect when STRAT-PY-RETIRE cut the engine over to TS v1 and',
|
|
99
|
+
' deleted the older execution path, so it has been unrunnable since that cutover.',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
lines.push(` The spec must declare \`version: ${TS_SPEC_VERSION}\` and use the TS v1 dialect.`);
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|
|
@@ -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)
|