tickmarkr 2.8.7 → 2.8.9
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/README.md +22 -0
- package/dist/cli/commands/compile.js +1 -1
- package/dist/cli/commands/report.js +21 -12
- package/dist/cli/commands/status.js +14 -2
- package/dist/cli/commands/verify.js +29 -5
- package/dist/compile/collateral.d.ts +3 -1
- package/dist/compile/collateral.js +8 -16
- package/dist/compile/native.js +7 -2
- package/dist/gates/acceptance.d.ts +8 -0
- package/dist/gates/acceptance.js +54 -37
- package/dist/gates/run-gates.js +31 -22
- package/dist/report/bundle.d.ts +5 -2
- package/dist/report/bundle.js +11 -6
- package/dist/report/compare.d.ts +9 -0
- package/dist/report/compare.js +58 -40
- package/dist/report/operator-record.js +4 -3
- package/dist/run/daemon.d.ts +7 -3
- package/dist/run/daemon.js +221 -42
- package/dist/run/git.d.ts +7 -2
- package/dist/run/git.js +37 -4
- package/dist/run/journal.js +11 -2
- package/dist/run/repair-disposition.d.ts +1 -1
- package/dist/run/repair-disposition.js +33 -2
- package/dist/run/wall-budget.js +8 -4
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +58 -3
- package/skills/tickmarkr-loop/SKILL.md +59 -5
- package/skills/tickmarkr-overseer/SKILL.md +134 -34
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +145 -18
package/dist/gates/run-gates.js
CHANGED
|
@@ -1147,7 +1147,21 @@ export async function runGates(task, ctx) {
|
|
|
1147
1147
|
invocations: [],
|
|
1148
1148
|
};
|
|
1149
1149
|
}
|
|
1150
|
-
|
|
1150
|
+
// Queue row 34(a) (D-2134 R5): resolve every lifetime author through both role pools.
|
|
1151
|
+
const channels = [...(ctx.judgeChannels ?? []), ...ctx.channels];
|
|
1152
|
+
const authors = ctx.carriedAuthors?.length ? ctx.carriedAuthors : [channelKey(ctx.author)];
|
|
1153
|
+
const authorVendors = carriedAuthorVendors(channels, authors);
|
|
1154
|
+
const independent = (seat) => !!seat.vendor
|
|
1155
|
+
&& !authorVendors.has(seat.vendor) && !authors.includes(channelKey(seat));
|
|
1156
|
+
const judgePool = () => (ctx.judgeChannels ?? []).filter((c) => disallowedBy(c, ctx.cfg.routing, "judge") === null);
|
|
1157
|
+
const rankJudges = (pool) => [...pool]
|
|
1158
|
+
.sort((x, y) => TIER_RANK[y.tier] - TIER_RANK[x.tier] || marginalCostRank(x) - marginalCostRank(y));
|
|
1159
|
+
const pickJudge = (pool, adapter) => rankJudges(pool.filter((c) => c.adapter !== adapter))[0] ?? rankJudges(pool)[0];
|
|
1160
|
+
const configured = channels.find((c) => channelKey(c) === channelKey(ctx.cfg.judge));
|
|
1161
|
+
const primary = authors.includes(channelKey(ctx.cfg.judge)) || (configured && authorVendors.has(configured.vendor))
|
|
1162
|
+
? pickJudge(judgePool().filter(independent), ctx.cfg.judge.adapter) ?? ctx.cfg.judge
|
|
1163
|
+
: ctx.cfg.judge;
|
|
1164
|
+
const judgeAdapter = getAdapter(primary.adapter, ctx.adapters);
|
|
1151
1165
|
const jvia = via
|
|
1152
1166
|
? { driver: via.driver, keep: via.keep, onSlot: via.onSlot, name: via.nameFor("judge", judgeAdapter.id), label: via.labelFor("judge") }
|
|
1153
1167
|
: undefined;
|
|
@@ -1181,11 +1195,11 @@ export async function runGates(task, ctx) {
|
|
|
1181
1195
|
...(unparseable ? { transcript: output } : {}),
|
|
1182
1196
|
});
|
|
1183
1197
|
}
|
|
1184
|
-
|
|
1198
|
+
const result = captured.value;
|
|
1199
|
+
const seat = channels.find((c) => channelKey(c) === result.meta?.judge);
|
|
1200
|
+
return typeof result.meta?.judge === "string" && result.meta.unparseable !== true && (!seat || !independent(seat))
|
|
1201
|
+
? { ...result, meta: { ...result.meta, judgeIndependent: false } } : result;
|
|
1185
1202
|
};
|
|
1186
|
-
const judgePool = () => (ctx.judgeChannels ?? []).filter((c) => disallowedBy(c, ctx.cfg.routing, "judge") === null);
|
|
1187
|
-
const rankJudges = (pool) => [...pool]
|
|
1188
|
-
.sort((x, y) => TIER_RANK[y.tier] - TIER_RANK[x.tier] || marginalCostRank(x) - marginalCostRank(y));
|
|
1189
1203
|
// OBS-1151 (+add.1): the fresh judgment always stands on its own reading — a prior PASS is never
|
|
1190
1204
|
// reused. Only a criterion that REVERSES the newest prior ruling on the same subject key over
|
|
1191
1205
|
// identical cited blobs needs a second, distinct judge; agreement stands (a sound FAIL included),
|
|
@@ -1210,16 +1224,18 @@ export async function runGates(task, ctx) {
|
|
|
1210
1224
|
if (!disputed.length)
|
|
1211
1225
|
return stamped;
|
|
1212
1226
|
await ctx.onGate?.({ phase: "note", gate: "acceptance", name: "judge-disagreement", payload: { primary, commit, disputed } });
|
|
1227
|
+
let notIndependent = fresh.meta?.judgeIndependent === false;
|
|
1228
|
+
const independence = () => notIndependent ? { judgeIndependent: false } : {};
|
|
1213
1229
|
const park = (why, adjudicator) => ({
|
|
1214
1230
|
gate: "acceptance", pass: false,
|
|
1215
1231
|
details: `judge disagreement on ${disputed.map((d) => d.id).join(", ")}: ${primary} reverses an earlier ruling over identical cited blobs (${[...new Set(disputed.flatMap((d) => d.paths))].join(", ")}) — ${why}; parked for an operator ruling, no worker charge`,
|
|
1216
|
-
meta: { classification: "infra", infra: true, retryable: false, cause: "judge-disagreement", judge: primary,
|
|
1232
|
+
meta: { classification: "infra", infra: true, retryable: false, cause: "judge-disagreement", judge: primary, ...independence(),
|
|
1217
1233
|
judgeDisagreement: { primary, ...(adjudicator ? { adjudicator } : {}), disputed, outcome: why } },
|
|
1218
1234
|
});
|
|
1219
1235
|
const primaryAdapter = primary.slice(0, primary.indexOf(":"));
|
|
1220
1236
|
// One DISTINCT seat: never the primary channel, a different adapter when the pool has one.
|
|
1221
1237
|
const pool = judgePool().filter((c) => channelKey(c) !== primary);
|
|
1222
|
-
const seat =
|
|
1238
|
+
const seat = pickJudge(pool.filter(independent), primaryAdapter) ?? pickJudge(pool, primaryAdapter);
|
|
1223
1239
|
if (!seat)
|
|
1224
1240
|
return park("no distinct eligible judge is available");
|
|
1225
1241
|
const adjudicator = channelKey(seat);
|
|
@@ -1230,13 +1246,14 @@ export async function runGates(task, ctx) {
|
|
|
1230
1246
|
const subjectPark = (leg, found, ran) => ({
|
|
1231
1247
|
gate: "acceptance", pass: false,
|
|
1232
1248
|
details: `the adjudication subject ${SUBJECT_LEG_TEXT[leg]}: sampled ${commit.slice(0, 12)}, found ${found.slice(0, 12)} (${leg}); parked for an operator ruling, no worker charge`,
|
|
1233
|
-
meta: { classification: "infra", infra: true, retryable: false, cause: "subject-moved", judge: primary,
|
|
1249
|
+
meta: { classification: "infra", infra: true, retryable: false, cause: "subject-moved", judge: primary, ...independence(),
|
|
1234
1250
|
adjudication: { primary, ...(ran ? { adjudicator: ran } : {}), sampled: commit, found, leg } },
|
|
1235
1251
|
});
|
|
1236
1252
|
const s0 = await openSubjectWitness(ctx.worktree, commit);
|
|
1237
1253
|
if ("leg" in s0)
|
|
1238
1254
|
return subjectPark(s0.leg, s0.leg === "unreadable" ? "unreadable" : commit);
|
|
1239
1255
|
const second = await invokeJudge(seatAdapter, seat.model, seatVia, configuredEffort(ctx.cfg, seat));
|
|
1256
|
+
notIndependent ||= second.meta?.judgeIndependent === false;
|
|
1240
1257
|
const changed = await adjudicationSubjectChange(ctx.worktree, commit, s0);
|
|
1241
1258
|
if (changed)
|
|
1242
1259
|
return subjectPark(changed.leg, changed.found, adjudicator);
|
|
@@ -1248,10 +1265,10 @@ export async function runGates(task, ctx) {
|
|
|
1248
1265
|
const agreed = disputed.every((d) => rulings.some((r) => r.id === d.id && r.met === d.met && r.paths.length > 0));
|
|
1249
1266
|
if (!agreed)
|
|
1250
1267
|
return park("the adjudicating judge split from the fresh ruling", adjudicator);
|
|
1251
|
-
return { ...stamped, meta: { ...stamped.meta, adjudication: { primary, adjudicator, criteria: disputed.map((d) => d.id), agreed: true } } };
|
|
1268
|
+
return { ...stamped, meta: { ...stamped.meta, ...independence(), adjudication: { primary, adjudicator, criteria: disputed.map((d) => d.id), agreed: true } } };
|
|
1252
1269
|
};
|
|
1253
1270
|
// OBS-1182: every judge seat launches at its OWN configured effort, never the worker's.
|
|
1254
|
-
let a = await invokeJudge(judgeAdapter,
|
|
1271
|
+
let a = await invokeJudge(judgeAdapter, primary.model, jvia, configuredEffort(ctx.cfg, primary));
|
|
1255
1272
|
// GATE-09: an unparseable judge verdict retries the JUDGE exactly once on a failover channel — never
|
|
1256
1273
|
// the worker (run-20260711-185020 P43-03 L70-72 billed a judge flake as a worker attempt). The flaked
|
|
1257
1274
|
// first verdict NEVER enters results (no false gate-result journal event, no operator notify, no stale
|
|
@@ -1268,18 +1285,10 @@ export async function runGates(task, ctx) {
|
|
|
1268
1285
|
if (!cancelled && a.meta?.unparseable === true && typeof a.meta.judge === "string") {
|
|
1269
1286
|
const flakedKey = a.meta.judge;
|
|
1270
1287
|
const flakedAdapter = flakedKey.slice(0, flakedKey.indexOf(":"));
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
// v1.87 T2: the failover seat obeys the same policy the primary judge just passed — a denied
|
|
1276
|
-
// channel is refused here too, never reached by falling through the exclusion arms below.
|
|
1277
|
-
const judgePool = (ctx.judgeChannels ?? []).filter((c) => disallowedBy(c, ctx.cfg.routing, "judge") === null);
|
|
1278
|
-
const crossAdapter = pick(judgePool.filter((c) => c.adapter !== flakedAdapter));
|
|
1279
|
-
const sameAdapter = pick(judgePool.filter((c) => c.adapter === flakedAdapter && channelKey(c) !== flakedKey));
|
|
1280
|
-
// Prefer a different adapter; if the fleet only has one adapter, retry on a different channel of
|
|
1281
|
-
// that adapter; if the fleet has only one channel, fall back to the original judge config.
|
|
1282
|
-
const retry = crossAdapter ?? sameAdapter ?? judgeSeat;
|
|
1288
|
+
// Queue row 34(a) (D-2134 R5): keep GATE-09's exclusions and fallback when independence is unavailable.
|
|
1289
|
+
const pool = judgePool().filter((c) => channelKey(c) !== flakedKey);
|
|
1290
|
+
const retry = pickJudge(pool.filter(independent), flakedAdapter)
|
|
1291
|
+
?? pickJudge(pool, flakedAdapter) ?? judgeSeat;
|
|
1283
1292
|
const retryAdapter = getAdapter(retry.adapter, ctx.adapters);
|
|
1284
1293
|
const retryJvia = via
|
|
1285
1294
|
// unconditional -r1 suffix: under keepPanes:forever a same-channel retry cannot collide with the
|
package/dist/report/bundle.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { EvidenceCitation } from "../gates/acceptance.js";
|
|
2
2
|
import type { RunEnvironment } from "../run/environment.js";
|
|
3
3
|
import type { JournalEvent } from "../run/journal.js";
|
|
4
|
-
|
|
4
|
+
import { type SessionEnvironment } from "./compare.js";
|
|
5
|
+
export declare const BUNDLE_SCHEMA_VERSION = 3;
|
|
5
6
|
export declare const gateDeclined: (data: Record<string, unknown>) => boolean;
|
|
6
7
|
export declare const KNOWN_LIMITS: readonly string[];
|
|
7
8
|
export type BundleEvidence = string | EvidenceCitation;
|
|
@@ -18,6 +19,7 @@ export interface BundleGateResult {
|
|
|
18
19
|
/** T11: true when the gate declined to run (journaled skipped:true, e.g. review below its
|
|
19
20
|
* complexity threshold) — neither a pass nor a fail; the details carry the reason. */
|
|
20
21
|
declined?: boolean;
|
|
22
|
+
held?: boolean;
|
|
21
23
|
details: string;
|
|
22
24
|
}
|
|
23
25
|
export interface BundleTask {
|
|
@@ -28,7 +30,7 @@ export interface BundleTask {
|
|
|
28
30
|
judgeCriteria: BundleJudgeCriterion[];
|
|
29
31
|
}
|
|
30
32
|
export interface BundleContentHashes {
|
|
31
|
-
/** sha256 of
|
|
33
|
+
/** Queue 35 (D-2134 R6): sha256 of JSON.stringify of the parsed source events. */
|
|
32
34
|
journal: string;
|
|
33
35
|
/** graphDefinitionHash from run-start when recorded. */
|
|
34
36
|
graphDefinitionHash?: string;
|
|
@@ -38,6 +40,7 @@ export interface ProofBundle {
|
|
|
38
40
|
schemaVersion: number;
|
|
39
41
|
runId: string;
|
|
40
42
|
environment?: RunEnvironment;
|
|
43
|
+
environments: SessionEnvironment[];
|
|
41
44
|
contentHashes: BundleContentHashes;
|
|
42
45
|
tasks: BundleTask[];
|
|
43
46
|
/** Plain-language known limits — this is not an unconditional proof of correctness. */
|
package/dist/report/bundle.js
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
// redactSecrets seam. No I/O and no network: the caller (report CLI) owns the write.
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { redactSecrets } from "../run/redact.js";
|
|
7
|
-
import {
|
|
7
|
+
import { normalizeGateOutcome } from "../run/outcome.js";
|
|
8
|
+
import { recordedEnvironment, recordedEnvironments } from "./compare.js";
|
|
8
9
|
// Bump when the packet shape changes in a way a future reader must branch on before parsing.
|
|
9
10
|
// 2 (T11): gates gained `declined`, and `pass` narrowed from "the gate did not fail" to "the gate
|
|
10
11
|
// ran and passed" — a schema-1 reader would read a decline as a plain failure.
|
|
11
|
-
|
|
12
|
+
// Queue 35 (D-2134 R6): held gates and session environments change the packet's meaning.
|
|
13
|
+
export const BUNDLE_SCHEMA_VERSION = 3;
|
|
12
14
|
// T11: the one predicate every decline surface shares — the record, the text tickmark rate, the
|
|
13
15
|
// comparison metrics and this packet must agree on what "never ran" means. Modern runs journal
|
|
14
16
|
// `skipped: true`; runs from before that field carry the same decline only in the details prefix
|
|
@@ -19,7 +21,7 @@ export const gateDeclined = (data) => data.skipped === true || /^skipped\b/.test
|
|
|
19
21
|
export const KNOWN_LIMITS = [
|
|
20
22
|
"This packet is a portable snapshot of journaled run facts, not an independent re-verification of the work or its gates.",
|
|
21
23
|
"Judge evidence citations are copied from the journal as the judge recorded them; this packet does not re-validate citations against the judged diff.",
|
|
22
|
-
"Content hashes bind this packet to the journal
|
|
24
|
+
"Content hashes bind this packet to the JSON.stringify serialization of parsed journal events used to build it; they do not prove the underlying gates, merges, or tip verification were correct.",
|
|
23
25
|
"Secret-shaped strings are redacted by local pattern matching only; redaction is not a formal security audit.",
|
|
24
26
|
"Producing this packet never contacts a network and never uploads anything.",
|
|
25
27
|
];
|
|
@@ -135,11 +137,13 @@ export function buildProofBundle(runId, events) {
|
|
|
135
137
|
const judgeCriteria = [];
|
|
136
138
|
for (const g of gateEvents) {
|
|
137
139
|
const gate = typeof g.data.gate === "string" ? g.data.gate : "unknown";
|
|
138
|
-
//
|
|
140
|
+
// Queue 35 (D-2134 R6): a passing screen remains held until its full suite speaks.
|
|
139
141
|
const declined = gateDeclined(g.data);
|
|
140
|
-
const
|
|
142
|
+
const outcome = normalizeGateOutcome(g.data);
|
|
143
|
+
const held = outcome.kind === "held";
|
|
144
|
+
const pass = outcome.kind === "passed" && !declined;
|
|
141
145
|
const details = typeof g.data.details === "string" ? g.data.details : "";
|
|
142
|
-
gates.push({ gate, pass, ...(declined ? { declined: true } : {}), details });
|
|
146
|
+
gates.push({ gate, pass, ...(declined ? { declined: true } : {}), ...(held ? { held: true } : {}), details });
|
|
143
147
|
if (gate === "acceptance") {
|
|
144
148
|
for (const c of parseJudgeCriteria(g.data))
|
|
145
149
|
judgeCriteria.push(c);
|
|
@@ -156,6 +160,7 @@ export function buildProofBundle(runId, events) {
|
|
|
156
160
|
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
157
161
|
runId,
|
|
158
162
|
...(environment ? { environment } : {}),
|
|
163
|
+
environments: recordedEnvironments(events),
|
|
159
164
|
contentHashes: {
|
|
160
165
|
journal: journalHash,
|
|
161
166
|
...(gHash ? { graphDefinitionHash: gHash } : {}),
|
package/dist/report/compare.d.ts
CHANGED
|
@@ -8,11 +8,18 @@ export type EnvironmentCompare = {
|
|
|
8
8
|
comparable: false;
|
|
9
9
|
reason: "mismatch";
|
|
10
10
|
recorded: string;
|
|
11
|
+
mixedSessions?: true;
|
|
11
12
|
} | {
|
|
12
13
|
comparable: false;
|
|
13
14
|
reason: "unbound";
|
|
14
15
|
};
|
|
15
16
|
export declare function hasRunStart(events: JournalEvent[]): boolean;
|
|
17
|
+
export interface SessionEnvironment {
|
|
18
|
+
event: "run-start" | "run-resume";
|
|
19
|
+
ts: string;
|
|
20
|
+
environment?: RunEnvironment;
|
|
21
|
+
}
|
|
22
|
+
export declare function recordedEnvironments(events: JournalEvent[]): SessionEnvironment[];
|
|
16
23
|
export declare function recordedEnvironment(events: JournalEvent[]): RunEnvironment | undefined;
|
|
17
24
|
export declare function environmentComparable(baselineEnv: RunEnvironment | undefined, currentEnv: RunEnvironment | undefined): EnvironmentCompare;
|
|
18
25
|
export interface RunMetrics {
|
|
@@ -21,6 +28,7 @@ export interface RunMetrics {
|
|
|
21
28
|
gateFail: number;
|
|
22
29
|
gateTotal: number;
|
|
23
30
|
costUsd: number | undefined;
|
|
31
|
+
costPartial: boolean;
|
|
24
32
|
tokensTotal: number | undefined;
|
|
25
33
|
}
|
|
26
34
|
export declare function runMetrics(events: JournalEvent[], rows?: TelemetryRow[], cost?: CostConfig): RunMetrics;
|
|
@@ -28,6 +36,7 @@ export interface RunDelta {
|
|
|
28
36
|
durationMs: number | undefined;
|
|
29
37
|
gateFail: number;
|
|
30
38
|
costUsd: number | undefined;
|
|
39
|
+
costPartial: boolean;
|
|
31
40
|
tokensTotal: number | undefined;
|
|
32
41
|
}
|
|
33
42
|
export type CompareOutcome = {
|
package/dist/report/compare.js
CHANGED
|
@@ -1,43 +1,50 @@
|
|
|
1
|
+
import { normalizeGateOutcome } from "../run/outcome.js";
|
|
1
2
|
import { estimateCosts } from "./cost.js";
|
|
2
3
|
export function hasRunStart(events) {
|
|
3
4
|
return events.some((e) => e.event === "run-start");
|
|
4
5
|
}
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
// Queue 35 (D-2134 R6): validate each session's stamp before folding the run's identity.
|
|
7
|
+
function parseEnvironment(env) {
|
|
8
|
+
if (!env || typeof env !== "object" || Array.isArray(env))
|
|
9
|
+
return undefined;
|
|
10
|
+
const o = env;
|
|
11
|
+
if (typeof o.tickmarkrVersion !== "string" || !o.tickmarkrVersion.trim()
|
|
12
|
+
|| typeof o.configHash !== "string" || !o.configHash.trim())
|
|
13
|
+
return undefined;
|
|
14
|
+
if (!o.adapterVersions || typeof o.adapterVersions !== "object" || Array.isArray(o.adapterVersions))
|
|
15
|
+
return undefined;
|
|
16
|
+
const adapterVersions = {};
|
|
17
|
+
for (const [k, v] of Object.entries(o.adapterVersions)) {
|
|
18
|
+
if (typeof v !== "string")
|
|
17
19
|
return undefined;
|
|
18
|
-
|
|
19
|
-
for (const [k, v] of Object.entries(o.adapterVersions)) {
|
|
20
|
-
if (typeof v !== "string")
|
|
21
|
-
return undefined;
|
|
22
|
-
adapterVersions[k] = v;
|
|
23
|
-
}
|
|
24
|
-
// Capacity arrived in v2.0 as a pair. Two absent fields are an honest older record; one without
|
|
25
|
-
// the other is a malformed new record and fails closed instead of being compared as legacy.
|
|
26
|
-
const hasCores = Object.hasOwn(o, "cores");
|
|
27
|
-
const hasForkCap = Object.hasOwn(o, "forkCap");
|
|
28
|
-
if (hasCores !== hasForkCap)
|
|
29
|
-
return undefined;
|
|
30
|
-
if (hasCores && (typeof o.cores !== "number" || !Number.isFinite(o.cores) || o.cores <= 0
|
|
31
|
-
|| typeof o.forkCap !== "number" || !Number.isFinite(o.forkCap) || o.forkCap <= 0))
|
|
32
|
-
return undefined;
|
|
33
|
-
return {
|
|
34
|
-
tickmarkrVersion: o.tickmarkrVersion,
|
|
35
|
-
configHash: o.configHash,
|
|
36
|
-
adapterVersions,
|
|
37
|
-
...(hasCores ? { cores: o.cores, forkCap: o.forkCap } : {}),
|
|
38
|
-
};
|
|
20
|
+
adapterVersions[k] = v;
|
|
39
21
|
}
|
|
40
|
-
|
|
22
|
+
// Capacity arrived in v2.0 as a pair. Two absent fields are an honest older record; one without
|
|
23
|
+
// the other is a malformed new record and fails closed instead of being compared as legacy.
|
|
24
|
+
const hasCores = Object.hasOwn(o, "cores");
|
|
25
|
+
const hasForkCap = Object.hasOwn(o, "forkCap");
|
|
26
|
+
if (hasCores !== hasForkCap)
|
|
27
|
+
return undefined;
|
|
28
|
+
if (hasCores && (typeof o.cores !== "number" || !Number.isFinite(o.cores) || o.cores <= 0
|
|
29
|
+
|| typeof o.forkCap !== "number" || !Number.isFinite(o.forkCap) || o.forkCap <= 0))
|
|
30
|
+
return undefined;
|
|
31
|
+
return {
|
|
32
|
+
tickmarkrVersion: o.tickmarkrVersion,
|
|
33
|
+
configHash: o.configHash,
|
|
34
|
+
adapterVersions,
|
|
35
|
+
...(hasCores ? { cores: o.cores, forkCap: o.forkCap } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function recordedEnvironments(events) {
|
|
39
|
+
return events.flatMap((e) => e.event === "run-start" || e.event === "run-resume"
|
|
40
|
+
? [{ event: e.event, ts: e.ts, environment: parseEnvironment(e.data.environment) }]
|
|
41
|
+
: []);
|
|
42
|
+
}
|
|
43
|
+
export function recordedEnvironment(events) {
|
|
44
|
+
const sessions = recordedEnvironments(events);
|
|
45
|
+
const first = sessions[0]?.environment;
|
|
46
|
+
return first && sessions.every(({ environment }) => environment && envEqual(first, environment))
|
|
47
|
+
? first : undefined;
|
|
41
48
|
}
|
|
42
49
|
// Canonical identity string for the `recorded` field — configHash is the axis the acceptance
|
|
43
50
|
// criteria name; full environment equality still decides .comparable.
|
|
@@ -81,12 +88,14 @@ export function runMetrics(events, rows = [], cost = {}) {
|
|
|
81
88
|
durationMs = to - from;
|
|
82
89
|
}
|
|
83
90
|
const gates = events.filter((e) => e.event === "gate-result");
|
|
84
|
-
const gatePass = gates.filter((e) => e.data.
|
|
85
|
-
const gateFail = gates.filter((e) => e.data.
|
|
91
|
+
const gatePass = gates.filter((e) => normalizeGateOutcome(e.data).kind === "passed").length;
|
|
92
|
+
const gateFail = gates.filter((e) => normalizeGateOutcome(e.data).kind === "failed").length;
|
|
86
93
|
let costUsd;
|
|
87
94
|
let costSum = 0;
|
|
88
95
|
let hasCost = false;
|
|
89
|
-
|
|
96
|
+
const prices = estimateCosts(rows, cost);
|
|
97
|
+
const costPartial = prices.some((p) => p.partialMetering);
|
|
98
|
+
for (const p of prices) {
|
|
90
99
|
if (p.apiUsd !== undefined) {
|
|
91
100
|
costSum += p.apiUsd;
|
|
92
101
|
hasCost = true;
|
|
@@ -110,7 +119,7 @@ export function runMetrics(events, rows = [], cost = {}) {
|
|
|
110
119
|
}
|
|
111
120
|
if (hasTokens)
|
|
112
121
|
tokensTotal = tokenSum;
|
|
113
|
-
return { durationMs, gatePass, gateFail, gateTotal: gates.length, costUsd, tokensTotal };
|
|
122
|
+
return { durationMs, gatePass, gateFail, gateTotal: gates.length, costUsd, costPartial, tokensTotal };
|
|
114
123
|
}
|
|
115
124
|
const n = (x) => x.toLocaleString("en-US");
|
|
116
125
|
const EM = "—";
|
|
@@ -161,6 +170,9 @@ function comparabilityLine(cmp, baselineEnv, currentEnv) {
|
|
|
161
170
|
if (cmp.reason === "unbound") {
|
|
162
171
|
return "comparability caveat — one or both runs lack a recorded environment identity; not apples-to-apples";
|
|
163
172
|
}
|
|
173
|
+
if (cmp.mixedSessions) {
|
|
174
|
+
return "comparability caveat — environment identity disagrees across mixed sessions; not apples-to-apples";
|
|
175
|
+
}
|
|
164
176
|
const base = baselineEnv?.configHash ?? EM;
|
|
165
177
|
const cur = currentEnv?.configHash ?? EM;
|
|
166
178
|
return `comparability caveat — environment identity disagrees (baseline configHash=${base} ≠ current configHash=${cur}; recorded baseline ${cmp.recorded}); not apples-to-apples`;
|
|
@@ -182,7 +194,7 @@ export function renderComparison(opts) {
|
|
|
182
194
|
`| duration | ${fmtDuration(baseline.durationMs)} | ${fmtDuration(current.durationMs)} | ${fmtSignedDuration(delta.durationMs)} |`,
|
|
183
195
|
`| gate failures | ${fmtInt(baseline.gateFail)} | ${fmtInt(current.gateFail)} | ${fmtSignedInt(delta.gateFail)} |`,
|
|
184
196
|
`| gate pass rate | ${baseline.gateTotal ? `${baseline.gatePass}/${baseline.gateTotal}` : EM} | ${current.gateTotal ? `${current.gatePass}/${current.gateTotal}` : EM} | ${EM} |`,
|
|
185
|
-
`| cost | ${fmtUsd(baseline.costUsd)} | ${fmtUsd(current.costUsd)} | ${fmtSignedUsd(delta.costUsd)} |`,
|
|
197
|
+
`| cost | ${fmtUsd(baseline.costUsd)}${baseline.costPartial ? " (partial)" : ""} | ${fmtUsd(current.costUsd)}${current.costPartial ? " (partial)" : ""} | ${fmtSignedUsd(delta.costUsd)}${delta.costPartial ? " (partial)" : ""} |`,
|
|
186
198
|
`| tokens | ${fmtInt(baseline.tokensTotal)} | ${fmtInt(current.tokensTotal)} | ${fmtSignedInt(delta.tokensTotal)} |`,
|
|
187
199
|
"",
|
|
188
200
|
];
|
|
@@ -208,10 +220,16 @@ export function compareRuns(opts) {
|
|
|
208
220
|
}
|
|
209
221
|
const baselineEnv = recordedEnvironment(opts.baselineEvents);
|
|
210
222
|
const currentEnv = recordedEnvironment(opts.events);
|
|
211
|
-
const
|
|
223
|
+
const sessions = [...recordedEnvironments(opts.baselineEvents), ...recordedEnvironments(opts.events)];
|
|
224
|
+
const comparability = sessions.some((s) => !s.environment)
|
|
225
|
+
? { comparable: false, reason: "unbound" }
|
|
226
|
+
: !baselineEnv || !currentEnv
|
|
227
|
+
? { comparable: false, reason: "mismatch", recorded: baselineEnv?.configHash ?? "mixed sessions", mixedSessions: true }
|
|
228
|
+
: environmentComparable(baselineEnv, currentEnv);
|
|
212
229
|
const baseline = runMetrics(opts.baselineEvents, opts.baselineRows ?? [], opts.cost ?? {});
|
|
213
230
|
const current = runMetrics(opts.events, opts.rows ?? [], opts.cost ?? {});
|
|
214
231
|
const delta = {
|
|
232
|
+
costPartial: current.costPartial || baseline.costPartial,
|
|
215
233
|
durationMs: current.durationMs !== undefined && baseline.durationMs !== undefined
|
|
216
234
|
? current.durationMs - baseline.durationMs
|
|
217
235
|
: undefined,
|
|
@@ -37,15 +37,16 @@ export function formatRateBasis(row) {
|
|
|
37
37
|
export function formatChannelMoney(row) {
|
|
38
38
|
const prices = [];
|
|
39
39
|
const bases = [];
|
|
40
|
+
const floor = row.partialMetering ? "≥ " : "";
|
|
40
41
|
if (row.apiUsd !== undefined)
|
|
41
|
-
prices.push(`price: $${row.apiUsd.toFixed(6)}`);
|
|
42
|
+
prices.push(`price: ${floor}$${row.apiUsd.toFixed(6)}`);
|
|
42
43
|
if (row.amortizedUsd !== undefined && row.subPlan !== undefined) {
|
|
43
44
|
const [low, high] = row.amortizedUsd;
|
|
44
|
-
prices.push(`price: $${low.toFixed(6)}–$${high.toFixed(6)} amortized`);
|
|
45
|
+
prices.push(`price: ${floor}$${low.toFixed(6)}–$${high.toFixed(6)} amortized`);
|
|
45
46
|
bases.push(`${n(row.attempts)} windows × $${row.subPlan.planMonthly}/month ÷ ${row.subPlan.windowsPerMonthHigh}–${row.subPlan.windowsPerMonthLow} windows/month`);
|
|
46
47
|
}
|
|
47
48
|
if (row.counterfactualUsd !== undefined)
|
|
48
|
-
prices.push(`API-equivalent: $${row.counterfactualUsd.toFixed(6)}`);
|
|
49
|
+
prices.push(`API-equivalent: ${floor}$${row.counterfactualUsd.toFixed(6)}`);
|
|
49
50
|
const basis = formatRateBasis(row);
|
|
50
51
|
if (basis)
|
|
51
52
|
bases.push(basis);
|
package/dist/run/daemon.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type ModeResolution, type RoutingMode, type TickmarkrConfig } from "../
|
|
|
3
3
|
import { type DriverChoice } from "../drivers/index.js";
|
|
4
4
|
import { type ExecutorDriver, type Slot } from "../drivers/types.js";
|
|
5
5
|
import { type Baseline, type BaselineProvenance, type GateEvidenceOptions } from "../gates/baseline.js";
|
|
6
|
+
import { environmentFingerprint } from "../gates/cache.js";
|
|
6
7
|
import type { GateResult } from "../gates/types.js";
|
|
7
8
|
import { type RunGraph, type Task } from "../graph/schema.js";
|
|
8
9
|
import { type PreserveProducer, type RunCapacity } from "./git.js";
|
|
@@ -226,6 +227,7 @@ export declare function resetHarvestSilentMsForTests(): void;
|
|
|
226
227
|
export declare const HARVESTED_RESULT_SUMMARY = "harvested: the worktree carries committed work; the worker emitted no TICKMARKR_RESULT trailer";
|
|
227
228
|
/** T4 (OBS-266): identity of the command SET a tip verify ran — a changed command is a different verify. */
|
|
228
229
|
export declare function commandsHash(commands: Record<string, string>): string;
|
|
230
|
+
type TipEnvironment = ReturnType<typeof environmentFingerprint>;
|
|
229
231
|
export interface TipProof {
|
|
230
232
|
kind: "fresh" | "reused" | "failed" | "incomplete";
|
|
231
233
|
/** Each completed gate keeps its own execution provenance, even in a mixed cycle. */
|
|
@@ -261,6 +263,7 @@ export interface TipClose {
|
|
|
261
263
|
expected?: string;
|
|
262
264
|
identityFailed: boolean;
|
|
263
265
|
deferred: DeferredVerdict[];
|
|
266
|
+
tipEnvironment?: TipEnvironment;
|
|
264
267
|
}
|
|
265
268
|
interface TipVerifyOptions {
|
|
266
269
|
lastMergedTask?: string;
|
|
@@ -293,6 +296,7 @@ export declare function appendIdentityFailure(journal: Journal, f: {
|
|
|
293
296
|
state?: IntegrationRefState;
|
|
294
297
|
cmdHash: string;
|
|
295
298
|
capacity: RunCapacity;
|
|
299
|
+
tipEnvironment?: TipEnvironment;
|
|
296
300
|
infra?: boolean;
|
|
297
301
|
}): void;
|
|
298
302
|
/** O: I(expected) — or, with no expected value, the ref must be regular and its commit becomes the expected. */
|
|
@@ -309,9 +313,9 @@ export declare function openingIdentity(identity: TipIdentity, journal: Journal)
|
|
|
309
313
|
/**
|
|
310
314
|
* OBS-34's strict tip verify, but it stops re-paying for an unmoved tip (~334m corpus-wide; 69.5m in
|
|
311
315
|
* one park-heavy run whose 18 resume cycles merged nothing new). The verify journals the SHA it
|
|
312
|
-
* verified and the hash of the command set, so a later run-end can recognize the same verified state
|
|
313
|
-
*
|
|
314
|
-
* `tip-verify-cached`
|
|
316
|
+
* verified and the hash of the command set, so a later run-end can recognize the same verified state.
|
|
317
|
+
* Queue row 24 (D-2134): the last green also binds every row's stamped runner and environment identity
|
|
318
|
+
* and capacity before `tip-verify-cached` can skip with no checkout. ANY doubt — moved tip, changed commands, a gate
|
|
315
319
|
* missing from that cycle's green set, a failure recorded in it, a cycle older than the last one —
|
|
316
320
|
* runs the full verify. 2.8.1 T1 (ANL-023 §7.1): with `identity` (the daemon always passes it) run end
|
|
317
321
|
* verifies a fresh detached checkout of the expected commit under a validated HEAD reflog, never the
|