@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
|
@@ -48,3 +48,57 @@ export function resolveProjectTag(cwd) {
|
|
|
48
48
|
export function sourcePathFor(projectTag, repoRel) {
|
|
49
49
|
return `compose/${projectTag}/${repoRel}`;
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* GOV-COMPOSE-SEAM-1 step 0 (`plumbing`).
|
|
54
|
+
*
|
|
55
|
+
* Translate the `smartmemory` config block into the three env vars Stratum's
|
|
56
|
+
* policy client reads, so Compose's ingest events and Stratum's enforcement
|
|
57
|
+
* events land in ONE workspace instead of two (or, today, one and nowhere).
|
|
58
|
+
*
|
|
59
|
+
* Two traps this function exists to close:
|
|
60
|
+
*
|
|
61
|
+
* 1. `workspaceId` is two different things. Top-level `compose.json#workspaceId`
|
|
62
|
+
* is a Compose PROJECT SLUG (`^[a-z][a-z0-9-]{1,63}$`, see
|
|
63
|
+
* discover-workspaces.js) used for provenance tagging. The SmartMemory
|
|
64
|
+
* workspace id lives at `smartmemory.workspaceId` and looks like
|
|
65
|
+
* `team_26f0bbe60a4c`. Sending the slug as `X-Workspace-Id` scopes every
|
|
66
|
+
* event to a workspace that does not exist, and the API answers 200, so the
|
|
67
|
+
* failure is silent. This reads the block, never the top level.
|
|
68
|
+
* 2. Stratum's `SmartMemoryClient` reads `process.env` ONCE at construction
|
|
69
|
+
* (policy/smartmemory_client.ts). Setting these after the MCP subprocess
|
|
70
|
+
* starts is a no-op — they must be in the spawn env, which is why this
|
|
71
|
+
* returns a plain object for `connect()` to merge rather than mutating
|
|
72
|
+
* `process.env`.
|
|
73
|
+
*
|
|
74
|
+
* Fail-quiet by design, matching the existing fail-open ingest contract: an
|
|
75
|
+
* unconfigured, disabled, or half-configured workspace returns `{}` and Stratum
|
|
76
|
+
* simply runs without policy delivery (it warns once on its own). A partial env
|
|
77
|
+
* would be worse than none — it produces events addressed to nowhere.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} cwd
|
|
80
|
+
* @returns {{SMARTMEMORY_API_URL?: string, SMARTMEMORY_API_KEY?: string, SMARTMEMORY_WORKSPACE_ID?: string}}
|
|
81
|
+
*/
|
|
82
|
+
export function resolveStratumPolicyEnv(cwd) {
|
|
83
|
+
let cfg;
|
|
84
|
+
try {
|
|
85
|
+
cfg = getSmartmemoryConfig(cwd);
|
|
86
|
+
} catch {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
if (cfg.enabled !== true) return {};
|
|
90
|
+
|
|
91
|
+
const baseUrl = typeof cfg.baseUrl === 'string' ? cfg.baseUrl.trim() : '';
|
|
92
|
+
// NEVER cfg.workspaceId's top-level namesake — see trap 1 above.
|
|
93
|
+
const workspaceId = typeof cfg.workspaceId === 'string' ? cfg.workspaceId.trim() : '';
|
|
94
|
+
const apiKey = cfg.apiKeyEnv ? process.env[cfg.apiKeyEnv] : undefined;
|
|
95
|
+
|
|
96
|
+
// All three or nothing.
|
|
97
|
+
if (!baseUrl || !workspaceId || !apiKey) return {};
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
SMARTMEMORY_API_URL: baseUrl,
|
|
101
|
+
SMARTMEMORY_API_KEY: apiKey,
|
|
102
|
+
SMARTMEMORY_WORKSPACE_ID: workspaceId,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -26,9 +26,23 @@ export function renderFeatureEventContent(row, projectTag) {
|
|
|
26
26
|
return `[compose:${projectTag}] ${row.ts} ${row.tool} ${row.code ?? row.feature_code ?? '-'} by ${row.actor ?? 'mcp:agent'}`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/** PURE. One deterministic line per gate-log entry. Shared with sync.
|
|
29
|
+
/** PURE. One deterministic line per gate-log entry. Shared with sync.
|
|
30
|
+
*
|
|
31
|
+
* GOV-COMPOSE-SEAM-1 step 0 (F2): this is a **Vision UI product gate** — a
|
|
32
|
+
* human approving a feature in the Compose UI, keyed by the UI gate id and a
|
|
33
|
+
* feature code. It is NOT Stratum's `gate_resolution`, which is a flow-step
|
|
34
|
+
* gate keyed by `run_id`/`step_id` and emitted by the engine when a run carries
|
|
35
|
+
* a policy bundle. The design assumed these were two records of one event and
|
|
36
|
+
* called for deduping them by `run_id`; they are two events, and no Stratum
|
|
37
|
+
* run id exists at this call site (see gate-log-store.js). Deduping them would
|
|
38
|
+
* have destroyed a real record.
|
|
39
|
+
*
|
|
40
|
+
* So they are disambiguated instead of merged: this line is tagged
|
|
41
|
+
* `vision-gate` rather than the bare `gate`, so a workspace query can separate
|
|
42
|
+
* product approvals from enforcement gates without guessing from shape.
|
|
43
|
+
*/
|
|
30
44
|
export function renderGateLogContent(entry, projectTag) {
|
|
31
|
-
return `[compose:${projectTag}] ${entry.timestamp} gate:${entry.decision} ${entry.feature_code ?? '-'} ${entry.id}`;
|
|
45
|
+
return `[compose:${projectTag}] ${entry.timestamp} vision-gate:${entry.decision} ${entry.feature_code ?? '-'} ${entry.id}`;
|
|
32
46
|
}
|
|
33
47
|
|
|
34
48
|
/** PURE. context for an event item (cli:compose regime — content-hash dedupe). */
|
|
@@ -45,6 +59,9 @@ export function buildGateLogContext(projectTag, entry) {
|
|
|
45
59
|
return {
|
|
46
60
|
origin: 'cli:compose',
|
|
47
61
|
project: projectTag,
|
|
62
|
+
// Explicit record kind so a Compose product-gate approval is never mistaken
|
|
63
|
+
// for a Stratum enforcement `gate_resolution`. See renderGateLogContent.
|
|
64
|
+
record_kind: 'compose_vision_gate',
|
|
48
65
|
source_path: sourcePathFor(projectTag, '.compose/data/gate-log.jsonl'),
|
|
49
66
|
event: entry,
|
|
50
67
|
};
|
package/lib/step-prompt.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
import {
|
|
7
|
+
import { findStaleArtifacts } from './lineage.js';
|
|
8
8
|
import { readHypotheses, formatRejectedHypotheses } from './bug-ledger.js';
|
|
9
9
|
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
@@ -242,13 +242,14 @@ export function buildGateContext(gateDispatch, context, gateExtras) {
|
|
|
242
242
|
sections.push(`## Files Changed by This Feature\n${context.filesChanged.map(f => '- ' + f).join('\n')}`);
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
// Staleness warnings — flag artifacts
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
// Staleness warnings — derivation-based (COMP-PROV-LINEAGE): flag artifacts
|
|
246
|
+
// whose upstream (wasDerivedFrom) changed more recently than they did. Phase-
|
|
247
|
+
// independent, so it does not need gateExtras.toPhase.
|
|
248
|
+
if (context.featureDir) {
|
|
249
|
+
const stale = findStaleArtifacts(context.featureDir).filter(a => a.stale);
|
|
249
250
|
if (stale.length > 0) {
|
|
250
251
|
const lines = stale.map(a =>
|
|
251
|
-
`- **${a.file}**
|
|
252
|
+
`- **${a.file}** may be outdated — an upstream it was derived from (${a.staleAgainst.join(', ')}) changed more recently`
|
|
252
253
|
);
|
|
253
254
|
sections.push(`## Stale Artifacts\nThe following artifacts may be outdated:\n${lines.join('\n')}`);
|
|
254
255
|
}
|
package/lib/stratum-engine.js
CHANGED
|
@@ -137,6 +137,46 @@ const BIN_CONFIG = {
|
|
|
137
137
|
},
|
|
138
138
|
};
|
|
139
139
|
|
|
140
|
+
// Sibling checkout's package.json — used only to tell the user when the
|
|
141
|
+
// installed dependency is shadowing a checkout at a different version.
|
|
142
|
+
export const LIVE_STRATUM_TS_PACKAGE_JSON = resolve(
|
|
143
|
+
__dirname, '..', '..', 'stratum', 'ts', 'package.json',
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
function readPackageVersion(packageJsonPath) {
|
|
147
|
+
try {
|
|
148
|
+
const version = JSON.parse(readFileSync(packageJsonPath, 'utf8'))?.version;
|
|
149
|
+
return typeof version === 'string' ? version : undefined;
|
|
150
|
+
} catch {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// One warning per (kind, paths, versions) per process: the resolver runs on every connect.
|
|
156
|
+
const shadowWarned = new Set();
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The installed dependency wins over the sibling checkout by design (CI has no
|
|
160
|
+
* sibling). In the monorepo that silently runs a STALE stratum after a local
|
|
161
|
+
* rebuild — found 2026-08-30 when the STRAT-LEARN-COST census ran against
|
|
162
|
+
* 0.3.3 (no `stratum_usage_report`) while `stratum/ts` was at 0.3.4. Say so
|
|
163
|
+
* once, with the escape hatch, whenever the two versions differ.
|
|
164
|
+
*/
|
|
165
|
+
function warnIfSiblingShadowed(kind, envVar, installedPackageJsonPath, siblingPackageJsonPath, warn) {
|
|
166
|
+
if (!siblingPackageJsonPath || !existsSync(siblingPackageJsonPath)) return;
|
|
167
|
+
const installedVersion = readPackageVersion(installedPackageJsonPath);
|
|
168
|
+
const siblingVersion = readPackageVersion(siblingPackageJsonPath);
|
|
169
|
+
if (!installedVersion || !siblingVersion || installedVersion === siblingVersion) return;
|
|
170
|
+
const key = `${kind}|${installedPackageJsonPath}|${siblingPackageJsonPath}|${installedVersion}|${siblingVersion}`;
|
|
171
|
+
if (shadowWarned.has(key)) return;
|
|
172
|
+
shadowWarned.add(key);
|
|
173
|
+
warn(
|
|
174
|
+
`[stratum-engine] using installed @smartmemory/stratum ${installedVersion}; `
|
|
175
|
+
+ `the sibling checkout at ${dirname(siblingPackageJsonPath)} is ${siblingVersion} and is NOT what compose runs. `
|
|
176
|
+
+ `Set ${envVar} to run a rebuilt checkout (${kind === 'mcp' ? 'dist/mcp/main.js' : 'dist/cli/stratum.js'}).`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
140
180
|
/** Resolve a bin from an installed @smartmemory/stratum package, if present. */
|
|
141
181
|
function resolveInstalledBin(packageBin, requireResolve) {
|
|
142
182
|
let packageJsonPath;
|
|
@@ -156,7 +196,7 @@ function resolveInstalledBin(packageBin, requireResolve) {
|
|
|
156
196
|
? packageJson.bin?.[packageBin]
|
|
157
197
|
: undefined;
|
|
158
198
|
if (typeof relativeBin !== 'string') return undefined;
|
|
159
|
-
return resolve(dirname(packageJsonPath), relativeBin);
|
|
199
|
+
return { bin: resolve(dirname(packageJsonPath), relativeBin), packageJsonPath };
|
|
160
200
|
} catch {
|
|
161
201
|
return undefined;
|
|
162
202
|
}
|
|
@@ -168,7 +208,7 @@ function resolveInstalledBin(packageBin, requireResolve) {
|
|
|
168
208
|
*
|
|
169
209
|
* @param {'mcp'|'cli'} kind
|
|
170
210
|
* @param {string} [cwd]
|
|
171
|
-
* @param {{env?: object, requireResolve?: Function, siblingBins?: object}} [deps]
|
|
211
|
+
* @param {{env?: object, requireResolve?: Function, siblingBins?: object, siblingPackageJson?: string, warn?: Function}} [deps]
|
|
172
212
|
*/
|
|
173
213
|
export function resolveStratumBin(kind, cwd = process.cwd(), deps = {}) {
|
|
174
214
|
const config = BIN_CONFIG[kind];
|
|
@@ -180,11 +220,20 @@ export function resolveStratumBin(kind, cwd = process.cwd(), deps = {}) {
|
|
|
180
220
|
|| (kind === 'cli' ? env.COMPOSE_STRATUM_TS_BIN : undefined);
|
|
181
221
|
if (envCandidate && existsSync(envCandidate)) return envCandidate;
|
|
182
222
|
|
|
183
|
-
const
|
|
223
|
+
const installed = resolveInstalledBin(
|
|
184
224
|
config.packageBin,
|
|
185
225
|
deps.requireResolve ?? require.resolve.bind(require),
|
|
186
226
|
);
|
|
187
|
-
if (
|
|
227
|
+
if (installed && existsSync(installed.bin)) {
|
|
228
|
+
warnIfSiblingShadowed(
|
|
229
|
+
kind,
|
|
230
|
+
config.env,
|
|
231
|
+
installed.packageJsonPath,
|
|
232
|
+
deps.siblingPackageJson ?? LIVE_STRATUM_TS_PACKAGE_JSON,
|
|
233
|
+
deps.warn ?? ((message) => console.warn(message)),
|
|
234
|
+
);
|
|
235
|
+
return installed.bin;
|
|
236
|
+
}
|
|
188
237
|
|
|
189
238
|
const siblingCandidate = deps.siblingBins?.[kind] ?? config.sibling;
|
|
190
239
|
if (siblingCandidate && existsSync(siblingCandidate)) return siblingCandidate;
|
|
@@ -21,6 +21,7 @@ import YAML from 'yaml';
|
|
|
21
21
|
import { validateBuildStreamEvent } from './build-stream-schema.js';
|
|
22
22
|
import { appendEvent, resolveDispatchLedgerCwd } from './dispatch-ledger.js';
|
|
23
23
|
import { resolveStratumMcpConnection } from './stratum-engine.js';
|
|
24
|
+
import { resolveStratumPolicyEnv } from './smartmemory-config.js';
|
|
24
25
|
|
|
25
26
|
const RUNTIME_INPUT_REF = /^\$\.input\.([A-Za-z_][A-Za-z0-9_]*)$/;
|
|
26
27
|
|
|
@@ -152,12 +153,9 @@ export function resolveStepProfile(profiles, stepId) {
|
|
|
152
153
|
|
|
153
154
|
/**
|
|
154
155
|
* Build the TS `stratum_agent_run` request from an agent string + compose-side
|
|
155
|
-
* options.
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* are NOT sent — they are compose-side concerns applied at the invocation
|
|
159
|
-
* (resolveAgentConfig). `agent` is normalized to the bare provider literal the
|
|
160
|
-
* engine requires; `cwd` is required and defaults to the process cwd.
|
|
156
|
+
* options. Capability restrictions and reasoning are execution requirements,
|
|
157
|
+
* not telemetry: preserve them on the wire. The server validates provider
|
|
158
|
+
* support, and refuses unsupported options instead of silently dropping them.
|
|
161
159
|
*/
|
|
162
160
|
export function buildAgentRunRequest(agentType, prompt, opts = {}) {
|
|
163
161
|
const provider = String(agentType ?? 'claude').split(':', 1)[0] || 'claude';
|
|
@@ -167,6 +165,11 @@ export function buildAgentRunRequest(agentType, prompt, opts = {}) {
|
|
|
167
165
|
cwd: opts.cwd ?? process.cwd(),
|
|
168
166
|
...(opts.modelID ? { model: opts.modelID } : {}),
|
|
169
167
|
...(opts.sandboxMode ? { sandboxMode: opts.sandboxMode } : {}),
|
|
168
|
+
...(opts.allowedTools !== undefined ? { allowedTools: opts.allowedTools } : {}),
|
|
169
|
+
...(opts.disallowedTools !== undefined ? { disallowedTools: opts.disallowedTools } : {}),
|
|
170
|
+
...(opts.thinking !== undefined ? { thinking: opts.thinking } : {}),
|
|
171
|
+
...(opts.effort !== undefined ? { effort: opts.effort } : {}),
|
|
172
|
+
...(opts.cancellationId !== undefined ? { cancellationId: opts.cancellationId } : {}),
|
|
170
173
|
};
|
|
171
174
|
}
|
|
172
175
|
|
|
@@ -183,6 +186,8 @@ export class StratumMcpClient {
|
|
|
183
186
|
#client = null;
|
|
184
187
|
#transport = null;
|
|
185
188
|
#connected = false;
|
|
189
|
+
#toolNames = null;
|
|
190
|
+
#agentFields = null;
|
|
186
191
|
// STRAT-PAR-STREAM: subscribers keyed by `${flowId}::${stepId}` → Set<handler>
|
|
187
192
|
#eventSubs = new Map();
|
|
188
193
|
|
|
@@ -237,11 +242,7 @@ export class StratumMcpClient {
|
|
|
237
242
|
const dispatchId = randomUUID();
|
|
238
243
|
const startedAt = Date.now();
|
|
239
244
|
try {
|
|
240
|
-
const result = await this.#
|
|
241
|
-
'stratum_agent_run',
|
|
242
|
-
buildAgentRunRequest(agentType, prompt, opts),
|
|
243
|
-
callOpts,
|
|
244
|
-
);
|
|
245
|
+
const result = await this.#invokeAgentRun(agentType, prompt, opts, callOpts);
|
|
245
246
|
this.#recordAgentDispatch(
|
|
246
247
|
dispatchId,
|
|
247
248
|
agentType,
|
|
@@ -266,6 +267,69 @@ export class StratumMcpClient {
|
|
|
266
267
|
}
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
async #invokeAgentRun(agentType, prompt, opts, callOpts) {
|
|
271
|
+
const signal = opts.signal;
|
|
272
|
+
const cancellationId = opts.cancellationId ?? (signal ? randomUUID() : undefined);
|
|
273
|
+
const request = buildAgentRunRequest(agentType, prompt, { ...opts, cancellationId });
|
|
274
|
+
signal?.throwIfAborted();
|
|
275
|
+
// Probe under the same deadline as execution. No run has been dispatched yet,
|
|
276
|
+
// so aborting a stalled probe requires no server cancellation acknowledgement.
|
|
277
|
+
if (this.#connected) {
|
|
278
|
+
if (!this.#agentFields) {
|
|
279
|
+
const listed = await abortable(this.#client.listTools(undefined, { signal }), signal);
|
|
280
|
+
const tool = listed.tools.find(entry => entry.name === 'stratum_agent_run');
|
|
281
|
+
this.#agentFields = new Set(Object.keys(tool?.inputSchema?.properties ?? {}));
|
|
282
|
+
}
|
|
283
|
+
const missing = Object.keys(request).filter(key => !this.#agentFields.has(key));
|
|
284
|
+
if (missing.length) {
|
|
285
|
+
const installed = this.#client.getServerVersion()?.version ?? 'unknown';
|
|
286
|
+
throw new StratumError('UNSUPPORTED_AGENT_OPTIONS',
|
|
287
|
+
`Installed Stratum ${installed} does not support ${missing.join(', ')} required by this call; ` +
|
|
288
|
+
'required execution surface: 17 (@smartmemory/stratum >=0.4.0).', '');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
signal?.throwIfAborted();
|
|
292
|
+
const teardownMs = opts.cancellationTimeoutMs ?? Number(process.env.COMPOSE_CANCEL_TIMEOUT_MS ?? 15000);
|
|
293
|
+
const rpc = this.#callTool('stratum_agent_run', request, callOpts);
|
|
294
|
+
// Keep the original rejection intact, including on a dead transport. Only an
|
|
295
|
+
// explicit abort initiates cancellation; a failed RPC alone must remain retryable.
|
|
296
|
+
const outcome = rpc.then(value => ({ value }), error => ({ error }));
|
|
297
|
+
let cancellation;
|
|
298
|
+
let rejectAbort;
|
|
299
|
+
const aborted = new Promise((_resolve, reject) => { rejectAbort = reject; });
|
|
300
|
+
const cancel = () => {
|
|
301
|
+
cancellation ??= (async () => {
|
|
302
|
+
let ack;
|
|
303
|
+
try {
|
|
304
|
+
ack = await cancellationDeadline(this.cancelAgentRun(cancellationId), teardownMs, 'Cancellation acknowledgement');
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (error?.code === 'CANCELLATION_TEARDOWN_TIMEOUT') throw error;
|
|
307
|
+
throw new StratumError('CANCELLATION_UNCONFIRMED', `Foreground cancellation could not be acknowledged: ${error.message}`, '');
|
|
308
|
+
}
|
|
309
|
+
if (!['cancelled', 'already_cancelled', 'already_complete', 'already_error', 'not_found'].includes(ack?.status)) {
|
|
310
|
+
throw new StratumError('CANCELLATION_UNCONFIRMED',
|
|
311
|
+
`Stratum did not confirm foreground termination (${ack?.status ?? 'missing status'})`, '');
|
|
312
|
+
}
|
|
313
|
+
const settled = await cancellationDeadline(outcome, teardownMs, 'Original agent RPC after cancellation acknowledgement');
|
|
314
|
+
const error = new Error('Agent execution cancelled after termination was acknowledged');
|
|
315
|
+
error.name = 'AbortError';
|
|
316
|
+
for (const key of ['usage', 'telemetry', 'split', 'usdSource']) {
|
|
317
|
+
if ((settled.error ?? settled.value)?.[key] !== undefined) error[key] = (settled.error ?? settled.value)[key];
|
|
318
|
+
}
|
|
319
|
+
throw error;
|
|
320
|
+
})();
|
|
321
|
+
cancellation.catch(rejectAbort);
|
|
322
|
+
};
|
|
323
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
324
|
+
if (signal?.aborted) cancel();
|
|
325
|
+
try {
|
|
326
|
+
const settled = await Promise.race([outcome, aborted]);
|
|
327
|
+
if (cancellation) await cancellation;
|
|
328
|
+
if (settled.error) throw settled.error;
|
|
329
|
+
return settled.value;
|
|
330
|
+
} finally { signal?.removeEventListener('abort', cancel); }
|
|
331
|
+
}
|
|
332
|
+
|
|
269
333
|
/**
|
|
270
334
|
* Subscribe to BuildStreamEvent push notifications scoped to a (flowId, stepId).
|
|
271
335
|
* Handler receives a parsed BuildStreamEvent envelope. Returns an unsubscribe fn.
|
|
@@ -385,7 +449,15 @@ export class StratumMcpClient {
|
|
|
385
449
|
// agent step failed with `403 forbidden / "Request not allowed"`. Pass the
|
|
386
450
|
// full env through; the stratum connector scrubs SENSITIVE_ENV_VARS
|
|
387
451
|
// (API keys, CLAUDECODE) before it spawns the agent.
|
|
388
|
-
|
|
452
|
+
// GOV-COMPOSE-SEAM-1 step 0: hand Stratum the SmartMemory coordinates from
|
|
453
|
+
// `.compose/compose.json` so its enforcement events and Compose's ingest
|
|
454
|
+
// events land in one workspace. Resolved from config, not inherited from the
|
|
455
|
+
// ambient shell, and `{}` when the coupling is off — so a workspace without
|
|
456
|
+
// the `smartmemory` block spawns a byte-identical env to before this change.
|
|
457
|
+
// Must be in the SPAWN env: Stratum's policy client reads process.env once,
|
|
458
|
+
// at construction.
|
|
459
|
+
const policyEnv = resolveStratumPolicyEnv(opts.cwd ?? defaults.cwd ?? process.cwd());
|
|
460
|
+
transportOpts.env = { ...(opts.env ?? { ...process.env }), ...policyEnv };
|
|
389
461
|
this.#transport = new StdioClientTransport(transportOpts);
|
|
390
462
|
|
|
391
463
|
this.#client = new Client(
|
|
@@ -395,6 +467,8 @@ export class StratumMcpClient {
|
|
|
395
467
|
|
|
396
468
|
await this.#client.connect(this.#transport);
|
|
397
469
|
this.#connected = true;
|
|
470
|
+
this.#toolNames = null;
|
|
471
|
+
this.#agentFields = null;
|
|
398
472
|
}
|
|
399
473
|
|
|
400
474
|
/** Kill subprocess and clean up. */
|
|
@@ -408,6 +482,28 @@ export class StratumMcpClient {
|
|
|
408
482
|
this.#client = null;
|
|
409
483
|
this.#transport = null;
|
|
410
484
|
this.#connected = false;
|
|
485
|
+
this.#toolNames = null;
|
|
486
|
+
this.#agentFields = null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Feature-detect an MCP tool once per connection. Listing failures mean absent. */
|
|
490
|
+
async hasTool(name) {
|
|
491
|
+
if (this.#toolNames) return this.#toolNames.has(name);
|
|
492
|
+
const client = (process.env.NODE_ENV === 'test' && this._testClient) || this.#client;
|
|
493
|
+
if (!client || (!this.#connected && !(process.env.NODE_ENV === 'test' && this._testClient))) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
const listed = await client.listTools();
|
|
498
|
+
this.#toolNames = new Set(
|
|
499
|
+
(Array.isArray(listed?.tools) ? listed.tools : [])
|
|
500
|
+
.map((tool) => tool?.name)
|
|
501
|
+
.filter((toolName) => typeof toolName === 'string'),
|
|
502
|
+
);
|
|
503
|
+
return this.#toolNames.has(name);
|
|
504
|
+
} catch {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
411
507
|
}
|
|
412
508
|
|
|
413
509
|
/**
|
|
@@ -437,7 +533,19 @@ export class StratumMcpClient {
|
|
|
437
533
|
requestOpts.maxTotalTimeout = 24 * 60 * 60 * 1000; // 24h hard cap
|
|
438
534
|
}
|
|
439
535
|
|
|
440
|
-
const result = await (client ?? this.#client).callTool(callArgs, undefined, requestOpts)
|
|
536
|
+
const result = await (client ?? this.#client).callTool(callArgs, undefined, requestOpts).catch(error => {
|
|
537
|
+
if (error?.data && typeof error.data === 'object') {
|
|
538
|
+
// C11: `error.code` arrives as the JSON-RPC numeric code and callers
|
|
539
|
+
// then overwrite it with the engine's string code. Keep the numeric one
|
|
540
|
+
// reachable instead of destroying it.
|
|
541
|
+
if (error.data.code !== undefined && typeof error.code === 'number') error.rpcCode = error.code;
|
|
542
|
+
for (const key of ['code', 'usage', 'split', 'usdSource', 'stderr', 'telemetry']) {
|
|
543
|
+
if (error.data[key] !== undefined) error[key] = error.data[key];
|
|
544
|
+
}
|
|
545
|
+
if (error.usage) error.usage = failureUsage(error);
|
|
546
|
+
}
|
|
547
|
+
throw error;
|
|
548
|
+
});
|
|
441
549
|
|
|
442
550
|
// TS stratum returns the payload as native MCP structured content and also
|
|
443
551
|
// mirrors it as JSON text. Prefer the native object, retaining text parsing
|
|
@@ -479,11 +587,12 @@ export class StratumMcpClient {
|
|
|
479
587
|
// Check for Stratum error envelope
|
|
480
588
|
if (parsed.status === 'error' || parsed.error) {
|
|
481
589
|
const err = parsed.error ?? parsed;
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
);
|
|
590
|
+
const error = new StratumError(err.code ?? 'STRATUM_ERROR', err.message ?? 'Stratum tool call failed', err.detail ?? '');
|
|
591
|
+
for (const key of ['usage', 'split', 'usdSource', 'stderr', 'telemetry']) {
|
|
592
|
+
if ((parsed[key] ?? err[key]) !== undefined) error[key] = parsed[key] ?? err[key];
|
|
593
|
+
}
|
|
594
|
+
if (error.usage) error.usage = failureUsage(error);
|
|
595
|
+
throw error;
|
|
487
596
|
}
|
|
488
597
|
|
|
489
598
|
return parsed;
|
|
@@ -510,6 +619,15 @@ export class StratumMcpClient {
|
|
|
510
619
|
...(typeof opts.workspaceRoot === 'string' && opts.workspaceRoot.length > 0
|
|
511
620
|
? { workspaceRoot: opts.workspaceRoot }
|
|
512
621
|
: {}),
|
|
622
|
+
// GOV-COMPOSE-SEAM-1: the engine arg is `policy_step_selector`, NOT
|
|
623
|
+
// `step_selector` — the latter is the field inside a bundle rule, and a
|
|
624
|
+
// caller selector can only narrow a rule's own selector (intersection),
|
|
625
|
+
// never widen it. Nothing passes these yet; `consume-bundle` (step 2)
|
|
626
|
+
// is the consumer, and is gated on the canon step landing first.
|
|
627
|
+
...(opts.policyBundle !== undefined ? { policy_bundle: opts.policyBundle } : {}),
|
|
628
|
+
...(typeof opts.policyStepSelector === 'string' && opts.policyStepSelector.length > 0
|
|
629
|
+
? { policy_step_selector: opts.policyStepSelector }
|
|
630
|
+
: {}),
|
|
513
631
|
});
|
|
514
632
|
}
|
|
515
633
|
|
|
@@ -541,6 +659,10 @@ export class StratumMcpClient {
|
|
|
541
659
|
});
|
|
542
660
|
}
|
|
543
661
|
|
|
662
|
+
async usageReport(runId, receipt) {
|
|
663
|
+
return this.#callTool('stratum_usage_report', { runId, receipt });
|
|
664
|
+
}
|
|
665
|
+
|
|
544
666
|
/**
|
|
545
667
|
* Resolve a gate step.
|
|
546
668
|
* @param {string} flowId
|
|
@@ -550,6 +672,25 @@ export class StratumMcpClient {
|
|
|
550
672
|
* @param {'human'|'agent'|'system'} resolvedBy
|
|
551
673
|
* @param {string} [gateToken] - Audit-discovered waiting-gate issuance token to echo
|
|
552
674
|
* @returns {Promise<object>}
|
|
675
|
+
*
|
|
676
|
+
* GOV-COMPOSE-SEAM-1 step 0 — why `rationale` and `resolvedBy` are accepted
|
|
677
|
+
* but deliberately NOT transmitted (they are used by this repo's own callers):
|
|
678
|
+
*
|
|
679
|
+
* - `rationale`: `stratum_gate_resolve` has no rationale parameter and the
|
|
680
|
+
* `gate_resolution` policy event has no field for it (engine.ts
|
|
681
|
+
* buildGateResolutionEvent). Sending it would be dropped on the floor.
|
|
682
|
+
* Carrying gate rationale into the audit chain needs a Stratum contract
|
|
683
|
+
* change; it is not step-0 plumbing.
|
|
684
|
+
* - `resolvedBy` is a ROLE (`human`/`agent`/`system`), not an identity, and
|
|
685
|
+
* Compose has no user identity at this call site at all. The engine's
|
|
686
|
+
* `user_id` populates `resolved_by_user_id`, an audit field that is
|
|
687
|
+
* supposed to name a person. Passing "human" into it would manufacture
|
|
688
|
+
* false provenance, which is worse than leaving it absent. Threading a
|
|
689
|
+
* real identity belongs with roles (Stratum P3 / GOV-ROLES-1).
|
|
690
|
+
*
|
|
691
|
+
* Note also that `gate_resolution` only fires when the run carries a
|
|
692
|
+
* `bundle_id`, i.e. only once a policy bundle is passed at plan time. Until
|
|
693
|
+
* `consume-bundle` lands there is no gate policy event to enrich either way.
|
|
553
694
|
*/
|
|
554
695
|
async gateResolve(flowId, stepId, outcome, rationale, resolvedBy = 'human', gateToken) {
|
|
555
696
|
return this.#callTool('stratum_gate_resolve', {
|
|
@@ -628,12 +769,8 @@ export class StratumMcpClient {
|
|
|
628
769
|
*/
|
|
629
770
|
async agentRun(agentType, prompt, opts = {}) {
|
|
630
771
|
const correlationId = opts.correlationId ?? randomUUID();
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
// undeclared key. The python-era tier accepted {type, allowed_tools, ...};
|
|
634
|
-
// those knobs are resolved compose-side (resolveAgentConfig) and applied at
|
|
635
|
-
// this seam — the engine's connector honors model + sandboxMode. `cwd` is
|
|
636
|
-
// required, so default to the process cwd when the caller omits it.
|
|
772
|
+
// The AbortSignal stays local; a cancellation ID crosses MCP so the caller
|
|
773
|
+
// waits for remote termination, unlike MCP's immediately rejecting abort.
|
|
637
774
|
return this.#dispatchAgentRun(
|
|
638
775
|
agentType,
|
|
639
776
|
prompt,
|
|
@@ -653,22 +790,87 @@ export class StratumMcpClient {
|
|
|
653
790
|
* @returns {Promise<string>}
|
|
654
791
|
*/
|
|
655
792
|
async runAgentText(agentType, prompt, opts = {}) {
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
793
|
+
const toUsageRecords = (value) => {
|
|
794
|
+
const usage = value?.usage;
|
|
795
|
+
if (!usage || typeof usage !== 'object') return [];
|
|
796
|
+
const numeric = [
|
|
797
|
+
usage.tokens, usage.input_tokens, usage.output_tokens,
|
|
798
|
+
usage.cache_read, usage.cache_read_input_tokens,
|
|
799
|
+
usage.cache_creation, usage.cache_creation_input_tokens,
|
|
800
|
+
usage.cost_usd, usage.usd, usage.ms, usage.duration_ms,
|
|
801
|
+
].some((item) => typeof item === 'number' && Number.isFinite(item) && item !== 0);
|
|
802
|
+
if (!numeric) return [];
|
|
803
|
+
const telemetry = value?.telemetry && typeof value.telemetry === 'object'
|
|
804
|
+
? value.telemetry
|
|
805
|
+
: {};
|
|
806
|
+
// STRAT-USAGE-SPLIT: connectors report the true input/output detail
|
|
807
|
+
// BESIDE the Budget-shaped usage (same seam as usdSource). Prefer it —
|
|
808
|
+
// the legacy reconstruction below cannot tell input from output and
|
|
809
|
+
// filed the whole aggregate as output for every record ever written.
|
|
810
|
+
const split = value?.split && typeof value.split === 'object' ? value.split : null;
|
|
811
|
+
const input = split?.input ?? usage.input_tokens ?? 0;
|
|
812
|
+
const output = split?.output
|
|
813
|
+
?? usage.output_tokens
|
|
814
|
+
?? (typeof usage.tokens === 'number' ? Math.max(0, usage.tokens - input) : 0);
|
|
815
|
+
const record = {
|
|
816
|
+
dispatch_id: value?.dispatchId ?? randomUUID(),
|
|
817
|
+
model: telemetry.model ?? usage.model ?? opts.modelID ?? 'unknown',
|
|
818
|
+
...(telemetry.effort ?? opts.effort ? { effort: telemetry.effort ?? opts.effort } : {}),
|
|
819
|
+
duration_ms: telemetry.durationMs ?? usage.duration_ms ?? usage.ms ?? 0,
|
|
820
|
+
input_tokens: input,
|
|
821
|
+
output_tokens: output,
|
|
822
|
+
...(typeof (split?.cacheRead ?? usage.cache_read ?? usage.cache_read_input_tokens) === 'number'
|
|
823
|
+
? { cache_read: split?.cacheRead ?? usage.cache_read ?? usage.cache_read_input_tokens }
|
|
824
|
+
: {}),
|
|
825
|
+
...(typeof (split?.cacheCreation ?? usage.cache_creation ?? usage.cache_creation_input_tokens) === 'number'
|
|
826
|
+
? { cache_creation: split?.cacheCreation ?? usage.cache_creation ?? usage.cache_creation_input_tokens }
|
|
827
|
+
: {}),
|
|
828
|
+
};
|
|
829
|
+
const cost = usage.cost_usd ?? usage.usd;
|
|
830
|
+
// Engine connectors label provenance as `usdSource` (camel, ConnectorUsage);
|
|
831
|
+
// compose-native usage records use `usd_source`. Either is acceptable;
|
|
832
|
+
// an unlabelled usd is still dropped (fail closed).
|
|
833
|
+
// The engine's ledger only admits budget keys inside `usage`, so the
|
|
834
|
+
// connector reports provenance BESIDE it (ConnectorResult.usdSource).
|
|
835
|
+
const rawUsdSource = usage.usd_source ?? value?.usdSource ?? usage.usdSource;
|
|
836
|
+
const usdSource = ['reported', 'estimated'].includes(rawUsdSource)
|
|
837
|
+
? rawUsdSource
|
|
838
|
+
: null;
|
|
839
|
+
if (typeof cost === 'number' && Number.isFinite(cost) && cost > 0 && usdSource) {
|
|
840
|
+
record.cost_usd = cost;
|
|
841
|
+
record.usd_source = usdSource;
|
|
842
|
+
}
|
|
843
|
+
return [record];
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const reportUsage = async (value) => {
|
|
847
|
+
if (typeof opts.onUsage !== 'function') return;
|
|
848
|
+
try {
|
|
849
|
+
await opts.onUsage(toUsageRecords(value));
|
|
850
|
+
} catch (error) {
|
|
851
|
+
console.warn(`[stratum-agent] onUsage hook failed: ${error?.message ?? error}`);
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
let result;
|
|
856
|
+
try {
|
|
857
|
+
result = await this.#dispatchAgentRun(
|
|
858
|
+
agentType,
|
|
859
|
+
prompt,
|
|
860
|
+
opts,
|
|
861
|
+
{ subscribeProgress: false },
|
|
862
|
+
);
|
|
863
|
+
} catch (error) {
|
|
864
|
+
if (error?.usage) await reportUsage(error);
|
|
865
|
+
throw error;
|
|
866
|
+
}
|
|
867
|
+
await reportUsage(result);
|
|
662
868
|
return result?.text ?? '';
|
|
663
869
|
}
|
|
664
870
|
|
|
665
871
|
/**
|
|
666
|
-
* Cancel
|
|
667
|
-
*
|
|
668
|
-
* the python-era {correlation_id} the engine now rejects. Only BACKGROUND runs
|
|
669
|
-
* carry a runId to cancel; a synchronous agent_run returns no handle, so this
|
|
670
|
-
* is a no-op (not_found) there — synchronous controlled executions are
|
|
671
|
-
* interrupted via the local connector's AbortController instead.
|
|
872
|
+
* Cancel a background runId or a foreground cancellationId. A successful
|
|
873
|
+
* acknowledgement means connector cleanup has finished.
|
|
672
874
|
*
|
|
673
875
|
* @param {string} runId
|
|
674
876
|
* @returns {Promise<object>}
|
|
@@ -677,3 +879,36 @@ export class StratumMcpClient {
|
|
|
677
879
|
return this.#callTool('stratum_cancel_agent_run', { runId });
|
|
678
880
|
}
|
|
679
881
|
}
|
|
882
|
+
|
|
883
|
+
function failureUsage(error) {
|
|
884
|
+
const u = error.usage;
|
|
885
|
+
return { ...u,
|
|
886
|
+
...(error.telemetry?.model ? { model: error.telemetry.model } : {}),
|
|
887
|
+
...(error.split ? { input_tokens: error.split.input, output_tokens: error.split.output,
|
|
888
|
+
cache_read_input_tokens: error.split.cacheRead ?? 0, cache_creation_input_tokens: error.split.cacheCreation ?? 0 } : {}),
|
|
889
|
+
...(u.usd !== undefined ? { cost_usd: u.usd } : {}),
|
|
890
|
+
...(u.ms !== undefined ? { duration_ms: u.ms } : {}),
|
|
891
|
+
...(error.usdSource ? { usd_source: error.usdSource } : {}),
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
async function abortable(operation, signal) {
|
|
896
|
+
if (!signal) return operation;
|
|
897
|
+
let abort;
|
|
898
|
+
try {
|
|
899
|
+
return await Promise.race([operation, new Promise((_, reject) => {
|
|
900
|
+
abort = () => reject(signal.reason ?? Object.assign(new Error('Aborted'), { name: 'AbortError' }));
|
|
901
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
902
|
+
if (signal.aborted) abort();
|
|
903
|
+
})]);
|
|
904
|
+
} finally { signal.removeEventListener('abort', abort); }
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
async function cancellationDeadline(operation, ms, label) {
|
|
908
|
+
let timer;
|
|
909
|
+
try {
|
|
910
|
+
return await Promise.race([operation, new Promise((_, reject) => {
|
|
911
|
+
timer = setTimeout(() => reject(new StratumError('CANCELLATION_TEARDOWN_TIMEOUT', `${label} did not settle after ${ms}ms`, '')), ms);
|
|
912
|
+
})]);
|
|
913
|
+
} finally { clearTimeout(timer); }
|
|
914
|
+
}
|
package/lib/test-bootstrap.js
CHANGED
|
@@ -562,3 +562,34 @@ export function deriveTestsPass(summary) {
|
|
|
562
562
|
if (!summary || !summary.parsed) return true;
|
|
563
563
|
return summary.test_count >= 1 && summary.pass_rate === 100;
|
|
564
564
|
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* COMP-COMPLETION-GATE: the attestation the completion gate consumes.
|
|
568
|
+
*
|
|
569
|
+
* This is `deriveTestsPass` with its safety valve removed, as a TRI-state:
|
|
570
|
+
*
|
|
571
|
+
* 'passed' — the output parsed, at least one test ran, all passed
|
|
572
|
+
* 'failed' — the output parsed and something failed (or nothing ran)
|
|
573
|
+
* 'no-signal' — the output could not be read at all
|
|
574
|
+
*
|
|
575
|
+
* The distinction that matters is the third one. `deriveTestsPass` collapses
|
|
576
|
+
* "unreadable" into `true`, so a framework the parser does not understand
|
|
577
|
+
* (`ava`, `tap`, anything unknown) silently attests a passing test run that was
|
|
578
|
+
* never observed. That value then goes onto a permanent completion record and,
|
|
579
|
+
* under the gate, into a tamper-evident ledger — which is exactly the kind of
|
|
580
|
+
* claim the ledger exists to make trustworthy.
|
|
581
|
+
*
|
|
582
|
+
* ABSENCE OF SIGNAL IS NEVER ATTESTATION. The gate refuses 'no-signal' and tells
|
|
583
|
+
* the operator to configure `guard.testCommand`, whose real exit code can attest
|
|
584
|
+
* where the parser cannot.
|
|
585
|
+
*
|
|
586
|
+
* `deriveTestsPass` is retained for the ungated ship-step path and its existing
|
|
587
|
+
* degrade contract; it is not the gate's input.
|
|
588
|
+
*
|
|
589
|
+
* @param {{ test_count: number, pass_rate: number, parsed: boolean }|null} summary
|
|
590
|
+
* @returns {'passed'|'failed'|'no-signal'}
|
|
591
|
+
*/
|
|
592
|
+
export function deriveTestsAttested(summary) {
|
|
593
|
+
if (!summary || !summary.parsed) return 'no-signal';
|
|
594
|
+
return (summary.test_count >= 1 && summary.pass_rate === 100) ? 'passed' : 'failed';
|
|
595
|
+
}
|