@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
|
@@ -19,10 +19,50 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
|
19
19
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
20
20
|
import YAML from 'yaml';
|
|
21
21
|
import { validateBuildStreamEvent } from './build-stream-schema.js';
|
|
22
|
+
import { appendEvent, resolveDispatchLedgerCwd } from './dispatch-ledger.js';
|
|
22
23
|
import { resolveStratumMcpConnection } from './stratum-engine.js';
|
|
24
|
+
import { resolveStratumPolicyEnv } from './smartmemory-config.js';
|
|
23
25
|
|
|
24
26
|
const RUNTIME_INPUT_REF = /^\$\.input\.([A-Za-z_][A-Za-z0-9_]*)$/;
|
|
25
27
|
|
|
28
|
+
function finiteOrNull(value) {
|
|
29
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function nonemptyStringOrNull(value) {
|
|
33
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isBlockedDispatch(value) {
|
|
37
|
+
const candidates = [
|
|
38
|
+
value?.status,
|
|
39
|
+
value?.outcome,
|
|
40
|
+
value?.code,
|
|
41
|
+
value?.error?.status,
|
|
42
|
+
value?.error?.code,
|
|
43
|
+
];
|
|
44
|
+
return candidates.some((candidate) => {
|
|
45
|
+
const normalized = typeof candidate === 'string' ? candidate.toLowerCase() : '';
|
|
46
|
+
return normalized === 'blocked'
|
|
47
|
+
|| normalized === 'budget_exhausted'
|
|
48
|
+
|| normalized === 'budget-exhausted';
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function attachDispatchId(value, dispatchId) {
|
|
53
|
+
if ((typeof value !== 'object' && typeof value !== 'function') || value === null) return;
|
|
54
|
+
try {
|
|
55
|
+
Object.defineProperty(value, 'dispatchId', {
|
|
56
|
+
configurable: true,
|
|
57
|
+
enumerable: false,
|
|
58
|
+
value: dispatchId,
|
|
59
|
+
});
|
|
60
|
+
} catch {
|
|
61
|
+
// A frozen third-party value cannot carry metadata. Dispatch behavior still
|
|
62
|
+
// wins over observability.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
26
66
|
/**
|
|
27
67
|
* Resolve producer-owned authoring values that TS v1 deliberately requires to
|
|
28
68
|
* be literals. Compose knows these values at plan time, so mutate a cloned spec
|
|
@@ -113,12 +153,9 @@ export function resolveStepProfile(profiles, stepId) {
|
|
|
113
153
|
|
|
114
154
|
/**
|
|
115
155
|
* Build the TS `stratum_agent_run` request from an agent string + compose-side
|
|
116
|
-
* options.
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* are NOT sent — they are compose-side concerns applied at the invocation
|
|
120
|
-
* (resolveAgentConfig). `agent` is normalized to the bare provider literal the
|
|
121
|
-
* 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.
|
|
122
159
|
*/
|
|
123
160
|
export function buildAgentRunRequest(agentType, prompt, opts = {}) {
|
|
124
161
|
const provider = String(agentType ?? 'claude').split(':', 1)[0] || 'claude';
|
|
@@ -128,6 +165,11 @@ export function buildAgentRunRequest(agentType, prompt, opts = {}) {
|
|
|
128
165
|
cwd: opts.cwd ?? process.cwd(),
|
|
129
166
|
...(opts.modelID ? { model: opts.modelID } : {}),
|
|
130
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 } : {}),
|
|
131
173
|
};
|
|
132
174
|
}
|
|
133
175
|
|
|
@@ -144,9 +186,150 @@ export class StratumMcpClient {
|
|
|
144
186
|
#client = null;
|
|
145
187
|
#transport = null;
|
|
146
188
|
#connected = false;
|
|
189
|
+
#toolNames = null;
|
|
190
|
+
#agentFields = null;
|
|
147
191
|
// STRAT-PAR-STREAM: subscribers keyed by `${flowId}::${stepId}` → Set<handler>
|
|
148
192
|
#eventSubs = new Map();
|
|
149
193
|
|
|
194
|
+
#recordAgentDispatch(dispatchId, agentType, opts, resultOrError, outcome, elapsedMs) {
|
|
195
|
+
try {
|
|
196
|
+
const context = opts.telemetry && typeof opts.telemetry === 'object'
|
|
197
|
+
? opts.telemetry
|
|
198
|
+
: {};
|
|
199
|
+
const returnedTelemetry = resultOrError?.telemetry
|
|
200
|
+
&& typeof resultOrError.telemetry === 'object'
|
|
201
|
+
? resultOrError.telemetry
|
|
202
|
+
: {};
|
|
203
|
+
const usage = resultOrError?.usage && typeof resultOrError.usage === 'object'
|
|
204
|
+
? resultOrError.usage
|
|
205
|
+
: {};
|
|
206
|
+
const effortIntended = Object.hasOwn(context, 'effort_intended')
|
|
207
|
+
? nonemptyStringOrNull(context.effort_intended)
|
|
208
|
+
: nonemptyStringOrNull(opts.effort);
|
|
209
|
+
const event = {
|
|
210
|
+
kind: 'dispatch',
|
|
211
|
+
dispatch_id: dispatchId,
|
|
212
|
+
site: nonemptyStringOrNull(context.site) ?? 'unattributed',
|
|
213
|
+
agent: String(agentType ?? 'claude').split(':', 1)[0] || 'claude',
|
|
214
|
+
outcome,
|
|
215
|
+
model: nonemptyStringOrNull(returnedTelemetry.model)
|
|
216
|
+
?? nonemptyStringOrNull(usage.model),
|
|
217
|
+
effort_intended: effortIntended,
|
|
218
|
+
effort_executed: nonemptyStringOrNull(returnedTelemetry.effort),
|
|
219
|
+
tokens_in: null,
|
|
220
|
+
tokens_out: null,
|
|
221
|
+
tokens_total: finiteOrNull(usage.tokens),
|
|
222
|
+
usd: finiteOrNull(usage.usd),
|
|
223
|
+
duration_ms: finiteOrNull(usage.ms)
|
|
224
|
+
?? finiteOrNull(returnedTelemetry.durationMs)
|
|
225
|
+
?? finiteOrNull(elapsedMs),
|
|
226
|
+
};
|
|
227
|
+
for (const [field, value] of [
|
|
228
|
+
['build_id', context.build_id],
|
|
229
|
+
['feature_code', context.feature_code],
|
|
230
|
+
['step_id', context.step_id],
|
|
231
|
+
]) {
|
|
232
|
+
if (nonemptyStringOrNull(value) !== null) event[field] = value;
|
|
233
|
+
}
|
|
234
|
+
if (finiteOrNull(context.attempt) !== null) event.attempt = context.attempt;
|
|
235
|
+
appendEvent(resolveDispatchLedgerCwd(context.project_cwd), event);
|
|
236
|
+
} catch {
|
|
237
|
+
// Dispatch capture is fail-open by contract.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async #dispatchAgentRun(agentType, prompt, opts, callOpts) {
|
|
242
|
+
const dispatchId = randomUUID();
|
|
243
|
+
const startedAt = Date.now();
|
|
244
|
+
try {
|
|
245
|
+
const result = await this.#invokeAgentRun(agentType, prompt, opts, callOpts);
|
|
246
|
+
this.#recordAgentDispatch(
|
|
247
|
+
dispatchId,
|
|
248
|
+
agentType,
|
|
249
|
+
opts,
|
|
250
|
+
result,
|
|
251
|
+
isBlockedDispatch(result) ? 'blocked' : 'ok',
|
|
252
|
+
Date.now() - startedAt,
|
|
253
|
+
);
|
|
254
|
+
attachDispatchId(result, dispatchId);
|
|
255
|
+
return result;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
this.#recordAgentDispatch(
|
|
258
|
+
dispatchId,
|
|
259
|
+
agentType,
|
|
260
|
+
opts,
|
|
261
|
+
error,
|
|
262
|
+
isBlockedDispatch(error) ? 'blocked' : 'error',
|
|
263
|
+
Date.now() - startedAt,
|
|
264
|
+
);
|
|
265
|
+
attachDispatchId(error, dispatchId);
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
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
|
+
|
|
150
333
|
/**
|
|
151
334
|
* Subscribe to BuildStreamEvent push notifications scoped to a (flowId, stepId).
|
|
152
335
|
* Handler receives a parsed BuildStreamEvent envelope. Returns an unsubscribe fn.
|
|
@@ -266,7 +449,15 @@ export class StratumMcpClient {
|
|
|
266
449
|
// agent step failed with `403 forbidden / "Request not allowed"`. Pass the
|
|
267
450
|
// full env through; the stratum connector scrubs SENSITIVE_ENV_VARS
|
|
268
451
|
// (API keys, CLAUDECODE) before it spawns the agent.
|
|
269
|
-
|
|
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 };
|
|
270
461
|
this.#transport = new StdioClientTransport(transportOpts);
|
|
271
462
|
|
|
272
463
|
this.#client = new Client(
|
|
@@ -276,6 +467,8 @@ export class StratumMcpClient {
|
|
|
276
467
|
|
|
277
468
|
await this.#client.connect(this.#transport);
|
|
278
469
|
this.#connected = true;
|
|
470
|
+
this.#toolNames = null;
|
|
471
|
+
this.#agentFields = null;
|
|
279
472
|
}
|
|
280
473
|
|
|
281
474
|
/** Kill subprocess and clean up. */
|
|
@@ -289,6 +482,28 @@ export class StratumMcpClient {
|
|
|
289
482
|
this.#client = null;
|
|
290
483
|
this.#transport = null;
|
|
291
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
|
+
}
|
|
292
507
|
}
|
|
293
508
|
|
|
294
509
|
/**
|
|
@@ -318,7 +533,19 @@ export class StratumMcpClient {
|
|
|
318
533
|
requestOpts.maxTotalTimeout = 24 * 60 * 60 * 1000; // 24h hard cap
|
|
319
534
|
}
|
|
320
535
|
|
|
321
|
-
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
|
+
});
|
|
322
549
|
|
|
323
550
|
// TS stratum returns the payload as native MCP structured content and also
|
|
324
551
|
// mirrors it as JSON text. Prefer the native object, retaining text parsing
|
|
@@ -360,11 +587,12 @@ export class StratumMcpClient {
|
|
|
360
587
|
// Check for Stratum error envelope
|
|
361
588
|
if (parsed.status === 'error' || parsed.error) {
|
|
362
589
|
const err = parsed.error ?? parsed;
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
);
|
|
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;
|
|
368
596
|
}
|
|
369
597
|
|
|
370
598
|
return parsed;
|
|
@@ -391,6 +619,15 @@ export class StratumMcpClient {
|
|
|
391
619
|
...(typeof opts.workspaceRoot === 'string' && opts.workspaceRoot.length > 0
|
|
392
620
|
? { workspaceRoot: opts.workspaceRoot }
|
|
393
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
|
+
: {}),
|
|
394
631
|
});
|
|
395
632
|
}
|
|
396
633
|
|
|
@@ -422,6 +659,10 @@ export class StratumMcpClient {
|
|
|
422
659
|
});
|
|
423
660
|
}
|
|
424
661
|
|
|
662
|
+
async usageReport(runId, receipt) {
|
|
663
|
+
return this.#callTool('stratum_usage_report', { runId, receipt });
|
|
664
|
+
}
|
|
665
|
+
|
|
425
666
|
/**
|
|
426
667
|
* Resolve a gate step.
|
|
427
668
|
* @param {string} flowId
|
|
@@ -431,6 +672,25 @@ export class StratumMcpClient {
|
|
|
431
672
|
* @param {'human'|'agent'|'system'} resolvedBy
|
|
432
673
|
* @param {string} [gateToken] - Audit-discovered waiting-gate issuance token to echo
|
|
433
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.
|
|
434
694
|
*/
|
|
435
695
|
async gateResolve(flowId, stepId, outcome, rationale, resolvedBy = 'human', gateToken) {
|
|
436
696
|
return this.#callTool('stratum_gate_resolve', {
|
|
@@ -509,15 +769,14 @@ export class StratumMcpClient {
|
|
|
509
769
|
*/
|
|
510
770
|
async agentRun(agentType, prompt, opts = {}) {
|
|
511
771
|
const correlationId = opts.correlationId ?? randomUUID();
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
{ subscribeProgress: true, correlationId });
|
|
772
|
+
// The AbortSignal stays local; a cancellation ID crosses MCP so the caller
|
|
773
|
+
// waits for remote termination, unlike MCP's immediately rejecting abort.
|
|
774
|
+
return this.#dispatchAgentRun(
|
|
775
|
+
agentType,
|
|
776
|
+
prompt,
|
|
777
|
+
opts,
|
|
778
|
+
{ subscribeProgress: true, correlationId },
|
|
779
|
+
);
|
|
521
780
|
}
|
|
522
781
|
|
|
523
782
|
/**
|
|
@@ -531,19 +790,87 @@ export class StratumMcpClient {
|
|
|
531
790
|
* @returns {Promise<string>}
|
|
532
791
|
*/
|
|
533
792
|
async runAgentText(agentType, prompt, opts = {}) {
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
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);
|
|
537
868
|
return result?.text ?? '';
|
|
538
869
|
}
|
|
539
870
|
|
|
540
871
|
/**
|
|
541
|
-
* Cancel
|
|
542
|
-
*
|
|
543
|
-
* the python-era {correlation_id} the engine now rejects. Only BACKGROUND runs
|
|
544
|
-
* carry a runId to cancel; a synchronous agent_run returns no handle, so this
|
|
545
|
-
* is a no-op (not_found) there — synchronous controlled executions are
|
|
546
|
-
* 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.
|
|
547
874
|
*
|
|
548
875
|
* @param {string} runId
|
|
549
876
|
* @returns {Promise<object>}
|
|
@@ -552,3 +879,36 @@ export class StratumMcpClient {
|
|
|
552
879
|
return this.#callTool('stratum_cancel_agent_run', { runId });
|
|
553
880
|
}
|
|
554
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
|
+
}
|