harnery 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/harness.d.ts.map +1 -1
- package/dist/commands/harness.js +10 -2
- package/dist/core/harnesses/attest.d.ts +5 -0
- package/dist/core/harnesses/attest.d.ts.map +1 -1
- package/dist/core/harnesses/attest.js +13 -3
- package/dist/core/harnesses/attestation.d.ts +9 -2
- package/dist/core/harnesses/attestation.d.ts.map +1 -1
- package/dist/core/harnesses/attestation.js +6 -3
- package/dist/core/harnesses/bench.d.ts +3 -0
- package/dist/core/harnesses/bench.d.ts.map +1 -1
- package/dist/core/harnesses/bench.js +3 -1
- package/dist/core/harnesses/profiles.d.ts +6 -6
- package/dist/core/harnesses/profiles.js +3 -3
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +2 -1
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +2 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +2 -1
- package/dist/core/workflow/spawn-failure.d.ts +18 -0
- package/dist/core/workflow/spawn-failure.d.ts.map +1 -0
- package/dist/core/workflow/spawn-failure.js +24 -0
- package/package.json +4 -2
- package/src/commands/harness.ts +14 -2
- package/src/core/harnesses/attest.ts +22 -3
- package/src/core/harnesses/attestation.ts +20 -2
- package/src/core/harnesses/bench.ts +6 -1
- package/src/core/harnesses/profiles.ts +3 -3
- package/src/core/workflow/spawn-claude.ts +2 -1
- package/src/core/workflow/spawn-codex.ts +2 -1
- package/src/core/workflow/spawn-cursor.ts +2 -1
- package/src/core/workflow/spawn-failure.ts +28 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/commands/harness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/commands/harness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EACL,KAAK,WAAW,EAEhB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EAIxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAkBjE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAuJhF;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,CAgBhF;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,CAU9E;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAkB7D;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CA6BpE;AAgBD,mFAAmF;AACnF,YAAY,EAAE,WAAW,EAAE,CAAC"}
|
package/dist/commands/harness.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { workflowSubscriptionOnly } from "../core/config.js";
|
|
1
2
|
import { createBuiltinHarnessRegistry, listAttestations, runHarnessAttestation, runHarnessBench, } from "../core/harnesses/index.js";
|
|
2
3
|
const registry = createBuiltinHarnessRegistry();
|
|
3
4
|
export function registerHarnessCommand(program, emit) {
|
|
@@ -66,6 +67,7 @@ export function registerHarnessCommand(program, emit) {
|
|
|
66
67
|
.command("attest [harnesses...]")
|
|
67
68
|
.description("Record what the installed vendor CLIs actually do. Runs one real model turn each; needs --yes.")
|
|
68
69
|
.option("--yes", "Confirm that this spends real vendor tokens")
|
|
70
|
+
.option("--subscription-only", "Scrub API-key vars so the child uses its stored login (repo default via config.jsonc workflow.subscriptionOnly)")
|
|
69
71
|
.option("--timeout <ms>", "Per-harness probe timeout in milliseconds")
|
|
70
72
|
.option("--json", "Machine-readable attestation report")
|
|
71
73
|
.action(async (harnesses, opts) => {
|
|
@@ -87,7 +89,12 @@ export function registerHarnessCommand(program, emit) {
|
|
|
87
89
|
return;
|
|
88
90
|
}
|
|
89
91
|
try {
|
|
90
|
-
const
|
|
92
|
+
const subscriptionOnly = opts.subscriptionOnly || workflowSubscriptionOnly();
|
|
93
|
+
const report = await runHarnessAttestation(registry, {
|
|
94
|
+
harnesses,
|
|
95
|
+
timeoutMs,
|
|
96
|
+
subscriptionOnly,
|
|
97
|
+
});
|
|
91
98
|
if (opts.json) {
|
|
92
99
|
emit.config({ format: "json" });
|
|
93
100
|
emit.data(report);
|
|
@@ -121,9 +128,10 @@ export function registerHarnessCommand(program, emit) {
|
|
|
121
128
|
emit.text("No attestations recorded. Run `harness attest --yes` to create one.");
|
|
122
129
|
return;
|
|
123
130
|
}
|
|
124
|
-
emit.text(renderTable(["HARNESS", "VERSION", "OBSERVED AT", "OBSERVATIONS"], records.map((record) => [
|
|
131
|
+
emit.text(renderTable(["HARNESS", "VERSION", "BILLING", "OBSERVED AT", "OBSERVATIONS"], records.map((record) => [
|
|
125
132
|
record.harness,
|
|
126
133
|
record.binary_version,
|
|
134
|
+
record.subscription_only ? "subscription" : "any",
|
|
127
135
|
record.observed_at,
|
|
128
136
|
Object.entries(record.observations)
|
|
129
137
|
.map(([dimension, support]) => `${dimension}=${support}`)
|
|
@@ -36,6 +36,11 @@ export interface RunHarnessAttestationOptions {
|
|
|
36
36
|
timeoutMs?: number;
|
|
37
37
|
cwd?: string;
|
|
38
38
|
coordRoot?: string;
|
|
39
|
+
/** Scrub API-key vars from the child so it can only use its stored login,
|
|
40
|
+
* matching `workflow run --subscription-only`. The observation is recorded
|
|
41
|
+
* against this mode, because a child that may fall back to an API key can
|
|
42
|
+
* behave differently from one that may not. */
|
|
43
|
+
subscriptionOnly?: boolean;
|
|
39
44
|
/** Test seam and alternate host probe. A null version means unavailable. */
|
|
40
45
|
versionProbe?: (binary: string) => string | null;
|
|
41
46
|
/** Test seam. Defaults to the adapter's production spawner. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attest.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/attest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"attest.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/attest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAQhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE/D;+CAC+C;AAC/C,eAAO,MAAM,kBAAkB,mCAAmC,CAAC;AAEnE,eAAO,MAAM,8BAA8B,SAAU,CAAC;AAqBtD,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,QAAQ,CAAC;AAEnF,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,SAAS,CAAC;IACnB,OAAO,EAAE,kBAAkB,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,iBAAiB,CAAC,CAAC,CAAC;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,OAAO,EAAE,wBAAwB,EAAE,CAAC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB;8DAC0D;IAC1D,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,4BAA4B;IAC3C,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;mDAG+C;IAC/C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACjD,+DAA+D;IAC/D,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IACxF,2DAA2D;IAC3D,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC/C,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,eAAe,EACzB,IAAI,GAAE,4BAAiC,GACtC,OAAO,CAAC,wBAAwB,CAAC,CA4FnC"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* uses, so what gets attested is the path production takes rather than a
|
|
6
6
|
* parallel test rig.
|
|
7
7
|
*/
|
|
8
|
-
import { profileDigest, sealAttestation, writeAttestation } from "./attestation.js";
|
|
8
|
+
import { ATTESTATION_SCHEMA_VERSION, profileDigest, sealAttestation, writeAttestation, } from "./attestation.js";
|
|
9
9
|
import { probeBinaryVersion } from "./bench.js";
|
|
10
10
|
/** Fixed and content-free, so nothing user-supplied can reach the record and
|
|
11
11
|
* the turn stays as cheap as a turn can be. */
|
|
@@ -15,18 +15,26 @@ export const DEFAULT_ATTESTATION_TIMEOUT_MS = 120_000;
|
|
|
15
15
|
* included. Reports are bounded evidence, so the reason is collapsed to one
|
|
16
16
|
* short line and the echoed prompt is removed. */
|
|
17
17
|
const MAX_NOTE_REASON_CHARS = 200;
|
|
18
|
+
/** Keep the TAIL, not the head. A CLI prints its banner, config, and startup
|
|
19
|
+
* warnings first and the reason it actually failed last, so truncating from the
|
|
20
|
+
* front reliably preserves the noise and discards the answer. Learned the hard
|
|
21
|
+
* way: a head-truncated note once surfaced a cosmetic startup warning while
|
|
22
|
+
* hiding the real "out of credits" failure on the final line. */
|
|
18
23
|
function boundedReason(reason) {
|
|
19
24
|
if (!reason)
|
|
20
25
|
return "no error reported";
|
|
21
26
|
const collapsed = reason.split(ATTESTATION_PROMPT).join("<prompt>").replace(/\s+/g, " ").trim();
|
|
27
|
+
if (!collapsed)
|
|
28
|
+
return "no error reported";
|
|
22
29
|
return collapsed.length > MAX_NOTE_REASON_CHARS
|
|
23
|
-
?
|
|
30
|
+
? `…${collapsed.slice(-MAX_NOTE_REASON_CHARS)}`
|
|
24
31
|
: collapsed;
|
|
25
32
|
}
|
|
26
33
|
export async function runHarnessAttestation(registry, opts = {}) {
|
|
27
34
|
const ids = opts.harnesses?.length ? [...opts.harnesses] : registry.ids();
|
|
28
35
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_ATTESTATION_TIMEOUT_MS;
|
|
29
36
|
const cwd = opts.cwd ?? process.cwd();
|
|
37
|
+
const subscriptionOnly = opts.subscriptionOnly === true;
|
|
30
38
|
const versionProbe = opts.versionProbe ?? probeBinaryVersion;
|
|
31
39
|
const now = opts.now ?? (() => new Date());
|
|
32
40
|
const results = [];
|
|
@@ -50,6 +58,7 @@ export async function runHarnessAttestation(registry, opts = {}) {
|
|
|
50
58
|
timeoutMs,
|
|
51
59
|
maxTurns: 1,
|
|
52
60
|
cwd,
|
|
61
|
+
subscriptionOnly,
|
|
53
62
|
});
|
|
54
63
|
}
|
|
55
64
|
catch (error) {
|
|
@@ -80,10 +89,11 @@ export async function runHarnessAttestation(registry, opts = {}) {
|
|
|
80
89
|
cost: result.costUsd !== undefined ? "supported" : "unsupported",
|
|
81
90
|
};
|
|
82
91
|
const record = sealAttestation({
|
|
83
|
-
schema_version:
|
|
92
|
+
schema_version: ATTESTATION_SCHEMA_VERSION,
|
|
84
93
|
harness: id,
|
|
85
94
|
binary_version: binaryVersion,
|
|
86
95
|
profile_digest: profileDigest(adapter.profile),
|
|
96
|
+
subscription_only: subscriptionOnly,
|
|
87
97
|
observed_at: now().toISOString(),
|
|
88
98
|
observations,
|
|
89
99
|
});
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* published.
|
|
13
13
|
*/
|
|
14
14
|
import type { CapabilitySupport, HarnessId, HarnessProfile } from "./types.js";
|
|
15
|
-
export declare const ATTESTATION_SCHEMA_VERSION =
|
|
15
|
+
export declare const ATTESTATION_SCHEMA_VERSION = 2;
|
|
16
16
|
/** Dimensions one minimal live turn can honestly establish. Everything else
|
|
17
17
|
* needs a purpose-built scenario and stays outside the record rather than
|
|
18
18
|
* being guessed at. */
|
|
@@ -27,6 +27,10 @@ export interface HarnessAttestation {
|
|
|
27
27
|
/** Digest of the capability declaration at record time, so an edited
|
|
28
28
|
* declaration also invalidates the record. */
|
|
29
29
|
profile_digest: string;
|
|
30
|
+
/** The billing policy the probe ran under. A child launched with API keys
|
|
31
|
+
* scrubbed can behave differently from one that can fall back to them, so an
|
|
32
|
+
* observation only speaks for the mode it was made in. */
|
|
33
|
+
subscription_only: boolean;
|
|
30
34
|
observed_at: string;
|
|
31
35
|
/** Only what the probe actually saw. A dimension absent from this map was
|
|
32
36
|
* not observed, which is not the same as unsupported. */
|
|
@@ -54,7 +58,7 @@ export declare function readAttestation(harness: HarnessId, opts?: AttestationSt
|
|
|
54
58
|
export declare function validateAttestation(value: unknown, harness?: HarnessId): HarnessAttestation | null;
|
|
55
59
|
/** An attestation speaks only for the vendor version and declaration it was
|
|
56
60
|
* recorded against. */
|
|
57
|
-
export declare function isAttestationCurrent(record: HarnessAttestation | null, binaryVersion: string | null, profile: HarnessProfile): record is HarnessAttestation;
|
|
61
|
+
export declare function isAttestationCurrent(record: HarnessAttestation | null, binaryVersion: string | null, profile: HarnessProfile, subscriptionOnly?: boolean): record is HarnessAttestation;
|
|
58
62
|
export declare function listAttestations(opts?: AttestationStoreOptions): HarnessAttestation[];
|
|
59
63
|
/** Both harness-derived proof inputs, read once, for a workflow run
|
|
60
64
|
* (ADR 0038). Callers inject the result so the engine performs no capability
|
|
@@ -62,6 +66,9 @@ export declare function listAttestations(opts?: AttestationStoreOptions): Harnes
|
|
|
62
66
|
* citation; that absence is not a proof unknown. */
|
|
63
67
|
export declare function harnessProofInputs(profiles: readonly HarnessProfile[], opts: AttestationStoreOptions & {
|
|
64
68
|
versionProbe: (binary: string) => string | null;
|
|
69
|
+
/** Billing policy this run will use, so a record made under the other mode
|
|
70
|
+
* is not cited as if it applied. */
|
|
71
|
+
subscriptionOnly?: boolean;
|
|
65
72
|
}): {
|
|
66
73
|
harnessEvidence: Record<string, {
|
|
67
74
|
toolEvidence: HarnessProfile["capabilities"]["toolEvidence"];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/attestation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAaH,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE/E,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C;;uBAEuB;AACvB,eAAO,MAAM,qBAAqB,6DAA8D,CAAC;AAEjG,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,SAAS,CAAC;IACnB;kFAC8E;IAC9E,cAAc,EAAE,MAAM,CAAC;IACvB;kDAC8C;IAC9C,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB;6DACyD;IACzD,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,iBAAiB,CAAC,CAAC,CAAC;IACtE,kEAAkE;IAClE,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,IAAI,GAAE,uBAA4B,GAAG,MAAM,CAI1E;AAED;wDACwD;AACxD,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAM7D;AAMD,wBAAgB,eAAe,CAC7B,MAAM,EAAE,IAAI,CAAC,kBAAkB,EAAE,eAAe,CAAC,GAChD,kBAAkB,CAEpB;AASD;;aAEa;AACb,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,kBAAkB,EAC1B,IAAI,GAAE,uBAA4B,GACjC,MAAM,CAmBR;AAED;;kEAEkE;AAClE,wBAAgB,eAAe,CAC7B,OAAO,EAAE,SAAS,EAClB,IAAI,GAAE,uBAA4B,GACjC,kBAAkB,GAAG,IAAI,CAc3B;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,SAAS,GAClB,kBAAkB,GAAG,IAAI,CAe3B;AAED;uBACuB;AACvB,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,kBAAkB,GAAG,IAAI,EACjC,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,OAAO,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/attestation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAaH,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE/E,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C;;uBAEuB;AACvB,eAAO,MAAM,qBAAqB,6DAA8D,CAAC;AAEjG,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,SAAS,CAAC;IACnB;kFAC8E;IAC9E,cAAc,EAAE,MAAM,CAAC;IACvB;kDAC8C;IAC9C,cAAc,EAAE,MAAM,CAAC;IACvB;;8DAE0D;IAC1D,iBAAiB,EAAE,OAAO,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB;6DACyD;IACzD,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,iBAAiB,CAAC,CAAC,CAAC;IACtE,kEAAkE;IAClE,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,IAAI,GAAE,uBAA4B,GAAG,MAAM,CAI1E;AAED;wDACwD;AACxD,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAM7D;AAMD,wBAAgB,eAAe,CAC7B,MAAM,EAAE,IAAI,CAAC,kBAAkB,EAAE,eAAe,CAAC,GAChD,kBAAkB,CAEpB;AASD;;aAEa;AACb,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,kBAAkB,EAC1B,IAAI,GAAE,uBAA4B,GACjC,MAAM,CAmBR;AAED;;kEAEkE;AAClE,wBAAgB,eAAe,CAC7B,OAAO,EAAE,SAAS,EAClB,IAAI,GAAE,uBAA4B,GACjC,kBAAkB,GAAG,IAAI,CAc3B;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,SAAS,GAClB,kBAAkB,GAAG,IAAI,CAe3B;AAED;uBACuB;AACvB,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,kBAAkB,GAAG,IAAI,EACjC,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,OAAO,EAAE,cAAc,EACvB,gBAAgB,CAAC,EAAE,OAAO,GACzB,MAAM,IAAI,kBAAkB,CAK9B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,uBAA4B,GAAG,kBAAkB,EAAE,CAczF;AAED;;;oDAGoD;AACpD,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,IAAI,EAAE,uBAAuB,GAAG;IAC9B,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAChD;wCACoC;IACpC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GACA;IACD,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,YAAY,EAAE,cAAc,CAAC,cAAc,CAAC,CAAC,cAAc,CAAC,CAAA;KAAE,CAAC,CAAC;IAClG,mBAAmB,EAAE,MAAM,CACzB,MAAM,EACN;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CACvE,CAAC;CACH,CAoCA"}
|
|
@@ -15,7 +15,7 @@ import { mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFile
|
|
|
15
15
|
import { resolve } from "node:path";
|
|
16
16
|
import { monorepoRoot } from "../agents/coord-client.js";
|
|
17
17
|
import { stableDigest } from "../workflow/durable-record.js";
|
|
18
|
-
export const ATTESTATION_SCHEMA_VERSION =
|
|
18
|
+
export const ATTESTATION_SCHEMA_VERSION = 2;
|
|
19
19
|
/** Dimensions one minimal live turn can honestly establish. Everything else
|
|
20
20
|
* needs a purpose-built scenario and stays outside the record rather than
|
|
21
21
|
* being guessed at. */
|
|
@@ -114,11 +114,13 @@ export function validateAttestation(value, harness) {
|
|
|
114
114
|
}
|
|
115
115
|
/** An attestation speaks only for the vendor version and declaration it was
|
|
116
116
|
* recorded against. */
|
|
117
|
-
export function isAttestationCurrent(record, binaryVersion, profile) {
|
|
117
|
+
export function isAttestationCurrent(record, binaryVersion, profile, subscriptionOnly) {
|
|
118
118
|
if (!record || !binaryVersion)
|
|
119
119
|
return false;
|
|
120
120
|
if (record.binary_version !== binaryVersion)
|
|
121
121
|
return false;
|
|
122
|
+
if (subscriptionOnly !== undefined && record.subscription_only !== subscriptionOnly)
|
|
123
|
+
return false;
|
|
122
124
|
return record.profile_digest === profileDigest(profile);
|
|
123
125
|
}
|
|
124
126
|
export function listAttestations(opts = {}) {
|
|
@@ -156,8 +158,9 @@ export function harnessProofInputs(profiles, opts) {
|
|
|
156
158
|
// No coord root or unreadable store: run unattested rather than fail.
|
|
157
159
|
continue;
|
|
158
160
|
}
|
|
159
|
-
if (!isAttestationCurrent(record, opts.versionProbe(profile.binary), profile))
|
|
161
|
+
if (!isAttestationCurrent(record, opts.versionProbe(profile.binary), profile, opts.subscriptionOnly)) {
|
|
160
162
|
continue;
|
|
163
|
+
}
|
|
161
164
|
harnessAttestations[profile.id] = {
|
|
162
165
|
binary_version: record.binary_version,
|
|
163
166
|
observed_at: record.observed_at,
|
|
@@ -42,6 +42,9 @@ export interface HarnessBenchOptions {
|
|
|
42
42
|
coordRoot?: string;
|
|
43
43
|
/** Test seam. Defaults to reading the attestation store. */
|
|
44
44
|
attestationReader?: (harness: HarnessId) => HarnessAttestation | null;
|
|
45
|
+
/** Only cite attestations recorded under this billing mode. Omitted means
|
|
46
|
+
* any mode is acceptable for reporting purposes. */
|
|
47
|
+
subscriptionOnly?: boolean;
|
|
45
48
|
}
|
|
46
49
|
export declare function runHarnessBench(registry: HarnessRegistry, opts?: HarnessBenchOptions): HarnessBenchReport;
|
|
47
50
|
/** Ask an installed vendor CLI what version it is. Null when it is absent or
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bench.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/bench.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAuB,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EACV,iBAAiB,EAEjB,0BAA0B,EAC1B,SAAS,EACV,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,SAAS,GACT,aAAa,GACb,gBAAgB,GAChB,SAAS,GACT,SAAS,GACT,OAAO,CAAC;AAEZ,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,QAAQ,GAAG,UAAU,GAAG,0BAA0B,CAAC;AAEjG;;;;;;wEAMwE;AACxE,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;AAE7D,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,SAAS,CAAC;IACnB,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,iBAAiB,GAAG,gBAAgB,CAAC;IAC/C,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACtC;sDACkD;IAClD,YAAY,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACzC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,UAAU,CAAC,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACnD,4EAA4E;IAC5E,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACjD;qDACiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,KAAK,kBAAkB,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"bench.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/bench.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAuB,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EACV,iBAAiB,EAEjB,0BAA0B,EAC1B,SAAS,EACV,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,SAAS,GACT,aAAa,GACb,gBAAgB,GAChB,SAAS,GACT,SAAS,GACT,OAAO,CAAC;AAEZ,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,QAAQ,GAAG,UAAU,GAAG,0BAA0B,CAAC;AAEjG;;;;;;wEAMwE;AACxE,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;AAE7D,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,SAAS,CAAC;IACnB,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,iBAAiB,GAAG,gBAAgB,CAAC;IAC/C,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACtC;sDACkD;IAClD,YAAY,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACzC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,UAAU,CAAC,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACnD,4EAA4E;IAC5E,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACjD;qDACiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,KAAK,kBAAkB,GAAG,IAAI,CAAC;IACtE;wDACoD;IACpD,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAkDD,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,eAAe,EACzB,IAAI,GAAE,mBAAwB,GAC7B,kBAAkB,CAwEpB;AA2KD;;kDAEkD;AAClD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIhE"}
|
|
@@ -30,7 +30,9 @@ function loadAttestation(id, version, adapter, opts) {
|
|
|
30
30
|
// No coord root, unreadable store: the bench still runs, just unattested.
|
|
31
31
|
return null;
|
|
32
32
|
}
|
|
33
|
-
return isAttestationCurrent(record, version, adapter.profile)
|
|
33
|
+
return isAttestationCurrent(record, version, adapter.profile, opts.subscriptionOnly)
|
|
34
|
+
? record
|
|
35
|
+
: null;
|
|
34
36
|
}
|
|
35
37
|
/** First version-shaped token in a string, or null when there is none.
|
|
36
38
|
* Tolerates the vendor prefixes and suffixes real CLIs print, so
|
|
@@ -14,8 +14,8 @@ export declare const BUILTIN_HARNESS_PROFILES: {
|
|
|
14
14
|
readonly modelFamily: "claude";
|
|
15
15
|
readonly effortValues: readonly ["low", "medium", "high", "xhigh", "max"];
|
|
16
16
|
readonly verified: {
|
|
17
|
-
readonly date: "2026-07-
|
|
18
|
-
readonly version: "
|
|
17
|
+
readonly date: "2026-07-25";
|
|
18
|
+
readonly version: "2.1.197 (Claude Code)";
|
|
19
19
|
};
|
|
20
20
|
readonly capabilities: HarnessCapabilities;
|
|
21
21
|
};
|
|
@@ -31,8 +31,8 @@ export declare const BUILTIN_HARNESS_PROFILES: {
|
|
|
31
31
|
readonly modelFamily: "gpt";
|
|
32
32
|
readonly effortValues: readonly ["none", "minimal", "low", "medium", "high", "xhigh"];
|
|
33
33
|
readonly verified: {
|
|
34
|
-
readonly date: "2026-07-
|
|
35
|
-
readonly version: "codex-cli 0.
|
|
34
|
+
readonly date: "2026-07-25";
|
|
35
|
+
readonly version: "codex-cli 0.144.5";
|
|
36
36
|
};
|
|
37
37
|
readonly capabilities: HarnessCapabilities;
|
|
38
38
|
};
|
|
@@ -48,8 +48,8 @@ export declare const BUILTIN_HARNESS_PROFILES: {
|
|
|
48
48
|
readonly modelFamily: "multi";
|
|
49
49
|
readonly effortValues: readonly [];
|
|
50
50
|
readonly verified: {
|
|
51
|
-
readonly date: "2026-07-
|
|
52
|
-
readonly version: "2026.07.
|
|
51
|
+
readonly date: "2026-07-25";
|
|
52
|
+
readonly version: "2026.07.23-e383d2b";
|
|
53
53
|
};
|
|
54
54
|
readonly capabilities: HarnessCapabilities;
|
|
55
55
|
};
|
|
@@ -39,7 +39,7 @@ export const BUILTIN_HARNESS_PROFILES = {
|
|
|
39
39
|
authModel: "own-auth",
|
|
40
40
|
modelFamily: "claude",
|
|
41
41
|
effortValues: ["low", "medium", "high", "xhigh", "max"],
|
|
42
|
-
verified: { date: "2026-07-
|
|
42
|
+
verified: { date: "2026-07-25", version: "2.1.197 (Claude Code)" },
|
|
43
43
|
capabilities: capabilities({
|
|
44
44
|
effortSelection: supported("Mapped to `--effort <level>`."),
|
|
45
45
|
maxTurns: supported("Mapped to `--max-turns <n>`."),
|
|
@@ -61,7 +61,7 @@ export const BUILTIN_HARNESS_PROFILES = {
|
|
|
61
61
|
authModel: "own-auth",
|
|
62
62
|
modelFamily: "gpt",
|
|
63
63
|
effortValues: ["none", "minimal", "low", "medium", "high", "xhigh"],
|
|
64
|
-
verified: { date: "2026-07-
|
|
64
|
+
verified: { date: "2026-07-25", version: "codex-cli 0.144.5" },
|
|
65
65
|
capabilities: capabilities({
|
|
66
66
|
effortSelection: supported('Mapped to `-c model_reasoning_effort="<level>"`.'),
|
|
67
67
|
maxTurns: unsupported("codex exec exposes no turn-ceiling flag."),
|
|
@@ -83,7 +83,7 @@ export const BUILTIN_HARNESS_PROFILES = {
|
|
|
83
83
|
authModel: "own-auth",
|
|
84
84
|
modelFamily: "multi",
|
|
85
85
|
effortValues: [],
|
|
86
|
-
verified: { date: "2026-07-
|
|
86
|
+
verified: { date: "2026-07-25", version: "2026.07.23-e383d2b" },
|
|
87
87
|
capabilities: capabilities({
|
|
88
88
|
effortSelection: unsupported("Cursor embeds effort in some parameterized model ids; Harnery does not rewrite model ids."),
|
|
89
89
|
maxTurns: unsupported("cursor-agent exposes no turn-ceiling flag."),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spawn-claude.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-claude.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"spawn-claude.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-claude.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAIjF,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAYrE,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CAc1E;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,gBAAgB,GAAG,WAAW,CAgDxE;AAED,eAAO,MAAM,iBAAiB,EAAE,OAc/B,CAAC"}
|
|
@@ -21,6 +21,7 @@ import { exec } from "../../lib/exec.js";
|
|
|
21
21
|
import { validateHarnessEffort } from "../harnesses/profiles.js";
|
|
22
22
|
import { buildChildEnv } from "./child-env.js";
|
|
23
23
|
import { notFoundError } from "./harnesses.js";
|
|
24
|
+
import { vendorFailureText } from "./spawn-failure.js";
|
|
24
25
|
export function buildClaudeInvocation(req) {
|
|
25
26
|
validateHarnessEffort("claude-code", req.effort);
|
|
26
27
|
const argv = [
|
|
@@ -52,7 +53,7 @@ export function normalizeClaudeResult(raw) {
|
|
|
52
53
|
ok: false,
|
|
53
54
|
text: "",
|
|
54
55
|
durationMs: raw.durationMs,
|
|
55
|
-
error: `claude exited ${raw.exitCode}: ${(raw
|
|
56
|
+
error: `claude exited ${raw.exitCode}: ${vendorFailureText(raw)}`,
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
59
|
let envelope;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spawn-codex.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-codex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"spawn-codex.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-codex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAIjF,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAErE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAgB9F;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,gBAAgB,GAAG,WAAW,CAiBvE;AAED,eAAO,MAAM,YAAY,EAAE,OA2B1B,CAAC"}
|
|
@@ -22,6 +22,7 @@ import { exec } from "../../lib/exec.js";
|
|
|
22
22
|
import { validateHarnessEffort } from "../harnesses/profiles.js";
|
|
23
23
|
import { buildChildEnv } from "./child-env.js";
|
|
24
24
|
import { notFoundError } from "./harnesses.js";
|
|
25
|
+
import { vendorFailureText } from "./spawn-failure.js";
|
|
25
26
|
export function buildCodexInvocation(req, resultFile) {
|
|
26
27
|
validateHarnessEffort("codex", req.effort);
|
|
27
28
|
if (!resultFile)
|
|
@@ -51,7 +52,7 @@ export function normalizeCodexResult(raw) {
|
|
|
51
52
|
ok: false,
|
|
52
53
|
text: "",
|
|
53
54
|
durationMs: raw.durationMs,
|
|
54
|
-
error: `codex exited ${raw.exitCode}: ${(raw
|
|
55
|
+
error: `codex exited ${raw.exitCode}: ${vendorFailureText(raw)}`,
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spawn-cursor.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-cursor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"spawn-cursor.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-cursor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAIjF,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AASrE,gEAAgE;AAChE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB,CAWA;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CAU1E;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,gBAAgB,GAAG,WAAW,CA6BxE;AAED,eAAO,MAAM,aAAa,EAAE,OAe3B,CAAC"}
|
|
@@ -22,6 +22,7 @@ import { exec } from "../../lib/exec.js";
|
|
|
22
22
|
import { validateHarnessEffort } from "../harnesses/profiles.js";
|
|
23
23
|
import { buildChildEnv } from "./child-env.js";
|
|
24
24
|
import { notFoundError } from "./harnesses.js";
|
|
25
|
+
import { vendorFailureText } from "./spawn-failure.js";
|
|
25
26
|
/** Exported for unit tests (no live binary to test against). */
|
|
26
27
|
export function parseCursorOutput(stdout) {
|
|
27
28
|
try {
|
|
@@ -57,7 +58,7 @@ export function normalizeCursorResult(raw) {
|
|
|
57
58
|
ok: false,
|
|
58
59
|
text: "",
|
|
59
60
|
durationMs: raw.durationMs,
|
|
60
|
-
error: `cursor-agent exited ${raw.exitCode}: ${(raw
|
|
61
|
+
error: `cursor-agent exited ${raw.exitCode}: ${vendorFailureText(raw)}`,
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
const parsed = parseCursorOutput(raw.stdout);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared failure-text extraction for the harness spawn adapters.
|
|
3
|
+
*
|
|
4
|
+
* A vendor CLI prints its banner, resolved config, and startup warnings first,
|
|
5
|
+
* and the reason it actually failed last. Every adapter used to keep the FIRST
|
|
6
|
+
* 500 characters of the transcript, which reliably preserved the banner and
|
|
7
|
+
* discarded the answer: a child that died with "your workspace is out of
|
|
8
|
+
* credits" reported a cosmetic startup warning instead.
|
|
9
|
+
*
|
|
10
|
+
* Two rules follow. Keep the tail. Include both streams, because which one
|
|
11
|
+
* carries the reason varies by vendor and by failure.
|
|
12
|
+
*/
|
|
13
|
+
/** Bounded failure text from a finished child, tail-preserving. */
|
|
14
|
+
export declare function vendorFailureText(raw: {
|
|
15
|
+
stdout?: string;
|
|
16
|
+
stderr?: string;
|
|
17
|
+
}, maxChars?: number): string;
|
|
18
|
+
//# sourceMappingURL=spawn-failure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spawn-failure.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-failure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,mEAAmE;AACnE,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EACzC,QAAQ,SAAoB,GAC3B,MAAM,CAQR"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared failure-text extraction for the harness spawn adapters.
|
|
3
|
+
*
|
|
4
|
+
* A vendor CLI prints its banner, resolved config, and startup warnings first,
|
|
5
|
+
* and the reason it actually failed last. Every adapter used to keep the FIRST
|
|
6
|
+
* 500 characters of the transcript, which reliably preserved the banner and
|
|
7
|
+
* discarded the answer: a child that died with "your workspace is out of
|
|
8
|
+
* credits" reported a cosmetic startup warning instead.
|
|
9
|
+
*
|
|
10
|
+
* Two rules follow. Keep the tail. Include both streams, because which one
|
|
11
|
+
* carries the reason varies by vendor and by failure.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_MAX_CHARS = 500;
|
|
14
|
+
/** Bounded failure text from a finished child, tail-preserving. */
|
|
15
|
+
export function vendorFailureText(raw, maxChars = DEFAULT_MAX_CHARS) {
|
|
16
|
+
const parts = [raw.stderr, raw.stdout]
|
|
17
|
+
.map((part) => part?.trim())
|
|
18
|
+
.filter((part) => !!part);
|
|
19
|
+
if (parts.length === 0)
|
|
20
|
+
return "";
|
|
21
|
+
// Both streams, stderr first, so a reason on the unexpected stream survives.
|
|
22
|
+
const combined = parts.join("\n");
|
|
23
|
+
return combined.length > maxChars ? `…${combined.slice(-maxChars)}` : combined;
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harnery",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ryan Kelly",
|
|
@@ -186,7 +186,9 @@
|
|
|
186
186
|
"smoke": "node scripts/smoke-test.mjs",
|
|
187
187
|
"docs:dev": "bun run --cwd docs dev",
|
|
188
188
|
"docs:build": "bun run --cwd docs build",
|
|
189
|
-
"brand:showcase": "node scripts/build-brand-showcase.mjs"
|
|
189
|
+
"brand:showcase": "node scripts/build-brand-showcase.mjs",
|
|
190
|
+
"verify:contracts": "bun run scripts/sync-verified-contracts.ts",
|
|
191
|
+
"verify:contracts:check": "bun run scripts/sync-verified-contracts.ts --check"
|
|
190
192
|
},
|
|
191
193
|
"dependencies": {
|
|
192
194
|
"@mozilla/readability": "^0.6.0",
|
package/src/commands/harness.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import type { EmitContext } from "../commander.ts";
|
|
3
|
+
import { workflowSubscriptionOnly } from "../core/config.ts";
|
|
3
4
|
import {
|
|
4
5
|
type BenchResult,
|
|
5
6
|
createBuiltinHarnessRegistry,
|
|
@@ -22,6 +23,7 @@ interface BenchOpts extends FormatOpts {
|
|
|
22
23
|
interface AttestOpts extends FormatOpts {
|
|
23
24
|
timeout?: string;
|
|
24
25
|
yes?: boolean;
|
|
26
|
+
subscriptionOnly?: boolean;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
const registry = createBuiltinHarnessRegistry();
|
|
@@ -94,6 +96,10 @@ export function registerHarnessCommand(program: Command, emit: EmitContext): voi
|
|
|
94
96
|
"Record what the installed vendor CLIs actually do. Runs one real model turn each; needs --yes.",
|
|
95
97
|
)
|
|
96
98
|
.option("--yes", "Confirm that this spends real vendor tokens")
|
|
99
|
+
.option(
|
|
100
|
+
"--subscription-only",
|
|
101
|
+
"Scrub API-key vars so the child uses its stored login (repo default via config.jsonc workflow.subscriptionOnly)",
|
|
102
|
+
)
|
|
97
103
|
.option("--timeout <ms>", "Per-harness probe timeout in milliseconds")
|
|
98
104
|
.option("--json", "Machine-readable attestation report")
|
|
99
105
|
.action(async (harnesses: string[], opts: AttestOpts) => {
|
|
@@ -116,7 +122,12 @@ export function registerHarnessCommand(program: Command, emit: EmitContext): voi
|
|
|
116
122
|
return;
|
|
117
123
|
}
|
|
118
124
|
try {
|
|
119
|
-
const
|
|
125
|
+
const subscriptionOnly = opts.subscriptionOnly || workflowSubscriptionOnly();
|
|
126
|
+
const report = await runHarnessAttestation(registry, {
|
|
127
|
+
harnesses,
|
|
128
|
+
timeoutMs,
|
|
129
|
+
subscriptionOnly,
|
|
130
|
+
});
|
|
120
131
|
if (opts.json) {
|
|
121
132
|
emit.config({ format: "json" });
|
|
122
133
|
emit.data(report);
|
|
@@ -151,10 +162,11 @@ export function registerHarnessCommand(program: Command, emit: EmitContext): voi
|
|
|
151
162
|
}
|
|
152
163
|
emit.text(
|
|
153
164
|
renderTable(
|
|
154
|
-
["HARNESS", "VERSION", "OBSERVED AT", "OBSERVATIONS"],
|
|
165
|
+
["HARNESS", "VERSION", "BILLING", "OBSERVED AT", "OBSERVATIONS"],
|
|
155
166
|
records.map((record) => [
|
|
156
167
|
record.harness,
|
|
157
168
|
record.binary_version,
|
|
169
|
+
record.subscription_only ? "subscription" : "any",
|
|
158
170
|
record.observed_at,
|
|
159
171
|
Object.entries(record.observations)
|
|
160
172
|
.map(([dimension, support]) => `${dimension}=${support}`)
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
import type { SpawnResult } from "../workflow/types.ts";
|
|
10
10
|
import type { AttestableDimension, HarnessAttestation } from "./attestation.ts";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
ATTESTATION_SCHEMA_VERSION,
|
|
13
|
+
profileDigest,
|
|
14
|
+
sealAttestation,
|
|
15
|
+
writeAttestation,
|
|
16
|
+
} from "./attestation.ts";
|
|
12
17
|
import { probeBinaryVersion } from "./bench.ts";
|
|
13
18
|
import type { HarnessRegistry } from "./registry.ts";
|
|
14
19
|
import type { CapabilitySupport, HarnessId } from "./types.ts";
|
|
@@ -24,11 +29,17 @@ export const DEFAULT_ATTESTATION_TIMEOUT_MS = 120_000;
|
|
|
24
29
|
* short line and the echoed prompt is removed. */
|
|
25
30
|
const MAX_NOTE_REASON_CHARS = 200;
|
|
26
31
|
|
|
32
|
+
/** Keep the TAIL, not the head. A CLI prints its banner, config, and startup
|
|
33
|
+
* warnings first and the reason it actually failed last, so truncating from the
|
|
34
|
+
* front reliably preserves the noise and discards the answer. Learned the hard
|
|
35
|
+
* way: a head-truncated note once surfaced a cosmetic startup warning while
|
|
36
|
+
* hiding the real "out of credits" failure on the final line. */
|
|
27
37
|
function boundedReason(reason: string | undefined): string {
|
|
28
38
|
if (!reason) return "no error reported";
|
|
29
39
|
const collapsed = reason.split(ATTESTATION_PROMPT).join("<prompt>").replace(/\s+/g, " ").trim();
|
|
40
|
+
if (!collapsed) return "no error reported";
|
|
30
41
|
return collapsed.length > MAX_NOTE_REASON_CHARS
|
|
31
|
-
?
|
|
42
|
+
? `…${collapsed.slice(-MAX_NOTE_REASON_CHARS)}`
|
|
32
43
|
: collapsed;
|
|
33
44
|
}
|
|
34
45
|
|
|
@@ -58,6 +69,11 @@ export interface RunHarnessAttestationOptions {
|
|
|
58
69
|
timeoutMs?: number;
|
|
59
70
|
cwd?: string;
|
|
60
71
|
coordRoot?: string;
|
|
72
|
+
/** Scrub API-key vars from the child so it can only use its stored login,
|
|
73
|
+
* matching `workflow run --subscription-only`. The observation is recorded
|
|
74
|
+
* against this mode, because a child that may fall back to an API key can
|
|
75
|
+
* behave differently from one that may not. */
|
|
76
|
+
subscriptionOnly?: boolean;
|
|
61
77
|
/** Test seam and alternate host probe. A null version means unavailable. */
|
|
62
78
|
versionProbe?: (binary: string) => string | null;
|
|
63
79
|
/** Test seam. Defaults to the adapter's production spawner. */
|
|
@@ -74,6 +90,7 @@ export async function runHarnessAttestation(
|
|
|
74
90
|
const ids = opts.harnesses?.length ? [...opts.harnesses] : registry.ids();
|
|
75
91
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_ATTESTATION_TIMEOUT_MS;
|
|
76
92
|
const cwd = opts.cwd ?? process.cwd();
|
|
93
|
+
const subscriptionOnly = opts.subscriptionOnly === true;
|
|
77
94
|
const versionProbe = opts.versionProbe ?? probeBinaryVersion;
|
|
78
95
|
const now = opts.now ?? (() => new Date());
|
|
79
96
|
const results: HarnessAttestationResult[] = [];
|
|
@@ -99,6 +116,7 @@ export async function runHarnessAttestation(
|
|
|
99
116
|
timeoutMs,
|
|
100
117
|
maxTurns: 1,
|
|
101
118
|
cwd,
|
|
119
|
+
subscriptionOnly,
|
|
102
120
|
});
|
|
103
121
|
} catch (error) {
|
|
104
122
|
results.push({
|
|
@@ -131,10 +149,11 @@ export async function runHarnessAttestation(
|
|
|
131
149
|
};
|
|
132
150
|
|
|
133
151
|
const record = sealAttestation({
|
|
134
|
-
schema_version:
|
|
152
|
+
schema_version: ATTESTATION_SCHEMA_VERSION,
|
|
135
153
|
harness: id,
|
|
136
154
|
binary_version: binaryVersion,
|
|
137
155
|
profile_digest: profileDigest(adapter.profile),
|
|
156
|
+
subscription_only: subscriptionOnly,
|
|
138
157
|
observed_at: now().toISOString(),
|
|
139
158
|
observations,
|
|
140
159
|
});
|
|
@@ -25,7 +25,7 @@ import { monorepoRoot } from "../agents/coord-client.ts";
|
|
|
25
25
|
import { stableDigest } from "../workflow/durable-record.ts";
|
|
26
26
|
import type { CapabilitySupport, HarnessId, HarnessProfile } from "./types.ts";
|
|
27
27
|
|
|
28
|
-
export const ATTESTATION_SCHEMA_VERSION =
|
|
28
|
+
export const ATTESTATION_SCHEMA_VERSION = 2;
|
|
29
29
|
|
|
30
30
|
/** Dimensions one minimal live turn can honestly establish. Everything else
|
|
31
31
|
* needs a purpose-built scenario and stays outside the record rather than
|
|
@@ -43,6 +43,10 @@ export interface HarnessAttestation {
|
|
|
43
43
|
/** Digest of the capability declaration at record time, so an edited
|
|
44
44
|
* declaration also invalidates the record. */
|
|
45
45
|
profile_digest: string;
|
|
46
|
+
/** The billing policy the probe ran under. A child launched with API keys
|
|
47
|
+
* scrubbed can behave differently from one that can fall back to them, so an
|
|
48
|
+
* observation only speaks for the mode it was made in. */
|
|
49
|
+
subscription_only: boolean;
|
|
46
50
|
observed_at: string;
|
|
47
51
|
/** Only what the probe actually saw. A dimension absent from this map was
|
|
48
52
|
* not observed, which is not the same as unsupported. */
|
|
@@ -164,9 +168,11 @@ export function isAttestationCurrent(
|
|
|
164
168
|
record: HarnessAttestation | null,
|
|
165
169
|
binaryVersion: string | null,
|
|
166
170
|
profile: HarnessProfile,
|
|
171
|
+
subscriptionOnly?: boolean,
|
|
167
172
|
): record is HarnessAttestation {
|
|
168
173
|
if (!record || !binaryVersion) return false;
|
|
169
174
|
if (record.binary_version !== binaryVersion) return false;
|
|
175
|
+
if (subscriptionOnly !== undefined && record.subscription_only !== subscriptionOnly) return false;
|
|
170
176
|
return record.profile_digest === profileDigest(profile);
|
|
171
177
|
}
|
|
172
178
|
|
|
@@ -194,6 +200,9 @@ export function harnessProofInputs(
|
|
|
194
200
|
profiles: readonly HarnessProfile[],
|
|
195
201
|
opts: AttestationStoreOptions & {
|
|
196
202
|
versionProbe: (binary: string) => string | null;
|
|
203
|
+
/** Billing policy this run will use, so a record made under the other mode
|
|
204
|
+
* is not cited as if it applied. */
|
|
205
|
+
subscriptionOnly?: boolean;
|
|
197
206
|
},
|
|
198
207
|
): {
|
|
199
208
|
harnessEvidence: Record<string, { toolEvidence: HarnessProfile["capabilities"]["toolEvidence"] }>;
|
|
@@ -220,7 +229,16 @@ export function harnessProofInputs(
|
|
|
220
229
|
// No coord root or unreadable store: run unattested rather than fail.
|
|
221
230
|
continue;
|
|
222
231
|
}
|
|
223
|
-
if (
|
|
232
|
+
if (
|
|
233
|
+
!isAttestationCurrent(
|
|
234
|
+
record,
|
|
235
|
+
opts.versionProbe(profile.binary),
|
|
236
|
+
profile,
|
|
237
|
+
opts.subscriptionOnly,
|
|
238
|
+
)
|
|
239
|
+
) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
224
242
|
harnessAttestations[profile.id] = {
|
|
225
243
|
binary_version: record.binary_version,
|
|
226
244
|
observed_at: record.observed_at,
|
|
@@ -65,6 +65,9 @@ export interface HarnessBenchOptions {
|
|
|
65
65
|
coordRoot?: string;
|
|
66
66
|
/** Test seam. Defaults to reading the attestation store. */
|
|
67
67
|
attestationReader?: (harness: HarnessId) => HarnessAttestation | null;
|
|
68
|
+
/** Only cite attestations recorded under this billing mode. Omitted means
|
|
69
|
+
* any mode is acceptable for reporting purposes. */
|
|
70
|
+
subscriptionOnly?: boolean;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
const EMPTY_SUMMARY: Record<BenchVerdict, number> = {
|
|
@@ -101,7 +104,9 @@ function loadAttestation(
|
|
|
101
104
|
// No coord root, unreadable store: the bench still runs, just unattested.
|
|
102
105
|
return null;
|
|
103
106
|
}
|
|
104
|
-
return isAttestationCurrent(record, version, adapter.profile)
|
|
107
|
+
return isAttestationCurrent(record, version, adapter.profile, opts.subscriptionOnly)
|
|
108
|
+
? record
|
|
109
|
+
: null;
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
/** First version-shaped token in a string, or null when there is none.
|
|
@@ -43,7 +43,7 @@ export const BUILTIN_HARNESS_PROFILES = {
|
|
|
43
43
|
authModel: "own-auth",
|
|
44
44
|
modelFamily: "claude",
|
|
45
45
|
effortValues: ["low", "medium", "high", "xhigh", "max"],
|
|
46
|
-
verified: { date: "2026-07-
|
|
46
|
+
verified: { date: "2026-07-25", version: "2.1.197 (Claude Code)" },
|
|
47
47
|
capabilities: capabilities({
|
|
48
48
|
effortSelection: supported("Mapped to `--effort <level>`."),
|
|
49
49
|
maxTurns: supported("Mapped to `--max-turns <n>`."),
|
|
@@ -69,7 +69,7 @@ export const BUILTIN_HARNESS_PROFILES = {
|
|
|
69
69
|
authModel: "own-auth",
|
|
70
70
|
modelFamily: "gpt",
|
|
71
71
|
effortValues: ["none", "minimal", "low", "medium", "high", "xhigh"],
|
|
72
|
-
verified: { date: "2026-07-
|
|
72
|
+
verified: { date: "2026-07-25", version: "codex-cli 0.144.5" },
|
|
73
73
|
capabilities: capabilities({
|
|
74
74
|
effortSelection: supported('Mapped to `-c model_reasoning_effort="<level>"`.'),
|
|
75
75
|
maxTurns: unsupported("codex exec exposes no turn-ceiling flag."),
|
|
@@ -95,7 +95,7 @@ export const BUILTIN_HARNESS_PROFILES = {
|
|
|
95
95
|
authModel: "own-auth",
|
|
96
96
|
modelFamily: "multi",
|
|
97
97
|
effortValues: [],
|
|
98
|
-
verified: { date: "2026-07-
|
|
98
|
+
verified: { date: "2026-07-25", version: "2026.07.23-e383d2b" },
|
|
99
99
|
capabilities: capabilities({
|
|
100
100
|
effortSelection: unsupported(
|
|
101
101
|
"Cursor embeds effort in some parameterized model ids; Harnery does not rewrite model ids.",
|
|
@@ -23,6 +23,7 @@ import { validateHarnessEffort } from "../harnesses/profiles.ts";
|
|
|
23
23
|
import type { HarnessInvocation, HarnessRawResult } from "../harnesses/types.ts";
|
|
24
24
|
import { buildChildEnv } from "./child-env.ts";
|
|
25
25
|
import { notFoundError } from "./harnesses.ts";
|
|
26
|
+
import { vendorFailureText } from "./spawn-failure.ts";
|
|
26
27
|
import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
|
|
27
28
|
|
|
28
29
|
interface ClaudeEnvelope {
|
|
@@ -65,7 +66,7 @@ export function normalizeClaudeResult(raw: HarnessRawResult): SpawnResult {
|
|
|
65
66
|
ok: false,
|
|
66
67
|
text: "",
|
|
67
68
|
durationMs: raw.durationMs,
|
|
68
|
-
error: `claude exited ${raw.exitCode}: ${(raw
|
|
69
|
+
error: `claude exited ${raw.exitCode}: ${vendorFailureText(raw)}`,
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -24,6 +24,7 @@ import { validateHarnessEffort } from "../harnesses/profiles.ts";
|
|
|
24
24
|
import type { HarnessInvocation, HarnessRawResult } from "../harnesses/types.ts";
|
|
25
25
|
import { buildChildEnv } from "./child-env.ts";
|
|
26
26
|
import { notFoundError } from "./harnesses.ts";
|
|
27
|
+
import { vendorFailureText } from "./spawn-failure.ts";
|
|
27
28
|
import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
|
|
28
29
|
|
|
29
30
|
export function buildCodexInvocation(req: SpawnRequest, resultFile?: string): HarnessInvocation {
|
|
@@ -53,7 +54,7 @@ export function normalizeCodexResult(raw: HarnessRawResult): SpawnResult {
|
|
|
53
54
|
ok: false,
|
|
54
55
|
text: "",
|
|
55
56
|
durationMs: raw.durationMs,
|
|
56
|
-
error: `codex exited ${raw.exitCode}: ${(raw
|
|
57
|
+
error: `codex exited ${raw.exitCode}: ${vendorFailureText(raw)}`,
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
return {
|
|
@@ -24,6 +24,7 @@ import { validateHarnessEffort } from "../harnesses/profiles.ts";
|
|
|
24
24
|
import type { HarnessInvocation, HarnessRawResult } from "../harnesses/types.ts";
|
|
25
25
|
import { buildChildEnv } from "./child-env.ts";
|
|
26
26
|
import { notFoundError } from "./harnesses.ts";
|
|
27
|
+
import { vendorFailureText } from "./spawn-failure.ts";
|
|
27
28
|
import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
|
|
28
29
|
|
|
29
30
|
interface CursorEnvelope {
|
|
@@ -72,7 +73,7 @@ export function normalizeCursorResult(raw: HarnessRawResult): SpawnResult {
|
|
|
72
73
|
ok: false,
|
|
73
74
|
text: "",
|
|
74
75
|
durationMs: raw.durationMs,
|
|
75
|
-
error: `cursor-agent exited ${raw.exitCode}: ${(raw
|
|
76
|
+
error: `cursor-agent exited ${raw.exitCode}: ${vendorFailureText(raw)}`,
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared failure-text extraction for the harness spawn adapters.
|
|
3
|
+
*
|
|
4
|
+
* A vendor CLI prints its banner, resolved config, and startup warnings first,
|
|
5
|
+
* and the reason it actually failed last. Every adapter used to keep the FIRST
|
|
6
|
+
* 500 characters of the transcript, which reliably preserved the banner and
|
|
7
|
+
* discarded the answer: a child that died with "your workspace is out of
|
|
8
|
+
* credits" reported a cosmetic startup warning instead.
|
|
9
|
+
*
|
|
10
|
+
* Two rules follow. Keep the tail. Include both streams, because which one
|
|
11
|
+
* carries the reason varies by vendor and by failure.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const DEFAULT_MAX_CHARS = 500;
|
|
15
|
+
|
|
16
|
+
/** Bounded failure text from a finished child, tail-preserving. */
|
|
17
|
+
export function vendorFailureText(
|
|
18
|
+
raw: { stdout?: string; stderr?: string },
|
|
19
|
+
maxChars = DEFAULT_MAX_CHARS,
|
|
20
|
+
): string {
|
|
21
|
+
const parts = [raw.stderr, raw.stdout]
|
|
22
|
+
.map((part) => part?.trim())
|
|
23
|
+
.filter((part): part is string => !!part);
|
|
24
|
+
if (parts.length === 0) return "";
|
|
25
|
+
// Both streams, stderr first, so a reason on the unexpected stream survives.
|
|
26
|
+
const combined = parts.join("\n");
|
|
27
|
+
return combined.length > maxChars ? `…${combined.slice(-maxChars)}` : combined;
|
|
28
|
+
}
|