@iowarp/clio-coder 0.3.7 → 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/CHANGELOG.md +31 -0
- package/README.md +7 -2
- package/dist/{acp-SK4MD6MM.js → acp-U67UHUK2.js} +13 -13
- package/dist/{agents-2FN2K6ME.js → agents-YU6SGALZ.js} +35 -33
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-QIYZWM5I.js → auth-5ZPJOIVG.js} +32 -24
- package/dist/builtins-C6JMZVV6.js +17 -0
- package/dist/{chunk-YD734TPH.js → chunk-26LEYJZH.js} +2 -2
- package/dist/{chunk-TSHXZTOQ.js → chunk-2HEJ2F35.js} +5 -5
- package/dist/{chunk-WJHBC77E.js → chunk-2HFZQUHL.js} +5 -5
- package/dist/{chunk-FO5ZOVUY.js → chunk-3BINW3FP.js} +5 -5
- package/dist/{chunk-EOOQZZDE.js → chunk-4SPRNWDE.js} +18 -18
- package/dist/{chunk-EBEFWSGL.js → chunk-5DHKRSMQ.js} +7 -7
- package/dist/{chunk-OBMAI2DP.js → chunk-5H3GB5BO.js} +11 -9
- package/dist/{chunk-JTSEDYVQ.js → chunk-5Q2VVUKB.js} +7 -7
- package/dist/{chunk-VQNODYQ4.js → chunk-7RFXX52T.js} +294 -3704
- package/dist/{chunk-XEGB6BCN.js → chunk-7RGZWPB6.js} +5 -5
- package/dist/{chunk-GH5622CP.js → chunk-A2NJGIB3.js} +2 -2
- package/dist/{chunk-CEYBNUGC.js → chunk-A3WNZD3P.js} +323 -27
- package/dist/{chunk-D4MDIG46.js → chunk-B5CSFE7B.js} +7 -7
- package/dist/{chunk-2SFS6XQE.js → chunk-DGSYXYMX.js} +3 -2
- package/dist/{chunk-ZWLZP4ZT.js → chunk-DYIM5TJT.js} +98 -12
- package/dist/{chunk-KCMKRQX4.js → chunk-E77JEWSD.js} +42 -49
- package/dist/{chunk-J7PIKKWC.js → chunk-EMYUUSFG.js} +7 -7
- package/dist/{chunk-JRIO5UD2.js → chunk-EQ63NRB7.js} +5 -5
- package/dist/{chunk-UVDSQ6LW.js → chunk-FBVTI2TJ.js} +74 -28
- package/dist/{chunk-BMWK7ZIZ.js → chunk-FHJEP5SW.js} +13 -13
- package/dist/{chunk-FOT2FX5J.js → chunk-GN57SG4G.js} +7 -7
- package/dist/{chunk-SROCI7ZU.js → chunk-GPIEI3LY.js} +5 -5
- package/dist/{chunk-UFQ3F4FW.js → chunk-GU2UIAFZ.js} +4 -4
- package/dist/{chunk-UND3GU2L.js → chunk-H7IXIC72.js} +2 -2
- package/dist/{chunk-VREKEFLL.js → chunk-HLE42MG7.js} +3 -3
- package/dist/{chunk-465YSENW.js → chunk-IFBNV6H6.js} +3 -3
- package/dist/{chunk-LADCF22A.js → chunk-IGWKHNIQ.js} +113 -54
- package/dist/chunk-IIZWH4XA.js +172 -0
- package/dist/{chunk-UHXRNZ2J.js → chunk-IJ7RPIYJ.js} +4 -4
- package/dist/{chunk-M4AKACEO.js → chunk-J3YUBZWY.js} +2 -2
- package/dist/{chunk-AB4XIIVB.js → chunk-JOZYP4GM.js} +6 -6
- package/dist/{chunk-ZGH7FGS5.js → chunk-K4XHGFR5.js} +742 -12
- package/dist/{chunk-EELBMBT6.js → chunk-KTYTFRMB.js} +62 -5
- package/dist/chunk-LU7P4LHA.js +33 -0
- package/dist/{chunk-DQA7QLMD.js → chunk-ME6CCNFO.js} +9 -24
- package/dist/{chunk-KZ2H5X4G.js → chunk-MXKJU4JB.js} +88 -14
- package/dist/{chunk-DMD2AGVS.js → chunk-N22QMJKY.js} +20 -18
- package/dist/chunk-NMPKI6XL.js +3006 -0
- package/dist/chunk-NUGM5KR6.js +165 -0
- package/dist/{chunk-5C77SEEY.js → chunk-P43ETTHK.js} +2 -2
- package/dist/{chunk-JNXPYBB4.js → chunk-PMDBGQSJ.js} +2 -2
- package/dist/chunk-PT7HYKEM.js +165 -0
- package/dist/{chunk-GEYXPTRF.js → chunk-RWSI4YD7.js} +2 -2
- package/dist/{chunk-C4JBQ5SR.js → chunk-TANS5ZJS.js} +6 -6
- package/dist/{chunk-YTYFXUI3.js → chunk-TB5666IT.js} +9 -9
- package/dist/{chunk-UUANF5CR.js → chunk-TLQJPP24.js} +375 -687
- package/dist/{chunk-OB5HIGJY.js → chunk-TT36MB5S.js} +2 -1
- package/dist/{chunk-DJNLUABN.js → chunk-TTHACPOM.js} +135 -17
- package/dist/{chunk-6M7VS3J3.js → chunk-TYPGUK6W.js} +4 -4
- package/dist/{chunk-THKY7CD7.js → chunk-U6MBIEMB.js} +108 -24
- package/dist/{chunk-PD3MESLB.js → chunk-VAWNZU7Z.js} +4 -4
- package/dist/{chunk-IR4CFBFN.js → chunk-VCBR6CU7.js} +12 -12
- package/dist/{chunk-WHJYKASB.js → chunk-VHN4MY6O.js} +2 -2
- package/dist/{chunk-MXI6J5JF.js → chunk-VWZOAB7K.js} +10 -10
- package/dist/{chunk-QCTRSGHQ.js → chunk-WLFILSD5.js} +36 -36
- package/dist/{chunk-5UJ6ECTS.js → chunk-WNIJTQQK.js} +6 -6
- package/dist/{chunk-4DWFMQDR.js → chunk-WSB3FPX7.js} +68 -20
- package/dist/{chunk-3HAPLH5M.js → chunk-WWCZ5F23.js} +112 -10
- package/dist/{chunk-X2KV5FXT.js → chunk-WXY7KU3G.js} +2 -2
- package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
- package/dist/{chunk-DOOEX22V.js → chunk-XWSF374K.js} +4 -4
- package/dist/{chunk-6TUKSZVF.js → chunk-YS5VLNH5.js} +8 -8
- package/dist/{chunk-ZZMN5OM4.js → chunk-ZNLWCMVZ.js} +2 -2
- package/dist/{chunk-WHGPSPT5.js → chunk-ZVJ5BLO2.js} +2 -2
- package/dist/cli/index.js +31 -31
- package/dist/{clio-WBVQEBKO.js → clio-QVTYJ57A.js} +8 -8
- package/dist/code-nav-FGGFIE7L.js +2 -2
- package/dist/codewiki/build-worker.js +4 -4
- package/dist/{components-F7OEATSO.js → components-ZFA3SAER.js} +8 -8
- package/dist/{config-TRBL3RCF.js → config-LW5IJFQN.js} +58 -53
- package/dist/{configure-OLCVPHNM.js → configure-7XIZCOU4.js} +26 -21
- package/dist/{context-XEWE3MOJ.js → context-L3WL3X7K.js} +47 -41
- package/dist/{context-WFPKQSM6.js → context-N52ZA626.js} +22 -22
- package/dist/{context-MJIJ6GOX.js → context-Y6Y7QPR6.js} +12 -12
- package/dist/{context-clear-KNOS2JPB.js → context-clear-MBQRLSDQ.js} +47 -41
- package/dist/{context-index-SSR5ECNE.js → context-index-HVMFQHK3.js} +5 -5
- package/dist/{context-working-set-EUXAZI6N.js → context-working-set-GS6DSO7F.js} +14 -14
- package/dist/{dispatch-runner-B7MTOVKL.js → dispatch-runner-22ZCNOM3.js} +83 -55
- package/dist/{docs-FLJTIDSE.js → docs-7LQ23DLM.js} +8 -8
- package/dist/{doctor-RN4YKO2X.js → doctor-M7YEDGAE.js} +24 -20
- package/dist/{eval-RUBJVSNQ.js → eval-BEC2WHDA.js} +71 -19
- package/dist/{evidence-JZNBUOQZ.js → evidence-REJUMSKM.js} +62 -56
- package/dist/{evolve-FJVC4KKI.js → evolve-PY5ZBA5K.js} +41 -35
- package/dist/{extensions-IQL36S7K.js → extensions-HVKU65YU.js} +6 -6
- package/dist/{fleet-BDKYJFCP.js → fleet-7WZEWRFA.js} +72 -67
- package/dist/{fleet-commands-ZFIWZSB3.js → fleet-commands-UVHWM76J.js} +10 -10
- package/dist/{fleet-graph-Y6HPXIVF.js → fleet-graph-6ULH7PES.js} +13 -13
- package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-J53T6CCE.js} +5 -5
- package/dist/{fleet-validate-BIYREGIK.js → fleet-validate-72PC4SLA.js} +17 -17
- package/dist/{init-LQUB5COQ.js → init-OG3TPGQG.js} +61 -56
- package/dist/{library-NJAHIGG4.js → library-CNTMPLRF.js} +18 -18
- package/dist/{memory-OG6HOYKM.js → memory-6IS7F275.js} +43 -37
- package/dist/{models-5ZG5XY7J.js → models-ENRJDA5W.js} +33 -28
- package/dist/{monitor-TJ7AMTGB.js → monitor-XLDVO7TN.js} +56 -38
- package/dist/{orchestrator-WZYB54DM.js → orchestrator-6KSPYRHA.js} +421 -303
- package/dist/{paths-XUC7GS6E.js → paths-DBXMZMDU.js} +5 -5
- package/dist/registry-LG64LTF4.js +11 -0
- package/dist/{reset-PXQT45IY.js → reset-RZ4ER727.js} +10 -10
- package/dist/{run-FQ74YF62.js → run-Y2CNK5RU.js} +79 -74
- package/dist/{share-FW7SVCL3.js → share-A55GYP6Z.js} +16 -16
- package/dist/{skills-7E7IRB3R.js → skills-ALC5J6AT.js} +19 -19
- package/dist/{skills-eval-LI75W6OK.js → skills-eval-JPBEBYQU.js} +52 -47
- package/dist/support-MIETYA5E.js +38 -0
- package/dist/{targets-4CIFKCTW.js → targets-VGNXIR3S.js} +42 -33
- package/dist/{terminal-lease-WUZY7ZV5.js → terminal-lease-WOBR64YA.js} +3 -3
- package/dist/{uninstall-7FV7IP4E.js → uninstall-ZJF5H5ZN.js} +8 -8
- package/dist/{upgrade-K2HVIVMQ.js → upgrade-FUSUAGHR.js} +27 -25
- package/dist/{usage-GTZELZQX.js → usage-N4MKVHKD.js} +51 -46
- package/dist/{verifiers-RLAHT27O.js → verifiers-YAWOJ3H2.js} +13 -13
- package/dist/{verify-BX3BRKH5.js → verify-LTDHYBGY.js} +9 -9
- package/dist/{wiki-generate-ASIFASCN.js → wiki-generate-6M7GHTBJ.js} +66 -61
- package/dist/worker/entry.js +68 -61
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +1 -1
- package/docs/artifact-versions.md +6 -5
- package/docs/built-in-agents.md +1 -1
- package/docs/commands-and-modes.md +2 -2
- package/docs/configuration-and-targets.md +8 -3
- package/docs/context-engine.md +1 -1
- package/docs/documentation-guide.md +1 -1
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +1 -1
- package/docs/evidence-and-memory.md +70 -6
- package/docs/evolution.md +1 -1
- package/docs/extensions-and-sharing.md +1 -1
- package/docs/fleet-dispatch.md +34 -9
- package/docs/glossary.md +21 -1
- package/docs/installation-and-lifecycle.md +1 -1
- package/docs/middleware-and-components.md +1 -1
- package/docs/model-catalog.md +1 -1
- package/docs/observability.md +2 -2
- package/docs/proactive-memory.md +1 -1
- package/docs/prompt-envelope-and-tools.md +4 -2
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +41 -38
- package/docs/safety-model.md +1 -1
- package/docs/scientific-validation.md +1 -1
- package/docs/skills-marketplace.md +1 -1
- package/docs/tool-usage.md +1 -1
- package/docs/trace-store.md +1 -1
- package/docs/tui-design.md +1 -1
- package/docs/worker-dispatch-mechanics.md +1 -1
- package/package.json +1 -2
- package/src/cli/argv.ts +5 -0
- package/src/cli/configure.ts +107 -23
- package/src/cli/doctor.ts +5 -1
- package/src/cli/evidence.ts +30 -25
- package/src/cli/fleet-preflight.ts +2 -12
- package/src/cli/shared.ts +1 -0
- package/src/cli/targets.ts +4 -1
- package/src/cli/validate-model.ts +60 -5
- package/src/core/bus-events.ts +25 -0
- package/src/core/commit-attribution.ts +4 -4
- package/src/core/path-boundary.ts +100 -0
- package/src/domains/agents/extension.ts +2 -11
- package/src/domains/agents/fleet-contract.ts +30 -12
- package/src/domains/agents/recipe.ts +7 -1
- package/src/domains/agents/registry.ts +73 -5
- package/src/domains/agents/result-contract.ts +128 -17
- package/src/domains/agents/write-boundary.ts +15 -50
- package/src/domains/context/project-rules.ts +51 -1
- package/src/domains/dispatch/assignment-reconcile.ts +22 -5
- package/src/domains/dispatch/assignment-store.ts +151 -14
- package/src/domains/dispatch/contract.ts +15 -1
- package/src/domains/dispatch/delegation-plan.ts +2 -5
- package/src/domains/dispatch/execution-role.ts +9 -1
- package/src/domains/dispatch/extension.ts +156 -67
- package/src/domains/dispatch/fleet-run.ts +57 -3
- package/src/domains/dispatch/gate-role-prompts.ts +38 -0
- package/src/domains/dispatch/index.ts +3 -1
- package/src/domains/dispatch/intent-requirements.ts +40 -0
- package/src/domains/dispatch/intent.ts +84 -8
- package/src/domains/dispatch/path-scope.ts +370 -0
- package/src/domains/dispatch/receipt-integrity.ts +2 -1
- package/src/domains/dispatch/types.ts +14 -7
- package/src/domains/dispatch/validation.ts +6 -3
- package/src/domains/dispatch/write-boundary-enforcer.ts +45 -0
- package/src/domains/dispatch/write-boundary.ts +201 -22
- package/src/domains/eval/metrics/evidence.ts +79 -2
- package/src/domains/eval/runners/clio-run.ts +12 -2
- package/src/domains/evidence/build.ts +69 -11
- package/src/domains/evidence/index.ts +21 -0
- package/src/domains/evidence/provenance.ts +46 -11
- package/src/domains/evidence/trust-projection.ts +274 -0
- package/src/domains/evidence/trust-status.ts +145 -17
- package/src/domains/evidence/types.ts +4 -0
- package/src/domains/extensions/discovery.ts +88 -1
- package/src/domains/extensions/resources.ts +20 -8
- package/src/domains/extensions/state.ts +6 -2
- package/src/domains/extensions/types.ts +4 -1
- package/src/domains/lifecycle/doctor.ts +140 -1
- package/src/domains/prompts/contract.ts +3 -5
- package/src/domains/providers/extension.ts +30 -2
- package/src/domains/resources/common-loader.ts +3 -0
- package/src/domains/resources/prompts/loader.ts +119 -14
- package/src/domains/safety/policy-engine.ts +5 -5
- package/src/domains/safety/run-effects.ts +64 -1
- package/src/domains/safety/scope.ts +7 -12
- package/src/engine/acp/server.ts +4 -1
- package/src/engine/prompt-templates.ts +18 -1
- package/src/engine/worker-runtime.ts +6 -3
- package/src/interactive/dispatch-board.ts +53 -3
- package/src/interactive/interactive-event-projection.ts +14 -0
- package/src/interactive/overlays/settings.ts +141 -47
- package/src/interactive/slash-commands.ts +7 -2
- package/src/interactive/view/artifacts.ts +42 -9
- package/src/interactive/view/view-overlay.ts +15 -3
- package/src/interactive/worker-receipts.ts +14 -2
- package/src/interactive/worker-stream.ts +8 -0
- package/src/tools/dispatch-admission.ts +12 -13
- package/src/tools/dispatch-arguments.ts +27 -0
- package/src/tools/dispatch-plan.ts +29 -0
- package/src/tools/dispatch-runner.ts +48 -13
- package/src/tools/monitor.ts +13 -0
- package/src/tools/worker-evidence.ts +19 -13
- package/src/worker/spec-contract.ts +2 -1
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* a path that changed cannot be pinned on one of them, and the plan compiler
|
|
9
9
|
* has already refused the case where more than one of them was allowed to
|
|
10
10
|
* write. Every step in a violating window fails, and the verdict says why.
|
|
11
|
+
*
|
|
12
|
+
* The window also carries what its runs recorded writing, which is what keeps
|
|
13
|
+
* the checkout diff from being read as authorship. A change nobody in the
|
|
14
|
+
* window recorded belongs to whoever else has the checkout open, and the
|
|
15
|
+
* verdict reports it without touching it.
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
import { existsSync } from "node:fs";
|
|
@@ -16,9 +21,11 @@ import type { ExecutionPlan } from "./execution-plan.js";
|
|
|
16
21
|
import type { ExecutionWriteBoundaryOutcome } from "./execution-scheduler.js";
|
|
17
22
|
import {
|
|
18
23
|
assertWriteBoundaryInsideRoot,
|
|
24
|
+
assertWriteBoundaryVisibleToGit,
|
|
19
25
|
captureWorkspaceSnapshot,
|
|
20
26
|
enforceWriteBoundary,
|
|
21
27
|
type WorkspaceSnapshot,
|
|
28
|
+
type WriteBoundaryAttribution,
|
|
22
29
|
type WriteBoundaryVerdict,
|
|
23
30
|
writeWriteBoundaryVerdict,
|
|
24
31
|
} from "./write-boundary.js";
|
|
@@ -30,6 +37,15 @@ export interface WriteBoundaryEnforcerInput {
|
|
|
30
37
|
rootId: string;
|
|
31
38
|
/** Declared allowlist for a step, or undefined when it declares no boundary. */
|
|
32
39
|
boundaryFor(stepId: string): ReadonlyArray<string> | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* What the step's own run recorded writing, or null when the step has no
|
|
42
|
+
* closed write record: a code step running a registered command, a runtime
|
|
43
|
+
* that publishes no tool telemetry, a run whose telemetry had holes, or a run
|
|
44
|
+
* that shelled out or spawned a worker and could have written a path no
|
|
45
|
+
* argument names. Absent entirely means the caller offers no record at all,
|
|
46
|
+
* and every change outside the declaration is blamed on the window.
|
|
47
|
+
*/
|
|
48
|
+
recordedWritesFor?(stepId: string): ReadonlyArray<string> | null;
|
|
33
49
|
/** Called once per closed window with the sealed verdict and where it landed. */
|
|
34
50
|
onVerdict?(verdict: WriteBoundaryVerdict, path: string): void;
|
|
35
51
|
}
|
|
@@ -54,6 +70,9 @@ export function preflightWriteBoundaries(plan: ExecutionPlan, root: string): voi
|
|
|
54
70
|
);
|
|
55
71
|
}
|
|
56
72
|
for (const step of declared) assertWriteBoundaryInsideRoot(root, step.writes ?? []);
|
|
73
|
+
// One question over the whole plan: an entry is refused for being invisible
|
|
74
|
+
// to git whichever step declared it, and the diagnostic names the path.
|
|
75
|
+
assertWriteBoundaryVisibleToGit(root, [...new Set(declared.flatMap((step) => [...(step.writes ?? [])]))]);
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
export function createWriteBoundaryEnforcer(input: WriteBoundaryEnforcerInput): WriteBoundaryEnforcer {
|
|
@@ -63,10 +82,34 @@ export function createWriteBoundaryEnforcer(input: WriteBoundaryEnforcerInput):
|
|
|
63
82
|
for (const stepId of stepIds) for (const entry of input.boundaryFor(stepId) ?? []) allow.add(entry);
|
|
64
83
|
return [...allow].sort();
|
|
65
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* The window's write witness. One step without a record makes the whole
|
|
87
|
+
* window's record incomplete: the steps share a checkout, so a change no
|
|
88
|
+
* other step wrote could still be the unobserved one's.
|
|
89
|
+
*/
|
|
90
|
+
const attributionFor = (stepIds: ReadonlyArray<string>): WriteBoundaryAttribution | undefined => {
|
|
91
|
+
const recordedWritesFor = input.recordedWritesFor;
|
|
92
|
+
if (recordedWritesFor === undefined) return undefined;
|
|
93
|
+
const recorded = new Set<string>();
|
|
94
|
+
let complete = true;
|
|
95
|
+
for (const stepId of stepIds) {
|
|
96
|
+
const observed = recordedWritesFor(stepId);
|
|
97
|
+
if (observed === null) {
|
|
98
|
+
complete = false;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
for (const target of observed) recorded.add(target);
|
|
102
|
+
}
|
|
103
|
+
return { recorded: [...recorded].sort(), complete };
|
|
104
|
+
};
|
|
66
105
|
return {
|
|
67
106
|
begin(window, stepIds) {
|
|
68
107
|
const allow = allowFor(stepIds);
|
|
69
108
|
assertWriteBoundaryInsideRoot(input.root, allow);
|
|
109
|
+
// Also checked whole-plan at preflight. Repeated here because a
|
|
110
|
+
// delegation plan splices steps into a running fleet, and their
|
|
111
|
+
// declarations reach this function without ever passing preflight.
|
|
112
|
+
assertWriteBoundaryVisibleToGit(input.root, allow);
|
|
70
113
|
open.set(window, { snapshot: captureWorkspaceSnapshot(input.root), allow });
|
|
71
114
|
},
|
|
72
115
|
async verify(window, stepIds) {
|
|
@@ -75,11 +118,13 @@ export function createWriteBoundaryEnforcer(input: WriteBoundaryEnforcerInput):
|
|
|
75
118
|
throw new Error(`write boundary: window '${window}' was verified without a snapshot`);
|
|
76
119
|
}
|
|
77
120
|
open.delete(window);
|
|
121
|
+
const attribution = attributionFor(stepIds);
|
|
78
122
|
const verdict = enforceWriteBoundary({
|
|
79
123
|
snapshot: pending.snapshot,
|
|
80
124
|
window,
|
|
81
125
|
stepIds,
|
|
82
126
|
allow: pending.allow,
|
|
127
|
+
...(attribution === undefined ? {} : { attribution }),
|
|
83
128
|
});
|
|
84
129
|
const path = writeWriteBoundaryVerdict(input.rootId, verdict);
|
|
85
130
|
input.onVerdict?.(verdict, path);
|
|
@@ -21,18 +21,27 @@
|
|
|
21
21
|
* Everything is pinned to the baseline commit recorded at snapshot time, so a
|
|
22
22
|
* commit step that moves HEAD without touching the working tree reads as "no
|
|
23
23
|
* change" instead of as a wholesale rewrite.
|
|
24
|
+
*
|
|
25
|
+
* A window's diff answers "what changed", never "who changed it". An operator
|
|
26
|
+
* editing the checkout while a fleet runs produces changes inside the window
|
|
27
|
+
* that no step made, and rolling those back to the baseline destroys work
|
|
28
|
+
* nobody asked Clio to touch. So a change is blamed on the window only when it
|
|
29
|
+
* intersects what the window's own runs recorded writing; everything else is
|
|
30
|
+
* reported as an unattributed concurrent change and left exactly as it is.
|
|
24
31
|
*/
|
|
25
32
|
|
|
26
33
|
import { execFileSync } from "node:child_process";
|
|
27
34
|
import { createHash } from "node:crypto";
|
|
28
35
|
import { existsSync, lstatSync, mkdirSync, readlinkSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
29
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
36
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
37
|
+
import { asDirectoryPathBoundary, pathBoundaryEntryCovers } from "../../core/path-boundary.js";
|
|
30
38
|
import { clioStateDir } from "../../core/xdg.js";
|
|
31
39
|
import { atomicWrite } from "../../engine/session.js";
|
|
32
40
|
import { isGitRepository } from "../../tools/compete-worktrees.js";
|
|
33
41
|
import {
|
|
34
42
|
describeWriteBoundary,
|
|
35
43
|
normalizeWriteBoundary,
|
|
44
|
+
normalizeWriteBoundaryEntry,
|
|
36
45
|
type WriteBoundary,
|
|
37
46
|
writeBoundaryCovers,
|
|
38
47
|
} from "../agents/write-boundary.js";
|
|
@@ -71,10 +80,15 @@ function gitBytes(root: string, args: ReadonlyArray<string>): Buffer {
|
|
|
71
80
|
*
|
|
72
81
|
* Enforcement sees exactly what git sees, so an ignored path is outside it: a
|
|
73
82
|
* repository decides what counts as its own content, and this is not the place
|
|
74
|
-
* to overrule that. The
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
83
|
+
* to overrule that. The consequence is that a boundary declared over an ignored
|
|
84
|
+
* path could never be verified, which is why `assertWriteBoundaryVisibleToGit`
|
|
85
|
+
* refuses that declaration before anything runs rather than letting this
|
|
86
|
+
* function certify an unobserved window as clean.
|
|
87
|
+
*
|
|
88
|
+
* The one thing subtracted is Clio's own state directory when an operator has
|
|
89
|
+
* placed it inside the workspace. Receipts, code-step logs, and boundary
|
|
90
|
+
* verdicts are the orchestrator writing its journal while the step runs, and
|
|
91
|
+
* blaming the step for them would make every window a violation.
|
|
78
92
|
*/
|
|
79
93
|
function dirtyPaths(root: string): string[] {
|
|
80
94
|
const raw = git(root, ["status", "--porcelain=v1", "-z", "-uall", "--no-renames"]);
|
|
@@ -222,14 +236,55 @@ export interface WriteBoundaryUnrecoverable {
|
|
|
222
236
|
}
|
|
223
237
|
|
|
224
238
|
/**
|
|
225
|
-
* `clean` means
|
|
226
|
-
*
|
|
227
|
-
* `rollback-incomplete`: the working tree is
|
|
228
|
-
* and handed to the operator with the list,
|
|
229
|
-
* content it never recorded destroys work.
|
|
239
|
+
* `clean` means no change outside the allowlist is attributable to this
|
|
240
|
+
* window's steps. `rolled-back` means one was and the repository now looks as
|
|
241
|
+
* it did before. Anything else is `rollback-incomplete`: the working tree is
|
|
242
|
+
* left exactly as the step made it and handed to the operator with the list,
|
|
243
|
+
* because a rollback that guesses at content it never recorded destroys work.
|
|
244
|
+
*
|
|
245
|
+
* A `clean` verdict can still carry `unattributed` paths. Those changed inside
|
|
246
|
+
* the window and no run in it recorded writing them, so they are the
|
|
247
|
+
* operator's, another process's, or a build artifact's, and nothing here may
|
|
248
|
+
* touch them.
|
|
230
249
|
*/
|
|
231
250
|
export type WriteBoundaryStatus = "clean" | "rolled-back" | "rollback-incomplete";
|
|
232
251
|
|
|
252
|
+
/**
|
|
253
|
+
* What the window's own runs recorded aiming a successful mutation at.
|
|
254
|
+
*
|
|
255
|
+
* This is the write-side witness the diff cannot be: the checkout says a path
|
|
256
|
+
* changed, and only the run's own tool-call record says whether the run is what
|
|
257
|
+
* changed it. The record comes from the same fold that grounds a sealed
|
|
258
|
+
* mutation report, so the boundary and the result contract judge one set of
|
|
259
|
+
* facts.
|
|
260
|
+
*
|
|
261
|
+
* A run can write through a channel no tool event enumerates, and then its path
|
|
262
|
+
* set is a lower bound rather than the whole list. That is not silently
|
|
263
|
+
* tolerated: the recorder knows which tools can do it (bash, verify, dispatch,
|
|
264
|
+
* steer, and any tool this process has no schema for) and reports the record
|
|
265
|
+
* open, which arrives here as `complete: false`. So the set is only ever read
|
|
266
|
+
* as a closed list for a run that could not have written outside it, and
|
|
267
|
+
* enforcement over a run that could stays exactly as strong as it was before
|
|
268
|
+
* attribution existed.
|
|
269
|
+
*/
|
|
270
|
+
export interface WriteBoundaryAttribution {
|
|
271
|
+
/**
|
|
272
|
+
* Paths the window's runs aimed a successful mutation at. Absolute, or
|
|
273
|
+
* relative to the workspace root; anything resolving outside the workspace is
|
|
274
|
+
* dropped, because git could not have reported it as a change either.
|
|
275
|
+
*/
|
|
276
|
+
recorded: ReadonlyArray<string>;
|
|
277
|
+
/**
|
|
278
|
+
* False when at least one step in the window has no closed write record: a
|
|
279
|
+
* code step running a registered command, a runtime that publishes no tool
|
|
280
|
+
* telemetry, a run whose telemetry came back with holes, or a run that
|
|
281
|
+
* called a tool able to write a path its arguments do not name. An open
|
|
282
|
+
* record cannot clear anybody, so the window falls back to blaming every
|
|
283
|
+
* change outside the allowlist, and the verdict says it did.
|
|
284
|
+
*/
|
|
285
|
+
complete: boolean;
|
|
286
|
+
}
|
|
287
|
+
|
|
233
288
|
export interface WriteBoundaryVerdict {
|
|
234
289
|
version: 1;
|
|
235
290
|
/** Which scheduling window this covers, e.g. `wave-2` or `revalidate-suite.check.1`. */
|
|
@@ -243,12 +298,25 @@ export interface WriteBoundaryVerdict {
|
|
|
243
298
|
checkedAt: string;
|
|
244
299
|
changedPaths: ReadonlyArray<string>;
|
|
245
300
|
violations: ReadonlyArray<string>;
|
|
301
|
+
/**
|
|
302
|
+
* Paths outside the allowlist that changed inside the window and that no run
|
|
303
|
+
* in it recorded writing. Recorded so the operator can see them, never rolled
|
|
304
|
+
* back: the window did not make them, so the baseline commit is not their
|
|
305
|
+
* prior content.
|
|
306
|
+
*/
|
|
307
|
+
unattributed: ReadonlyArray<string>;
|
|
308
|
+
/** Whether every step in the window offered an enumerable write record. */
|
|
309
|
+
attributionComplete: boolean;
|
|
246
310
|
rolledBack: ReadonlyArray<WriteBoundaryRollback>;
|
|
247
311
|
unrecoverable: ReadonlyArray<WriteBoundaryUnrecoverable>;
|
|
248
312
|
status: WriteBoundaryStatus;
|
|
249
313
|
/** Typed failure reason, or null when the window stayed inside its boundary. */
|
|
250
314
|
reason: typeof WRITE_BOUNDARY_VIOLATION_REASON | null;
|
|
251
|
-
/**
|
|
315
|
+
/**
|
|
316
|
+
* Operator-facing message naming the offending paths and the declaration, or
|
|
317
|
+
* naming the concurrent changes the window saw but did not cause. Null only
|
|
318
|
+
* when neither happened.
|
|
319
|
+
*/
|
|
252
320
|
detail: string | null;
|
|
253
321
|
digest: string;
|
|
254
322
|
}
|
|
@@ -258,6 +326,12 @@ export interface EnforceWriteBoundaryInput {
|
|
|
258
326
|
window: string;
|
|
259
327
|
stepIds: ReadonlyArray<string>;
|
|
260
328
|
allow: WriteBoundary;
|
|
329
|
+
/**
|
|
330
|
+
* What the window's runs recorded writing. Absent means no caller offered a
|
|
331
|
+
* record, which reads the same as an incomplete one: every change outside the
|
|
332
|
+
* allowlist is blamed on the window.
|
|
333
|
+
*/
|
|
334
|
+
attribution?: WriteBoundaryAttribution;
|
|
261
335
|
}
|
|
262
336
|
|
|
263
337
|
function canonicalVerdict(verdict: Omit<WriteBoundaryVerdict, "digest">): string {
|
|
@@ -317,6 +391,22 @@ function rollbackPath(
|
|
|
317
391
|
return { rolledBack: { path: change.path, action: "restored", restoredFrom: `${snapshot.head}:${change.path}` } };
|
|
318
392
|
}
|
|
319
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Whether one changed path falls inside what a run recorded writing.
|
|
396
|
+
*
|
|
397
|
+
* A recorded target is usually the exact file a write or edit named, but a
|
|
398
|
+
* command's delete target can be a directory, so a change under a recorded
|
|
399
|
+
* prefix counts as the same write. Matching is on segment boundaries: `work`
|
|
400
|
+
* does not cover `workspace/a.txt`.
|
|
401
|
+
*/
|
|
402
|
+
function withinRecordedWrite(recorded: ReadonlySet<string>, path: string): boolean {
|
|
403
|
+
if (recorded.has(path)) return true;
|
|
404
|
+
for (const target of recorded) {
|
|
405
|
+
if (path.startsWith(`${target}/`)) return true;
|
|
406
|
+
}
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
320
410
|
/**
|
|
321
411
|
* Compare, roll back, and return the verdict. Never throws for a violation: a
|
|
322
412
|
* violation is evidence the caller records and acts on, not an exception that
|
|
@@ -325,7 +415,21 @@ function rollbackPath(
|
|
|
325
415
|
export function enforceWriteBoundary(input: EnforceWriteBoundaryInput): WriteBoundaryVerdict {
|
|
326
416
|
const allow = normalizeWriteBoundary(input.allow);
|
|
327
417
|
const changes = diffWorkspace(input.snapshot);
|
|
328
|
-
const
|
|
418
|
+
const outside = changes.filter((change) => !writeBoundaryCovers(allow, change.path));
|
|
419
|
+
const attributionComplete = input.attribution?.complete === true;
|
|
420
|
+
const recorded = new Set<string>();
|
|
421
|
+
for (const target of input.attribution?.recorded ?? []) {
|
|
422
|
+
const relativeTarget = repoRelative(input.snapshot.root, target);
|
|
423
|
+
if (relativeTarget !== null) recorded.add(relativeTarget);
|
|
424
|
+
}
|
|
425
|
+
// Without a complete record nothing can be cleared, so the window keeps the
|
|
426
|
+
// whole diff. With one, only what the runs actually wrote is theirs.
|
|
427
|
+
const violations = attributionComplete
|
|
428
|
+
? outside.filter((change) => withinRecordedWrite(recorded, change.path))
|
|
429
|
+
: outside;
|
|
430
|
+
const unattributed = attributionComplete
|
|
431
|
+
? outside.filter((change) => !withinRecordedWrite(recorded, change.path))
|
|
432
|
+
: [];
|
|
329
433
|
const rolledBack: WriteBoundaryRollback[] = [];
|
|
330
434
|
const unrecoverable: WriteBoundaryUnrecoverable[] = [];
|
|
331
435
|
for (const change of violations) {
|
|
@@ -335,15 +439,15 @@ export function enforceWriteBoundary(input: EnforceWriteBoundaryInput): WriteBou
|
|
|
335
439
|
}
|
|
336
440
|
const status: WriteBoundaryStatus =
|
|
337
441
|
violations.length === 0 ? "clean" : unrecoverable.length === 0 ? "rolled-back" : "rollback-incomplete";
|
|
338
|
-
const
|
|
442
|
+
const who =
|
|
339
443
|
input.stepIds.length === 1
|
|
340
444
|
? `step '${input.stepIds[0]}'`
|
|
341
445
|
: `steps ${input.stepIds.map((id) => `'${id}'`).join(", ")} ran concurrently in one checkout, so the write cannot be attributed to one of them`;
|
|
342
|
-
const
|
|
446
|
+
const violationDetail =
|
|
343
447
|
violations.length === 0
|
|
344
|
-
?
|
|
448
|
+
? []
|
|
345
449
|
: [
|
|
346
|
-
`${
|
|
450
|
+
`${who} wrote outside its declared boundary.`,
|
|
347
451
|
`Unauthorized paths: ${violations.map((change) => change.path).join(", ")}.`,
|
|
348
452
|
`Declared writes: ${describeWriteBoundary(allow)}.`,
|
|
349
453
|
status === "rolled-back"
|
|
@@ -351,8 +455,21 @@ export function enforceWriteBoundary(input: EnforceWriteBoundaryInput): WriteBou
|
|
|
351
455
|
: `Rollback is incomplete; the working tree is left as the step made it. Unrestorable: ${unrecoverable
|
|
352
456
|
.map((entry) => `${entry.path} (${entry.reason})`)
|
|
353
457
|
.join("; ")}.`,
|
|
458
|
+
attributionComplete
|
|
459
|
+
? "Each was blamed on this window because a run in it recorded writing that path."
|
|
460
|
+
: "This window had no complete write record, so every change outside the declaration was blamed on it.",
|
|
354
461
|
"If the change was legitimate, widen the step's `writes:` declaration to cover it.",
|
|
355
|
-
]
|
|
462
|
+
];
|
|
463
|
+
const concurrentDetail =
|
|
464
|
+
unattributed.length === 0
|
|
465
|
+
? []
|
|
466
|
+
: [
|
|
467
|
+
`Concurrent changes were seen in this window that no run in it recorded writing: ${unattributed
|
|
468
|
+
.map((change) => change.path)
|
|
469
|
+
.join(", ")}.`,
|
|
470
|
+
"They were left untouched, because a path this window did not write is not this window's to restore.",
|
|
471
|
+
];
|
|
472
|
+
const detail = [...violationDetail, ...concurrentDetail];
|
|
356
473
|
const body: Omit<WriteBoundaryVerdict, "digest"> = {
|
|
357
474
|
version: 1,
|
|
358
475
|
window: input.window,
|
|
@@ -363,11 +480,13 @@ export function enforceWriteBoundary(input: EnforceWriteBoundaryInput): WriteBou
|
|
|
363
480
|
checkedAt: new Date().toISOString(),
|
|
364
481
|
changedPaths: changes.map((change) => change.path),
|
|
365
482
|
violations: violations.map((change) => change.path),
|
|
483
|
+
unattributed: unattributed.map((change) => change.path),
|
|
484
|
+
attributionComplete,
|
|
366
485
|
rolledBack,
|
|
367
486
|
unrecoverable,
|
|
368
487
|
status,
|
|
369
488
|
reason: violations.length === 0 ? null : WRITE_BOUNDARY_VIOLATION_REASON,
|
|
370
|
-
detail,
|
|
489
|
+
detail: detail.length === 0 ? null : detail.join(" "),
|
|
371
490
|
};
|
|
372
491
|
return { ...body, digest: createHash("sha256").update(canonicalVerdict(body), "utf8").digest("hex") };
|
|
373
492
|
}
|
|
@@ -387,16 +506,76 @@ export function assertWriteBoundaryInsideRoot(root: string, allow: WriteBoundary
|
|
|
387
506
|
// path a step is about to create.
|
|
388
507
|
while (probe !== realRoot && !existsSync(probe)) probe = dirname(probe);
|
|
389
508
|
const resolved = realpathSync(probe);
|
|
390
|
-
|
|
509
|
+
// The root is inside itself. The prefix walk above stops at the root when
|
|
510
|
+
// a declared directory does not exist yet, and `pathBoundaryEntryCovers`
|
|
511
|
+
// answers a different question: it asks whether a changed path falls under
|
|
512
|
+
// a declaration, where the directory entry itself is not a change git ever
|
|
513
|
+
// reports. Asking it about the root would refuse every boundary naming a
|
|
514
|
+
// path the step is about to create.
|
|
515
|
+
if (resolved !== realRoot && !pathBoundaryEntryCovers(asDirectoryPathBoundary(realRoot), resolved)) {
|
|
391
516
|
throw new Error(`write boundary: entry '${entry}' resolves to ${resolved}, outside the workspace at ${realRoot}`);
|
|
392
517
|
}
|
|
393
518
|
}
|
|
394
519
|
}
|
|
395
520
|
|
|
396
|
-
/**
|
|
521
|
+
/**
|
|
522
|
+
* Refuse a declaration git will never report on.
|
|
523
|
+
*
|
|
524
|
+
* `git status` does not list an ignored path, so a boundary entry the
|
|
525
|
+
* repository ignores names a region enforcement is blind to: every window over
|
|
526
|
+
* it is certified clean without a single observation, which is a stronger claim
|
|
527
|
+
* than "nothing was checked" and reads as a passing one. That is the same
|
|
528
|
+
* defect `assertWriteBoundaryInsideRoot` refuses for an entry that leaves the
|
|
529
|
+
* workspace through a symlink, so it is refused in the same place and at the
|
|
530
|
+
* same time, before anything runs.
|
|
531
|
+
*
|
|
532
|
+
* A tracked path is reportable whatever the ignore rules say, and git's own
|
|
533
|
+
* index-aware check is what decides that, so this never refuses a path the
|
|
534
|
+
* repository is actually following.
|
|
535
|
+
*/
|
|
536
|
+
export function assertWriteBoundaryVisibleToGit(root: string, allow: WriteBoundary): void {
|
|
537
|
+
// Entry by entry rather than through `normalizeWriteBoundary`, whose 32-entry
|
|
538
|
+
// cap is a per-step declaration limit. A caller may legitimately ask this
|
|
539
|
+
// question about every entry a whole plan declared.
|
|
540
|
+
const entries = [...new Set(allow.map(normalizeWriteBoundaryEntry))].sort();
|
|
541
|
+
if (entries.length === 0) return;
|
|
542
|
+
const absolute = resolve(root);
|
|
543
|
+
let raw: string;
|
|
544
|
+
try {
|
|
545
|
+
raw = git(absolute, ["check-ignore", "-v", "-z", "--stdin"], `${entries.join("\0")}\0`);
|
|
546
|
+
} catch (error) {
|
|
547
|
+
// `check-ignore` exits 1 when nothing matched, which is the common answer
|
|
548
|
+
// and not a failure. Any other exit is a real inability to decide, and
|
|
549
|
+
// enforcement fails closed rather than assuming visibility.
|
|
550
|
+
const status = (error as { status?: number }).status;
|
|
551
|
+
if (status === 1) return;
|
|
552
|
+
throw new Error(
|
|
553
|
+
`write boundary: could not determine whether ${describeWriteBoundary(entries)} is ignored by git in ${absolute}: ${(error as Error).message}`,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
const fields = raw.split("\0");
|
|
557
|
+
const refusals: string[] = [];
|
|
558
|
+
// `-v -z` emits source, line number, pattern, and pathname per match.
|
|
559
|
+
for (let index = 0; index + 3 < fields.length; index += 4) {
|
|
560
|
+
const [source, line, pattern, path] = fields.slice(index, index + 4);
|
|
561
|
+
if (path === undefined || path.length === 0) continue;
|
|
562
|
+
refusals.push(`'${path}' is ignored by ${source}:${line}:${pattern}`);
|
|
563
|
+
}
|
|
564
|
+
if (refusals.length === 0) return;
|
|
565
|
+
throw new Error(
|
|
566
|
+
`write boundary: ${refusals.join(", ")}. git status never reports an ignored path, so a boundary declared over one is verified against nothing and every window would be certified clean without an observation. Declare a path the repository tracks, or stop ignoring this one.`,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Workspace-relative form of a path, or null when it resolves outside. A
|
|
572
|
+
* relative input is taken as already workspace-relative, which is how a
|
|
573
|
+
* recorded write target and an absolute state directory both normalize here.
|
|
574
|
+
*/
|
|
397
575
|
function repoRelative(root: string, path: string): string | null {
|
|
398
|
-
const
|
|
399
|
-
|
|
576
|
+
const base = resolve(root);
|
|
577
|
+
const rel = relative(base, resolve(base, path));
|
|
578
|
+
if (rel.length === 0 || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;
|
|
400
579
|
return rel.split(sep).join("/");
|
|
401
580
|
}
|
|
402
581
|
|
|
@@ -6,7 +6,35 @@
|
|
|
6
6
|
* than silently pass.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import type { RunEnvelope, RunReceipt } from "../../dispatch/types.js";
|
|
12
|
+
import { formatTrustSummary, summarizeTrustStatus } from "../../evidence/trust-projection.js";
|
|
13
|
+
import {
|
|
14
|
+
adaptRunReceiptTrustStatus,
|
|
15
|
+
inspectRunReceiptTrustStatus,
|
|
16
|
+
TRUST_STATUS_AXES,
|
|
17
|
+
} from "../../evidence/trust-status.js";
|
|
18
|
+
|
|
19
|
+
function dispatchScopeMetrics(receipt: RunReceipt): Record<string, string | boolean | number> {
|
|
20
|
+
const scope = receipt.pathScope;
|
|
21
|
+
if (scope === undefined) return {};
|
|
22
|
+
const entries = [...scope.workingContextPaths, ...scope.writeBoundaries];
|
|
23
|
+
const evidence = entries.flatMap((entry) => entry.evidence);
|
|
24
|
+
const count = (source: string): number => evidence.filter((entry) => entry.source === source).length;
|
|
25
|
+
return {
|
|
26
|
+
"dispatch.scope.mode": scope.mode,
|
|
27
|
+
"dispatch.scope.inferredPathCount": entries.filter((entry) =>
|
|
28
|
+
entry.evidence.some((item) => item.provenance === "inferred"),
|
|
29
|
+
).length,
|
|
30
|
+
"dispatch.scope.derivedPathCount": entries.filter((entry) =>
|
|
31
|
+
entry.evidence.some((item) => item.provenance === "derived"),
|
|
32
|
+
).length,
|
|
33
|
+
"dispatch.scope.source.task": count("task"),
|
|
34
|
+
"dispatch.scope.source.briefing": count("briefing"),
|
|
35
|
+
"dispatch.scope.source.writeRoots": count("writeRoots"),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
10
38
|
|
|
11
39
|
/**
|
|
12
40
|
* Extract the sealed RunReceipt that `clio-coder run --agent … --json` prints after
|
|
@@ -53,9 +81,14 @@ function isReceiptShaped(value: unknown): value is RunReceipt {
|
|
|
53
81
|
* mirrors the durable findings summary and is omitted when that summary is
|
|
54
82
|
* absent, so a gate on the metric fails closed instead of inventing a value.
|
|
55
83
|
*/
|
|
56
|
-
export function evidenceMetricsFromReceipt(
|
|
84
|
+
export function evidenceMetricsFromReceipt(
|
|
85
|
+
receipt: RunReceipt,
|
|
86
|
+
options: { envelope?: RunEnvelope | null } = {},
|
|
87
|
+
): Record<string, string | boolean | number> {
|
|
57
88
|
return {
|
|
58
89
|
"evidence.verification": receipt.verification.state,
|
|
90
|
+
...dispatchScopeMetrics(receipt),
|
|
91
|
+
...evidenceTrustMetrics(receipt, options.envelope ?? null),
|
|
59
92
|
...(receipt.findingsSummary === undefined
|
|
60
93
|
? {}
|
|
61
94
|
: { "evidence.firstPassSuccess": receipt.findingsSummary.firstPassSuccess === true }),
|
|
@@ -64,3 +97,47 @@ export function evidenceMetricsFromReceipt(receipt: RunReceipt): Record<string,
|
|
|
64
97
|
...(typeof receipt.costUsd === "number" && Number.isFinite(receipt.costUsd) ? { "cost.usd": receipt.costUsd } : {}),
|
|
65
98
|
};
|
|
66
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The canonical trust status as metrics, so a benchmark table reads the same
|
|
103
|
+
* verdict as every operator surface instead of the raw receipt marker. The
|
|
104
|
+
* receipt is authenticated against its ledger row when the caller can supply
|
|
105
|
+
* one; without a row the canonical model reports the seal unchecked and the
|
|
106
|
+
* receipt-owned axes unobserved, which is the honest answer for a receipt
|
|
107
|
+
* that only ever arrived through stdout.
|
|
108
|
+
*/
|
|
109
|
+
function evidenceTrustMetrics(
|
|
110
|
+
receipt: RunReceipt,
|
|
111
|
+
envelope: RunEnvelope | null,
|
|
112
|
+
): Record<string, string | boolean | number> {
|
|
113
|
+
// A missing row means the seal was never checked, not that it failed:
|
|
114
|
+
// only a row that exists and disagrees can break a seal.
|
|
115
|
+
const status =
|
|
116
|
+
envelope === null ? adaptRunReceiptTrustStatus(receipt) : inspectRunReceiptTrustStatus(receipt, envelope).status;
|
|
117
|
+
const summary = summarizeTrustStatus(status);
|
|
118
|
+
return {
|
|
119
|
+
"evidence.trust.version": summary.version,
|
|
120
|
+
"evidence.trust.verdict": summary.verdict,
|
|
121
|
+
"evidence.trust.summary": formatTrustSummary(status),
|
|
122
|
+
...Object.fromEntries(TRUST_STATUS_AXES.map((axis) => [`evidence.trust.${axis}`, status[axis].state])),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The ledger row a stdout receipt was sealed from, read from the state dir
|
|
128
|
+
* the run wrote to. Null when the ledger or the row cannot be read; the
|
|
129
|
+
* caller then reports the seal unchecked rather than guessing.
|
|
130
|
+
*/
|
|
131
|
+
export function readRunEnvelopeForReceipt(receipt: RunReceipt, stateDir: string): RunEnvelope | null {
|
|
132
|
+
try {
|
|
133
|
+
const parsed: unknown = JSON.parse(readFileSync(join(stateDir, "runs.json"), "utf8"));
|
|
134
|
+
if (!Array.isArray(parsed)) return null;
|
|
135
|
+
const row = parsed.find(
|
|
136
|
+
(entry): entry is RunEnvelope =>
|
|
137
|
+
typeof entry === "object" && entry !== null && (entry as { id?: unknown }).id === receipt.runId,
|
|
138
|
+
);
|
|
139
|
+
return row ?? null;
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { shellQuote } from "../../../core/shell-quote.js";
|
|
2
|
-
import {
|
|
2
|
+
import { clioStateDir } from "../../../core/xdg.js";
|
|
3
|
+
import {
|
|
4
|
+
evidenceMetricsFromReceipt,
|
|
5
|
+
readRunEnvelopeForReceipt,
|
|
6
|
+
receiptFromRunJsonStdout,
|
|
7
|
+
} from "../metrics/evidence.js";
|
|
3
8
|
import { streamInvariantMetrics } from "../metrics/invariants.js";
|
|
4
9
|
import { tokenMetricEntries } from "../metrics/token-stream.js";
|
|
5
10
|
import type { EvalRunnerV2, EvalSuiteTargetV2 } from "../schema/suite.js";
|
|
@@ -35,6 +40,11 @@ export async function runClioRunRunner(
|
|
|
35
40
|
// prints; a runner without a receipt leaves them absent so any gate on
|
|
36
41
|
// them fails closed instead of reading prose labels.
|
|
37
42
|
const receipt = receiptFromRunJsonStdout(result.stdout);
|
|
43
|
+
// The canonical trust metrics authenticate the receipt against the ledger
|
|
44
|
+
// row the child wrote; a stdout receipt on its own only ever reads as an
|
|
45
|
+
// unchecked seal.
|
|
46
|
+
const envelope =
|
|
47
|
+
receipt === null ? null : readRunEnvelopeForReceipt(receipt, env?.CLIO_CODER_STATE_DIR ?? clioStateDir());
|
|
38
48
|
return {
|
|
39
49
|
assignmentId: receipt === null ? null : (receipt.lineage?.rootRunId ?? receipt.runId),
|
|
40
50
|
terminalReceiptDigest: receipt?.integrity.digest ?? null,
|
|
@@ -52,7 +62,7 @@ export async function runClioRunRunner(
|
|
|
52
62
|
"tools.failed": tools.failed,
|
|
53
63
|
"tools.blocked": tools.blocked,
|
|
54
64
|
"verifier.exitCode": result.exitCode,
|
|
55
|
-
...(receipt === null ? {} : evidenceMetricsFromReceipt(receipt)),
|
|
65
|
+
...(receipt === null ? {} : evidenceMetricsFromReceipt(receipt, { envelope })),
|
|
56
66
|
...(receipt === null
|
|
57
67
|
? {}
|
|
58
68
|
: { "evidence.qualityLabel": receipt.quality.typedValidations.length > 0 ? "measured" : "unmeasured" }),
|