@smartmemory/compose 0.3.5-beta → 0.3.7
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/bin/compose.js +288 -0
- package/bin/git-hooks/pre-push.template +29 -0
- package/bin/judgment-import.js +572 -0
- package/contracts/feature-json.schema.json +5 -0
- package/contracts/judgment-record.schema.json +821 -0
- package/dist/assets/App-PkZzHeMj.js +894 -0
- package/dist/assets/_baseUniq-Bo837sRJ.js +1 -0
- package/dist/assets/arc-BafGpyqE.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-BOBfUsqL.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-Dwodev1a.js +132 -0
- package/dist/assets/{browser-BSM23If2.js → browser-1ntj1-x_.js} +6 -6
- package/dist/assets/{c4Diagram-LMCZKHZV-CUmTCrMW.js → c4Diagram-AHTNJAMY-CU_bhYag.js} +1 -1
- package/dist/assets/channel-qVK_qn4E.js +1 -0
- package/dist/assets/{chunk-JWPE2WC7-DQFs7gjy.js → chunk-4BX2VUAB-p8WsDwnO.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-B8h7-eR0.js +206 -0
- package/dist/assets/{chunk-XXDRQBXY-CSdt06Hp.js → chunk-55IACEB6-DxeEr98s.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Dvhgnm7E.js → chunk-EDXVE4YY-BYt8F151.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-Qw-L7OoJ.js → chunk-FMBD7UC4-DGSOVeie.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-B-QdgYR2.js +231 -0
- package/dist/assets/{chunk-2Q5K7J3B-Cyvi1AFH.js → chunk-QZHKN3VN-Du5UAZLs.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-B1y5FpOd.js → chunk-YZCP3GAM-C8JbNBSk.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +1 -0
- package/dist/assets/clone-Pu3RyLUh.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-G8l05tXy.js → cose-bilkent-S5V4N54A-O1ESaqge.js} +1 -1
- package/dist/assets/dagre-KV5264BT-CPTmFPHw.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-B3PNrWs5.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-Cscfr6vc.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-CSfqZ-TM.js +43 -0
- package/dist/assets/diagram-TYMM5635-Cg4aYS7W.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-_ZqwG5pl.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-C83boxFT.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-CWnIjuEi.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-DrMdxZfH.js +106 -0
- package/dist/assets/graph-Bi99_6Yf.js +331 -0
- package/dist/assets/graph-RE4I7Ty7.js +1 -0
- package/dist/assets/index-Rm2RE-c0.js +123 -0
- package/dist/assets/infoDiagram-42DDH7IO-BLmP4Epr.js +2 -0
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-Hm4zSDlM.js → ishikawaDiagram-UXIWVN3A-yuWWshKN.js} +5 -5
- package/dist/assets/{journeyDiagram-5HDEW3XC-CIqcTFuO.js → journeyDiagram-VCZTEJTY-BOfhaJov.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-CB9jCXj_.js → kanban-definition-6JOO6SKY-Bbolde15.js} +7 -7
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-BSf33zm8.js +1 -0
- package/dist/assets/{linear-CsXqv70E.js → linear-AvSTWMqx.js} +1 -1
- package/dist/assets/min-QBM8H4xN.js +1 -0
- package/dist/assets/{mindmap-definition-LN4V7U3C-CpqH5F_P.js → mindmap-definition-QFDTVHPH-BuvgtqIc.js} +7 -7
- package/dist/assets/{mobile-CaoXUwAr.js → mobile-BnXEOE3U.js} +2 -2
- package/dist/assets/pieDiagram-DEJITSTG-DIzF16vh.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-D-mbUIjS.js +7 -0
- package/dist/assets/{requirementDiagram-TGXJPOKE-D1lG1P7h.js → requirementDiagram-MS252O5E-CEs4kCLd.js} +3 -3
- package/dist/assets/sankeyDiagram-XADWPNL6-DFsnCr9n.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-BEJYdTjQ.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-BBXs57uY.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +1 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-CzqqUDvt.js → timeline-definition-GMOUNBTQ-BGvLoVAY.js} +3 -3
- package/dist/assets/vennDiagram-DHZGUBPP-9LaBTMe0.js +34 -0
- package/dist/assets/wardley-RL74JXVD-P4MEqMTP.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-o-tmxnlC.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-Dpn7V6qk.js +7 -0
- package/dist/index.html +2 -2
- package/lib/bug-escalation.js +30 -4
- package/lib/build.js +777 -52
- package/lib/canon-guard.js +223 -0
- package/lib/canon-registry.js +187 -0
- package/lib/codex-preflight.js +26 -4
- package/lib/dispatch-ledger.js +301 -0
- package/lib/dispatch-metrics.js +236 -0
- package/lib/experiment-judge.js +6 -1
- package/lib/feature-writer.js +9 -0
- package/lib/gsd.js +11 -2
- package/lib/hooks-status.js +32 -3
- package/lib/idempotency.js +33 -8
- package/lib/judgment/schema.js +23 -0
- package/lib/judgment/store/index.js +207 -0
- package/lib/judgment/store/records.js +405 -0
- package/lib/judgment-attest.js +259 -0
- package/lib/judgment-gen.js +691 -0
- package/lib/judgment-verify.js +153 -0
- package/lib/judgment-write-guard.js +230 -0
- package/lib/judgment-writer.js +3403 -0
- package/lib/lane-gate.js +2 -0
- package/lib/lifecycle-modes.js +42 -0
- package/lib/local-claude-connector.js +199 -54
- package/lib/mcp-enforcement.js +21 -35
- package/lib/result-normalizer.js +93 -15
- package/lib/review-normalize.js +4 -0
- package/lib/stratum-mcp-client.js +131 -6
- package/package.json +2 -2
- package/server/compose-mcp-tools.js +49 -1
- package/server/compose-mcp.js +220 -0
- package/server/mcp-tool-policy.js +1 -0
- package/dist/assets/App-BG92y_CY.js +0 -896
- package/dist/assets/abnfDiagram-VRR7QNED-Bip6cMjP.js +0 -1
- package/dist/assets/arc-CyMTM07c.js +0 -1
- package/dist/assets/architectureDiagram-ZJ3FMSHR-BukV0CJw.js +0 -36
- package/dist/assets/blockDiagram-677ZJIJ3-Bjb_MNB4.js +0 -132
- package/dist/assets/channel-BzkmtaLy.js +0 -1
- package/dist/assets/chunk-EX3LRPZG-G_MZEji_.js +0 -231
- package/dist/assets/chunk-MOJQB5TN-CQZQYMcz.js +0 -88
- package/dist/assets/chunk-RYQCIY6F-CmAda4sZ.js +0 -1
- package/dist/assets/chunk-V7JOEXUC-CbjBz-Ek.js +0 -206
- package/dist/assets/classDiagram-OUVF2IWQ-BIvLcbex.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-BIvLcbex.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-C3n1PNJC.js +0 -178
- package/dist/assets/cynefinDiagram-TSTJHNR4-C8N_CvKJ.js +0 -62
- package/dist/assets/dagre-VKFMJZFB-B2pkwDhN.js +0 -4
- package/dist/assets/diagram-FQU43EPY-DPHD_t-1.js +0 -3
- package/dist/assets/diagram-G47NLZAW-DGmHIZRr.js +0 -24
- package/dist/assets/diagram-NH7WQ7WH-BlLWwKVt.js +0 -24
- package/dist/assets/diagram-OA4YK3LP-DEv704Re.js +0 -30
- package/dist/assets/diagram-WEI45ONY-Bj6PGUCe.js +0 -41
- package/dist/assets/ebnfDiagram-CCIWWBDH-Ch-icksT.js +0 -1
- package/dist/assets/erDiagram-Q63AITRT-CI3lwEFA.js +0 -85
- package/dist/assets/flowDiagram-23GEKE2U-DcUANpAz.js +0 -156
- package/dist/assets/ganttDiagram-NO4QXBWP-Cy0Ie8KS.js +0 -292
- package/dist/assets/gitGraphDiagram-IHSO6WYX-BswI52az.js +0 -106
- package/dist/assets/graph-BXPQrYYB.js +0 -331
- package/dist/assets/graph-C9eacEi8.js +0 -1
- package/dist/assets/index-B1H7-G2y.js +0 -119
- package/dist/assets/infoDiagram-FWYZ7A6U-BBunqvr-.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/pegDiagram-2B236MQR-HpQyrDcm.js +0 -1
- package/dist/assets/pieDiagram-ENE6RG2P-xkcpHwnY.js +0 -39
- package/dist/assets/quadrantDiagram-ABIIQ3AL-kebZIOmu.js +0 -7
- package/dist/assets/railroadDiagram-RFXS5EU6-DNvGwWm9.js +0 -1
- package/dist/assets/sankeyDiagram-HTMAVEWB-DOEEcn52.js +0 -40
- package/dist/assets/sequenceDiagram-DBY2YBRQ-DynIVCk_.js +0 -162
- package/dist/assets/sizeCapture-X5ZJPWSS-DJXFjUUL.js +0 -1
- package/dist/assets/stateDiagram-2N3HPSRC-OZ7trFh5.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-GcPRHAJT.js +0 -1
- package/dist/assets/swimlanes-5IMT3BWC-DULYAFmB.js +0 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-BB0gCy01.js +0 -8
- package/dist/assets/vennDiagram-L72KCM5P-CdKmtcFL.js +0 -34
- package/dist/assets/wardleyDiagram-EHGQE667-DknGwjCA.js +0 -78
- package/dist/assets/xychartDiagram-FW5EYKEG-DdYyKfQ3.js +0 -7
package/lib/lane-gate.js
CHANGED
|
@@ -87,6 +87,7 @@ export async function applyFrontTriage({ featureCode, request, provider, cachedF
|
|
|
87
87
|
triageTier: tier,
|
|
88
88
|
lane,
|
|
89
89
|
estimateSource,
|
|
90
|
+
triageConfidence: front.confidence,
|
|
90
91
|
profile: buildProfile,
|
|
91
92
|
triageTimestamp: new Date().toISOString(),
|
|
92
93
|
};
|
|
@@ -109,6 +110,7 @@ export async function applyFrontTriage({ featureCode, request, provider, cachedF
|
|
|
109
110
|
buildProfile,
|
|
110
111
|
tier,
|
|
111
112
|
lane,
|
|
113
|
+
confidence: front.confidence,
|
|
112
114
|
tierLabel: fields.complexity,
|
|
113
115
|
rationale: front.rationale,
|
|
114
116
|
cachedFeature: updated,
|
package/lib/lifecycle-modes.js
CHANGED
|
@@ -136,6 +136,48 @@ export const LIFECYCLE_MODES = {
|
|
|
136
136
|
defaultTemplate: 'plan',
|
|
137
137
|
},
|
|
138
138
|
},
|
|
139
|
+
|
|
140
|
+
// -------------------------------------------------------------------------
|
|
141
|
+
// JUDGMENT — the joint state machine (COMP-JUDGMENT-WRITER design Decision 2).
|
|
142
|
+
// Consumed ONLY by lib/judgment-writer.js through guardedTransition(mode:
|
|
143
|
+
// 'judgment'), which makes the Stratum guard the single lifecycle authority
|
|
144
|
+
// for joint transitions where capabilities.guard is on. The per-edge required
|
|
145
|
+
// ARTIFACTS (resolution packages, dissolution, reopen, re-dispose) are
|
|
146
|
+
// enforced by lib/judgment-write-guard.js before the guard is called —
|
|
147
|
+
// edgeEvidence stays empty because the evidence lives inside records, not in
|
|
148
|
+
// per-feature files. Never run by any CLI runner; the runner block is inert.
|
|
149
|
+
//
|
|
150
|
+
// KNOWN SUPERSET: buildPhaseGraph() unconditionally adds `resolved →
|
|
151
|
+
// complete` and `<non-terminal> → killed` edges to every registered graph
|
|
152
|
+
// (an adapter invariant this feature may not touch). Those five extra edges
|
|
153
|
+
// are UNREACHABLE through the judgment writer — the write-guard's edge→
|
|
154
|
+
// artifact table refuses them before any guard call — and the parity test
|
|
155
|
+
// pins the superset to exactly that auto-added set. Closing them at the
|
|
156
|
+
// guard itself is epic S4–S6 enforcement territory.
|
|
157
|
+
// -------------------------------------------------------------------------
|
|
158
|
+
judgment: {
|
|
159
|
+
transitions: {
|
|
160
|
+
open: ['under_test', 'superseded', 'dissolved'],
|
|
161
|
+
under_test: ['resolved', 'inconclusive', 'open', 'superseded', 'dissolved'],
|
|
162
|
+
resolved: ['open'],
|
|
163
|
+
inconclusive: ['under_test', 'open'],
|
|
164
|
+
},
|
|
165
|
+
skippable: [],
|
|
166
|
+
terminal: ['superseded', 'dissolved', 'complete', 'killed'],
|
|
167
|
+
genesis: 'open',
|
|
168
|
+
completablePhase: 'resolved',
|
|
169
|
+
phaseArtifacts: [],
|
|
170
|
+
edgeEvidence: {},
|
|
171
|
+
phaseOrder: ['open', 'under_test', 'resolved', 'inconclusive'],
|
|
172
|
+
runner: {
|
|
173
|
+
artifactRoot: 'docs/judgment/records/joints',
|
|
174
|
+
runsTriage: false,
|
|
175
|
+
tracksFeatureJson: false,
|
|
176
|
+
descriptionLoader: 'plan',
|
|
177
|
+
planInputs: 'plan',
|
|
178
|
+
defaultTemplate: 'plan',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
139
181
|
};
|
|
140
182
|
|
|
141
183
|
/**
|
|
@@ -17,8 +17,11 @@
|
|
|
17
17
|
* and reports usage. The SDK `query` is injectable for tests.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { randomUUID } from 'node:crypto';
|
|
20
21
|
import { query as sdkQuery } from '@anthropic-ai/claude-agent-sdk';
|
|
21
22
|
|
|
23
|
+
import { appendEvent, resolveDispatchLedgerCwd } from './dispatch-ledger.js';
|
|
24
|
+
|
|
22
25
|
const SENSITIVE_ENV_VARS = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'CLAUDE_API_KEY', 'CLAUDECODE'];
|
|
23
26
|
|
|
24
27
|
function isRecord(value) {
|
|
@@ -29,6 +32,77 @@ function nonneg(value) {
|
|
|
29
32
|
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
function reportedNumber(value) {
|
|
36
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function reportedString(value) {
|
|
40
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isBlockedDispatch(value) {
|
|
44
|
+
const candidates = [value?.status, value?.outcome, value?.code, value?.subtype];
|
|
45
|
+
return candidates.some((candidate) => {
|
|
46
|
+
const normalized = typeof candidate === 'string' ? candidate.toLowerCase() : '';
|
|
47
|
+
return normalized === 'blocked'
|
|
48
|
+
|| normalized === 'budget_exhausted'
|
|
49
|
+
|| normalized === 'budget-exhausted';
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function attachDispatchId(value, dispatchId) {
|
|
54
|
+
if ((typeof value !== 'object' && typeof value !== 'function') || value === null) return;
|
|
55
|
+
try {
|
|
56
|
+
Object.defineProperty(value, 'dispatchId', {
|
|
57
|
+
configurable: true,
|
|
58
|
+
enumerable: false,
|
|
59
|
+
value: dispatchId,
|
|
60
|
+
});
|
|
61
|
+
} catch {
|
|
62
|
+
// Capture metadata must never replace the SDK's original behavior.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function recordDispatch(dispatchId, context, capture, outcome, elapsedMs, effortExecuted) {
|
|
67
|
+
try {
|
|
68
|
+
const event = {
|
|
69
|
+
kind: 'dispatch',
|
|
70
|
+
dispatch_id: dispatchId,
|
|
71
|
+
site: reportedString(context.site) ?? 'unattributed',
|
|
72
|
+
agent: 'claude',
|
|
73
|
+
outcome,
|
|
74
|
+
model: reportedString(capture.model),
|
|
75
|
+
effort_intended: reportedString(context.effort_intended),
|
|
76
|
+
// Executed effort is only recorded once a model run is confirmed — the SDK
|
|
77
|
+
// emitted a model identity (init or result). A transport/startup failure
|
|
78
|
+
// before any model spoke leaves effort_executed null, so the executed-effort
|
|
79
|
+
// curve is not polluted by tiers that were requested but never actually ran
|
|
80
|
+
// (mirrors the codex route, whose effort_executed comes from run telemetry).
|
|
81
|
+
effort_executed: capture.model ? reportedString(effortExecuted) : null,
|
|
82
|
+
tokens_in: reportedNumber(capture.inputTokens),
|
|
83
|
+
tokens_out: reportedNumber(capture.outputTokens),
|
|
84
|
+
tokens_total: capture.inputTokens !== null || capture.outputTokens !== null
|
|
85
|
+
? (reportedNumber(capture.inputTokens) ?? 0) + (reportedNumber(capture.outputTokens) ?? 0)
|
|
86
|
+
: null,
|
|
87
|
+
usd: reportedNumber(capture.costUsd),
|
|
88
|
+
duration_ms: reportedNumber(capture.durationMs) ?? reportedNumber(elapsedMs),
|
|
89
|
+
};
|
|
90
|
+
for (const [field, value] of [
|
|
91
|
+
['build_id', context.build_id],
|
|
92
|
+
['feature_code', context.feature_code],
|
|
93
|
+
['step_id', context.step_id],
|
|
94
|
+
]) {
|
|
95
|
+
if (reportedString(value) !== null) event[field] = value;
|
|
96
|
+
}
|
|
97
|
+
if (typeof context.attempt === 'number' && Number.isFinite(context.attempt)) {
|
|
98
|
+
event.attempt = context.attempt;
|
|
99
|
+
}
|
|
100
|
+
appendEvent(resolveDispatchLedgerCwd(context.project_cwd), event);
|
|
101
|
+
} catch {
|
|
102
|
+
// Dispatch capture is fail-open by contract.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
32
106
|
/**
|
|
33
107
|
* Run a controlled claude agent locally.
|
|
34
108
|
*
|
|
@@ -39,17 +113,26 @@ function nonneg(value) {
|
|
|
39
113
|
* @param {string[]} [opts.allowedTools] enforced tool allowlist (read-only review)
|
|
40
114
|
* @param {string[]} [opts.disallowedTools]
|
|
41
115
|
* @param {object} [opts.thinking]
|
|
116
|
+
* @param {string} [opts.effort] reasoning-effort tier (low|medium|high|xhigh|max)
|
|
42
117
|
* @param {AbortController} [opts.abortController] abort → interrupt the run
|
|
43
118
|
* @param {(ev:{tool:string,input:object})=>void} [opts.onToolUse] per tool_use block
|
|
44
119
|
* @param {NodeJS.ProcessEnv} [opts.env]
|
|
45
120
|
* @param {Function} [opts.query] SDK `query` seam for tests
|
|
121
|
+
* @param {object} [opts.telemetry] Compose-only dispatch context
|
|
46
122
|
* @returns {Promise<{text:string, usage:object, telemetry:object}>}
|
|
47
123
|
*/
|
|
48
124
|
export async function runLocalClaudeAgent(prompt, opts = {}) {
|
|
49
125
|
const query = opts.query ?? sdkQuery;
|
|
126
|
+
const { telemetry } = opts;
|
|
127
|
+
const telemetryContext = telemetry && typeof telemetry === 'object'
|
|
128
|
+
? telemetry
|
|
129
|
+
: {};
|
|
50
130
|
const env = { ...(opts.env ?? process.env) };
|
|
51
131
|
for (const key of SENSITIVE_ENV_VARS) delete env[key];
|
|
52
132
|
|
|
133
|
+
// Only forward a non-empty effort so routes without a configured tier keep the
|
|
134
|
+
// SDK's default behavior; the applied value is what we record as effort_executed.
|
|
135
|
+
const appliedEffort = reportedString(opts.effort);
|
|
53
136
|
const sdkOptions = {
|
|
54
137
|
cwd: opts.cwd ?? process.cwd(),
|
|
55
138
|
model: opts.model ?? process.env.CLAUDE_MODEL ?? 'claude-sonnet-4-6',
|
|
@@ -57,6 +140,7 @@ export async function runLocalClaudeAgent(prompt, opts = {}) {
|
|
|
57
140
|
env,
|
|
58
141
|
...(opts.abortController ? { abortController: opts.abortController } : {}),
|
|
59
142
|
...(opts.thinking !== undefined ? { thinking: opts.thinking } : {}),
|
|
143
|
+
...(appliedEffort !== null ? { effort: appliedEffort } : {}),
|
|
60
144
|
};
|
|
61
145
|
// A read-only profile passes an explicit allowlist; without one, the agent
|
|
62
146
|
// gets the full claude_code preset (unrestricted).
|
|
@@ -83,67 +167,128 @@ export async function runLocalClaudeAgent(prompt, opts = {}) {
|
|
|
83
167
|
let inputTokens = 0;
|
|
84
168
|
let outputTokens = 0;
|
|
85
169
|
let costUsd = 0;
|
|
170
|
+
const capture = {
|
|
171
|
+
model: null,
|
|
172
|
+
inputTokens: null,
|
|
173
|
+
outputTokens: null,
|
|
174
|
+
costUsd: null,
|
|
175
|
+
durationMs: null,
|
|
176
|
+
};
|
|
177
|
+
let resultError = null;
|
|
178
|
+
let resultFailureOutcome = null;
|
|
179
|
+
const dispatchId = randomUUID();
|
|
180
|
+
const dispatchStartedAt = Date.now();
|
|
86
181
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
182
|
+
try {
|
|
183
|
+
for await (const raw of query({ prompt, options: sdkOptions })) {
|
|
184
|
+
if (!isRecord(raw)) continue;
|
|
185
|
+
if (raw.type === 'system' && raw.subtype === 'init' && typeof raw.model === 'string') {
|
|
186
|
+
resolvedModel = raw.model;
|
|
187
|
+
capture.model = raw.model;
|
|
188
|
+
}
|
|
189
|
+
if (raw.type === 'assistant' && isRecord(raw.message) && Array.isArray(raw.message.content)) {
|
|
190
|
+
for (const block of raw.message.content) {
|
|
191
|
+
if (!isRecord(block)) continue;
|
|
192
|
+
if (block.type === 'text' && typeof block.text === 'string') assistantText += block.text;
|
|
193
|
+
if (block.type === 'tool_use' && typeof block.name === 'string' && typeof opts.onToolUse === 'function') {
|
|
194
|
+
opts.onToolUse({ tool: block.name, input: isRecord(block.input) ? block.input : {} });
|
|
195
|
+
}
|
|
98
196
|
}
|
|
99
197
|
}
|
|
198
|
+
if (raw.type !== 'result') continue;
|
|
199
|
+
durationMs = nonneg(raw.duration_ms);
|
|
200
|
+
capture.durationMs = reportedNumber(raw.duration_ms);
|
|
201
|
+
capture.costUsd = reportedNumber(raw.total_cost_usd);
|
|
202
|
+
if (reportedString(raw.model) !== null) capture.model = raw.model;
|
|
203
|
+
if (isRecord(raw.usage)) {
|
|
204
|
+
capture.inputTokens = reportedNumber(raw.usage.input_tokens);
|
|
205
|
+
capture.outputTokens = reportedNumber(raw.usage.output_tokens);
|
|
206
|
+
capture.model = reportedString(raw.usage.model) ?? capture.model;
|
|
207
|
+
}
|
|
208
|
+
if (raw.subtype !== 'success') {
|
|
209
|
+
// F3: a failed run still consumed billable tokens/cost. Capture them from
|
|
210
|
+
// the error result (SDKResultError carries usage + total_cost_usd) and
|
|
211
|
+
// attach to the thrown Error — same usage shape as the success return — so
|
|
212
|
+
// the consumer failure path can debit the engine/GSD ledgers. Without this,
|
|
213
|
+
// repeated failures evade budget exhaustion.
|
|
214
|
+
const failCost = nonneg(raw.total_cost_usd);
|
|
215
|
+
const failIn = isRecord(raw.usage) ? nonneg(raw.usage.input_tokens) : 0;
|
|
216
|
+
const failOut = isRecord(raw.usage) ? nonneg(raw.usage.output_tokens) : 0;
|
|
217
|
+
const errors = Array.isArray(raw.errors) ? raw.errors.filter((v) => typeof v === 'string') : [];
|
|
218
|
+
const err = new Error(errors.join('; ') || `claude query failed: ${String(raw.subtype)}`);
|
|
219
|
+
err.usage = {
|
|
220
|
+
input_tokens: failIn,
|
|
221
|
+
output_tokens: failOut,
|
|
222
|
+
tokens: failIn + failOut,
|
|
223
|
+
cost_usd: failCost,
|
|
224
|
+
usd: failCost,
|
|
225
|
+
duration_ms: durationMs,
|
|
226
|
+
ms: durationMs,
|
|
227
|
+
model: resolvedModel,
|
|
228
|
+
};
|
|
229
|
+
err.costUsd = failCost;
|
|
230
|
+
resultError = err;
|
|
231
|
+
resultFailureOutcome = isBlockedDispatch(raw) ? 'blocked' : 'error';
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
if (typeof raw.result === 'string') finalText = raw.result;
|
|
235
|
+
costUsd = nonneg(raw.total_cost_usd);
|
|
236
|
+
if (isRecord(raw.usage)) {
|
|
237
|
+
inputTokens = nonneg(raw.usage.input_tokens);
|
|
238
|
+
outputTokens = nonneg(raw.usage.output_tokens);
|
|
239
|
+
}
|
|
100
240
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const failIn = isRecord(raw.usage) ? nonneg(raw.usage.input_tokens) : 0;
|
|
111
|
-
const failOut = isRecord(raw.usage) ? nonneg(raw.usage.output_tokens) : 0;
|
|
112
|
-
const errors = Array.isArray(raw.errors) ? raw.errors.filter((v) => typeof v === 'string') : [];
|
|
113
|
-
const err = new Error(errors.join('; ') || `claude query failed: ${String(raw.subtype)}`);
|
|
114
|
-
err.usage = {
|
|
115
|
-
input_tokens: failIn,
|
|
116
|
-
output_tokens: failOut,
|
|
117
|
-
tokens: failIn + failOut,
|
|
118
|
-
cost_usd: failCost,
|
|
119
|
-
usd: failCost,
|
|
241
|
+
|
|
242
|
+
const result = {
|
|
243
|
+
text: finalText ?? assistantText,
|
|
244
|
+
usage: {
|
|
245
|
+
input_tokens: inputTokens,
|
|
246
|
+
output_tokens: outputTokens,
|
|
247
|
+
tokens: inputTokens + outputTokens,
|
|
248
|
+
cost_usd: costUsd,
|
|
249
|
+
usd: costUsd,
|
|
120
250
|
duration_ms: durationMs,
|
|
121
251
|
ms: durationMs,
|
|
122
252
|
model: resolvedModel,
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
253
|
+
},
|
|
254
|
+
telemetry: { durationMs, model: resolvedModel },
|
|
255
|
+
};
|
|
256
|
+
recordDispatch(
|
|
257
|
+
dispatchId,
|
|
258
|
+
telemetryContext,
|
|
259
|
+
capture,
|
|
260
|
+
'ok',
|
|
261
|
+
Date.now() - dispatchStartedAt,
|
|
262
|
+
appliedEffort,
|
|
263
|
+
);
|
|
264
|
+
attachDispatchId(result, dispatchId);
|
|
265
|
+
return result;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (error !== resultError && error && typeof error === 'object') {
|
|
268
|
+
const errorUsage = isRecord(error.usage) ? error.usage : {};
|
|
269
|
+
const errorTelemetry = isRecord(error.telemetry) ? error.telemetry : {};
|
|
270
|
+
capture.model = reportedString(errorTelemetry.model)
|
|
271
|
+
?? reportedString(errorUsage.model)
|
|
272
|
+
?? capture.model;
|
|
273
|
+
capture.inputTokens = reportedNumber(errorUsage.input_tokens) ?? capture.inputTokens;
|
|
274
|
+
capture.outputTokens = reportedNumber(errorUsage.output_tokens) ?? capture.outputTokens;
|
|
275
|
+
capture.costUsd = reportedNumber(errorUsage.cost_usd)
|
|
276
|
+
?? reportedNumber(errorUsage.usd)
|
|
277
|
+
?? capture.costUsd;
|
|
278
|
+
capture.durationMs = reportedNumber(errorUsage.duration_ms)
|
|
279
|
+
?? reportedNumber(errorUsage.ms)
|
|
280
|
+
?? reportedNumber(errorTelemetry.durationMs)
|
|
281
|
+
?? capture.durationMs;
|
|
132
282
|
}
|
|
283
|
+
recordDispatch(
|
|
284
|
+
dispatchId,
|
|
285
|
+
telemetryContext,
|
|
286
|
+
capture,
|
|
287
|
+
resultFailureOutcome ?? (isBlockedDispatch(error) ? 'blocked' : 'error'),
|
|
288
|
+
Date.now() - dispatchStartedAt,
|
|
289
|
+
appliedEffort,
|
|
290
|
+
);
|
|
291
|
+
attachDispatchId(error, dispatchId);
|
|
292
|
+
throw error;
|
|
133
293
|
}
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
text: finalText ?? assistantText,
|
|
137
|
-
usage: {
|
|
138
|
-
input_tokens: inputTokens,
|
|
139
|
-
output_tokens: outputTokens,
|
|
140
|
-
tokens: inputTokens + outputTokens,
|
|
141
|
-
cost_usd: costUsd,
|
|
142
|
-
usd: costUsd,
|
|
143
|
-
duration_ms: durationMs,
|
|
144
|
-
ms: durationMs,
|
|
145
|
-
model: resolvedModel,
|
|
146
|
-
},
|
|
147
|
-
telemetry: { durationMs, model: resolvedModel },
|
|
148
|
-
};
|
|
149
294
|
}
|
package/lib/mcp-enforcement.js
CHANGED
|
@@ -11,19 +11,20 @@
|
|
|
11
11
|
|
|
12
12
|
import { existsSync, readFileSync } from 'fs';
|
|
13
13
|
import { join } from 'path';
|
|
14
|
+
import {
|
|
15
|
+
isGuarded as registryIsGuarded,
|
|
16
|
+
toolsForPath as registryToolsForPath,
|
|
17
|
+
featureCodeForPath as registryFeatureCodeForPath,
|
|
18
|
+
} from './canon-registry.js';
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
'link_features',
|
|
24
|
-
'record_completion',
|
|
25
|
-
'propose_followup',
|
|
26
|
-
];
|
|
20
|
+
// COMP-CANON-GUARD S1: the guarded-path/tool declarations that used to live here
|
|
21
|
+
// as literal sets now live in lib/canon-registry.js — the single source of truth
|
|
22
|
+
// shared with the write-time hook. This module is the 'ship' enforcement point;
|
|
23
|
+
// it consumes ONLY the registry's ship subset, so its behavior is unchanged
|
|
24
|
+
// (docs/judgment/** is a hook-only path and is invisible here, exactly as before
|
|
25
|
+
// this refactor, when it was not declared at all). The contract test
|
|
26
|
+
// (test/canon-registry-contract.test.js) pins that equivalence.
|
|
27
|
+
const ENFORCEMENT_POINT = 'ship';
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Read `enforcement.mcpForFeatureMgmt` and normalize to 'block' | 'log' | 'off'.
|
|
@@ -63,11 +64,7 @@ export function filterGuarded(dirtyFiles, featuresDir) {
|
|
|
63
64
|
*/
|
|
64
65
|
export function isGuardedPath(path, featuresDir) {
|
|
65
66
|
if (typeof path !== 'string') return false;
|
|
66
|
-
|
|
67
|
-
// <featuresDir>/<CODE>/feature.json
|
|
68
|
-
const prefix = featuresDir.replace(/\/$/, '') + '/';
|
|
69
|
-
if (!path.startsWith(prefix)) return false;
|
|
70
|
-
return path.endsWith('/feature.json');
|
|
67
|
+
return registryIsGuarded(path, { featuresDir, point: ENFORCEMENT_POINT });
|
|
71
68
|
}
|
|
72
69
|
|
|
73
70
|
/**
|
|
@@ -80,13 +77,7 @@ export function isGuardedPath(path, featuresDir) {
|
|
|
80
77
|
* @returns {string[]}
|
|
81
78
|
*/
|
|
82
79
|
export function expectedToolsForPath(path, featuresDir) {
|
|
83
|
-
|
|
84
|
-
if (path === 'CHANGELOG.md') return [...TOOLS_FOR_CHANGELOG];
|
|
85
|
-
const prefix = featuresDir.replace(/\/$/, '') + '/';
|
|
86
|
-
if (path.startsWith(prefix) && path.endsWith('/feature.json')) {
|
|
87
|
-
return [...TOOLS_FOR_FEATURE_JSON];
|
|
88
|
-
}
|
|
89
|
-
return [];
|
|
80
|
+
return registryToolsForPath(path, { featuresDir, point: ENFORCEMENT_POINT });
|
|
90
81
|
}
|
|
91
82
|
|
|
92
83
|
/**
|
|
@@ -98,11 +89,7 @@ export function expectedToolsForPath(path, featuresDir) {
|
|
|
98
89
|
* @returns {string|null}
|
|
99
90
|
*/
|
|
100
91
|
export function featureCodeFromPath(path, featuresDir) {
|
|
101
|
-
|
|
102
|
-
if (!path.startsWith(prefix) || !path.endsWith('/feature.json')) return null;
|
|
103
|
-
const middle = path.slice(prefix.length, -'/feature.json'.length);
|
|
104
|
-
if (!middle || middle.includes('/')) return null;
|
|
105
|
-
return middle;
|
|
92
|
+
return registryFeatureCodeForPath(path, { featuresDir });
|
|
106
93
|
}
|
|
107
94
|
|
|
108
95
|
/**
|
|
@@ -165,9 +152,8 @@ export function enforcementError(violations) {
|
|
|
165
152
|
return err;
|
|
166
153
|
}
|
|
167
154
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
};
|
|
155
|
+
// NOTE: the `_internals` export (guarded-file set + tool sets) was removed in
|
|
156
|
+
// COMP-CANON-GUARD S1. Those declarations now live in lib/canon-registry.js —
|
|
157
|
+
// the single source of truth — with no in-repo consumer of the old shim. Import
|
|
158
|
+
// from canon-registry.js if you need the raw sets, so there is exactly ONE
|
|
159
|
+
// declaration, not a copy here that can drift.
|
package/lib/result-normalizer.js
CHANGED
|
@@ -224,6 +224,21 @@ export class AgentAbortedError extends Error {
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
function copyDispatchId(source, target) {
|
|
228
|
+
try {
|
|
229
|
+
if (!source || !target || typeof source.dispatchId !== 'string') return target;
|
|
230
|
+
Object.defineProperty(target, 'dispatchId', {
|
|
231
|
+
configurable: true,
|
|
232
|
+
enumerable: false,
|
|
233
|
+
value: source.dispatchId,
|
|
234
|
+
});
|
|
235
|
+
} catch {
|
|
236
|
+
// Error replacement must preserve the original control flow even if a
|
|
237
|
+
// third-party target is unexpectedly frozen.
|
|
238
|
+
}
|
|
239
|
+
return target;
|
|
240
|
+
}
|
|
241
|
+
|
|
227
242
|
/**
|
|
228
243
|
* STRAT-DEDUP-AGENTRUN-V3: `runAndNormalize` is now a thin wrapper around the
|
|
229
244
|
* Python connector tier exposed through `stratum_agent_run`. Events arrive as
|
|
@@ -258,6 +273,18 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
258
273
|
// overrides the bare literal for capability resolution; the provider is
|
|
259
274
|
// unchanged. Absent → bare literal (no restrictions), preserving old behavior.
|
|
260
275
|
const cfg = resolveAgentConfig(opts.profile || agentType);
|
|
276
|
+
const callerTelemetry = opts.telemetry && typeof opts.telemetry === 'object'
|
|
277
|
+
? opts.telemetry
|
|
278
|
+
: {};
|
|
279
|
+
const primaryTelemetry = {};
|
|
280
|
+
for (const field of ['project_cwd', 'site', 'build_id', 'feature_code']) {
|
|
281
|
+
if (callerTelemetry[field] !== undefined) primaryTelemetry[field] = callerTelemetry[field];
|
|
282
|
+
}
|
|
283
|
+
primaryTelemetry.step_id = stepId;
|
|
284
|
+
if (typeof stepDispatch.attempt === 'number' && Number.isFinite(stepDispatch.attempt)) {
|
|
285
|
+
primaryTelemetry.attempt = stepDispatch.attempt;
|
|
286
|
+
}
|
|
287
|
+
primaryTelemetry.effort_intended = cfg.effort ?? null;
|
|
261
288
|
// A read-only profile (no Edit/Write/Bash) also maps to a read-only sandbox so
|
|
262
289
|
// the restriction binds at the engine's connector, not just at this invocation.
|
|
263
290
|
const readOnlyProfile = Array.isArray(cfg.disallowedTools)
|
|
@@ -430,6 +457,8 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
430
457
|
};
|
|
431
458
|
|
|
432
459
|
let runResult;
|
|
460
|
+
let primaryDispatchId = null;
|
|
461
|
+
let repairDispatchId = null;
|
|
433
462
|
try {
|
|
434
463
|
if (useLocalClaude) {
|
|
435
464
|
// Test seam: an installed factory shim exposes an SDK-shaped query adapter
|
|
@@ -443,8 +472,10 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
443
472
|
allowedTools: cfg.allowedTools ?? undefined,
|
|
444
473
|
disallowedTools: cfg.disallowedTools ?? undefined,
|
|
445
474
|
thinking: cfg.thinking ?? undefined,
|
|
475
|
+
effort: cfg.effort ?? undefined,
|
|
446
476
|
abortController,
|
|
447
477
|
onToolUse: localOnToolUse,
|
|
478
|
+
telemetry: primaryTelemetry,
|
|
448
479
|
...(localQuery ? { query: localQuery } : {}),
|
|
449
480
|
});
|
|
450
481
|
} else {
|
|
@@ -457,8 +488,12 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
457
488
|
sandboxMode,
|
|
458
489
|
cwd: opts.cwd ?? undefined,
|
|
459
490
|
correlationId,
|
|
491
|
+
telemetry: primaryTelemetry,
|
|
460
492
|
});
|
|
461
493
|
}
|
|
494
|
+
primaryDispatchId = typeof runResult?.dispatchId === 'string'
|
|
495
|
+
? runResult.dispatchId
|
|
496
|
+
: null;
|
|
462
497
|
} catch (err) {
|
|
463
498
|
// F3/G3: preserve any billable usage the failed run reported (the local
|
|
464
499
|
// connector attaches it on a non-success result / usage-bearing rejection) so
|
|
@@ -469,17 +504,19 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
469
504
|
if (timedOut) {
|
|
470
505
|
const e = new AgentTimeoutError(stepId, Date.now() - startTime);
|
|
471
506
|
if (errUsage) e.usage = errUsage;
|
|
472
|
-
throw e;
|
|
507
|
+
throw copyDispatchId(err, e);
|
|
508
|
+
}
|
|
509
|
+
if (userInterruptAction) {
|
|
510
|
+
throw copyDispatchId(err, new UserInterruptError(stepId, userInterruptAction));
|
|
473
511
|
}
|
|
474
|
-
if (userInterruptAction) throw new UserInterruptError(stepId, userInterruptAction);
|
|
475
512
|
if (abortReason) {
|
|
476
513
|
const e = new AgentAbortedError(stepId, abortReason);
|
|
477
514
|
if (errUsage) e.usage = errUsage;
|
|
478
|
-
throw e;
|
|
515
|
+
throw copyDispatchId(err, e);
|
|
479
516
|
}
|
|
480
517
|
const agentError = new AgentError(err?.message ?? 'Agent run failed');
|
|
481
518
|
if (errUsage) agentError.usage = errUsage;
|
|
482
|
-
throw agentError;
|
|
519
|
+
throw copyDispatchId(err, agentError);
|
|
483
520
|
} finally {
|
|
484
521
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
485
522
|
if (onInterrupt && progress?.removeListener) progress.removeListener('interrupt', onInterrupt);
|
|
@@ -498,13 +535,15 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
498
535
|
if (timedOut) {
|
|
499
536
|
const e = new AgentTimeoutError(stepId, Date.now() - startTime);
|
|
500
537
|
if (lateUsage) e.usage = lateUsage;
|
|
501
|
-
throw e;
|
|
538
|
+
throw copyDispatchId(runResult, e);
|
|
539
|
+
}
|
|
540
|
+
if (userInterruptAction) {
|
|
541
|
+
throw copyDispatchId(runResult, new UserInterruptError(stepId, userInterruptAction));
|
|
502
542
|
}
|
|
503
|
-
if (userInterruptAction) throw new UserInterruptError(stepId, userInterruptAction);
|
|
504
543
|
if (abortReason) {
|
|
505
544
|
const e = new AgentAbortedError(stepId, abortReason);
|
|
506
545
|
if (lateUsage) e.usage = lateUsage;
|
|
507
|
-
throw e;
|
|
546
|
+
throw copyDispatchId(runResult, e);
|
|
508
547
|
}
|
|
509
548
|
|
|
510
549
|
// D2(b): the TS agent_run path returns a synchronous `complete` envelope with
|
|
@@ -540,13 +579,35 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
540
579
|
if (opts.reviewMode === true) {
|
|
541
580
|
const reviewAgentType = agentType; // already resolved from stepDispatch.agent at line 178
|
|
542
581
|
const reviewModelId = usageTotals.model ?? cfg.modelID ?? null;
|
|
582
|
+
// The repair dispatch is only CREDITED (dispatchIds.repair) when its output
|
|
583
|
+
// actually replaced the primary parse — a failed or unparseable repair still
|
|
584
|
+
// bills its usage but must not absorb the step's settlement.
|
|
585
|
+
let repairUsed = false;
|
|
586
|
+
const foldRepairUsage = (usage) => {
|
|
587
|
+
if (!usage || typeof usage !== 'object') return;
|
|
588
|
+
if (typeof usage.tokens === 'number') usageTotals.output_tokens += usage.tokens;
|
|
589
|
+
if (typeof usage.usd === 'number') usageTotals.cost_usd += usage.usd;
|
|
590
|
+
};
|
|
543
591
|
const repairFn = stratum
|
|
544
592
|
? async (repairPrompt) => {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
593
|
+
try {
|
|
594
|
+
const repairResult = await stratum.agentRun(reviewAgentType, repairPrompt, {
|
|
595
|
+
modelID: cfg.modelID ?? undefined,
|
|
596
|
+
cwd: opts.cwd ?? undefined,
|
|
597
|
+
telemetry: { ...primaryTelemetry, site: 'review-repair' },
|
|
598
|
+
});
|
|
599
|
+
repairDispatchId = typeof repairResult?.dispatchId === 'string'
|
|
600
|
+
? repairResult.dispatchId
|
|
601
|
+
: null;
|
|
602
|
+
foldRepairUsage(repairResult?.usage);
|
|
603
|
+
return repairResult?.text ?? '';
|
|
604
|
+
} catch (error) {
|
|
605
|
+
repairDispatchId = typeof error?.dispatchId === 'string'
|
|
606
|
+
? error.dispatchId
|
|
607
|
+
: null;
|
|
608
|
+
foldRepairUsage(error?.usage);
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
550
611
|
}
|
|
551
612
|
: undefined;
|
|
552
613
|
const reviewResult = await normalizeReviewResult(text, {
|
|
@@ -555,12 +616,23 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
555
616
|
confidenceGate: opts.confidenceGate ?? 7,
|
|
556
617
|
lens: opts.lens ?? 'general',
|
|
557
618
|
repairFn,
|
|
619
|
+
onRepairUsed: () => { repairUsed = true; },
|
|
558
620
|
});
|
|
559
|
-
return {
|
|
621
|
+
return {
|
|
622
|
+
text,
|
|
623
|
+
result: reviewResult,
|
|
624
|
+
usage: usageTotals,
|
|
625
|
+
dispatchIds: { primary: primaryDispatchId, repair: repairUsed ? repairDispatchId : null },
|
|
626
|
+
};
|
|
560
627
|
}
|
|
561
628
|
|
|
562
629
|
if (!hasStructuredOutput) {
|
|
563
|
-
return {
|
|
630
|
+
return {
|
|
631
|
+
text,
|
|
632
|
+
result: null,
|
|
633
|
+
usage: usageTotals,
|
|
634
|
+
dispatchIds: { primary: primaryDispatchId, repair: repairDispatchId },
|
|
635
|
+
};
|
|
564
636
|
}
|
|
565
637
|
|
|
566
638
|
const result = extractJson(text);
|
|
@@ -577,8 +649,14 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
577
649
|
result: { summary: normalizationFailure },
|
|
578
650
|
usage: usageTotals,
|
|
579
651
|
normalizationFailure,
|
|
652
|
+
dispatchIds: { primary: primaryDispatchId, repair: repairDispatchId },
|
|
580
653
|
};
|
|
581
654
|
}
|
|
582
655
|
|
|
583
|
-
return {
|
|
656
|
+
return {
|
|
657
|
+
text,
|
|
658
|
+
result,
|
|
659
|
+
usage: usageTotals,
|
|
660
|
+
dispatchIds: { primary: primaryDispatchId, repair: repairDispatchId },
|
|
661
|
+
};
|
|
584
662
|
}
|
package/lib/review-normalize.js
CHANGED
|
@@ -144,6 +144,7 @@ export async function normalizeReviewResult(rawText, {
|
|
|
144
144
|
confidenceGate = 7,
|
|
145
145
|
lens = 'general',
|
|
146
146
|
repairFn,
|
|
147
|
+
onRepairUsed,
|
|
147
148
|
} = {}) {
|
|
148
149
|
// Step 1: Try direct JSON parse
|
|
149
150
|
let parsed = parseReviewJson(rawText);
|
|
@@ -158,6 +159,9 @@ export async function normalizeReviewResult(rawText, {
|
|
|
158
159
|
|
|
159
160
|
if (repairText) {
|
|
160
161
|
parsed = parseReviewJson(repairText);
|
|
162
|
+
// Only a parseable repair actually replaces the primary output — the
|
|
163
|
+
// caller uses this to decide which dispatch gets credited.
|
|
164
|
+
if (parsed && typeof onRepairUsed === 'function') onRepairUsed();
|
|
161
165
|
}
|
|
162
166
|
}
|
|
163
167
|
|