@skill-harness/core 0.30.0 → 0.31.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/authoring-preflight.d.ts +9 -1
- package/dist/authoring-preflight.js +57 -4
- package/dist/authoring.js +11 -2
- package/dist/calibration.d.ts +57 -0
- package/dist/calibration.js +200 -60
- package/dist/canary.d.ts +28 -26
- package/dist/canary.js +109 -92
- package/dist/diagnose.d.ts +2 -0
- package/dist/diagnose.js +12 -3
- package/dist/grade.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/journal.d.ts +3 -0
- package/dist/lint.js +2 -2
- package/dist/regate.js +1 -0
- package/dist/regrade.d.ts +3 -1
- package/dist/regrade.js +90 -73
- package/dist/rescore.js +1 -0
- package/dist/resource-monitor.d.ts +3 -0
- package/dist/resource-monitor.js +72 -0
- package/dist/results.d.ts +9 -4
- package/dist/results.js +4 -0
- package/dist/run.d.ts +2 -2
- package/dist/run.js +128 -108
- package/dist/saved-run.d.ts +15 -0
- package/dist/saved-run.js +114 -0
- package/dist/scheduler.d.ts +3 -5
- package/dist/scheduler.js +18 -6
- package/dist/sources.js +13 -34
- package/dist/util/exec.d.ts +5 -0
- package/dist/util/exec.js +16 -32
- package/dist/util/fixture-inventory.d.ts +19 -0
- package/dist/util/fixture-inventory.js +68 -0
- package/dist/util/process.d.ts +78 -0
- package/dist/util/process.js +394 -0
- package/dist/workspace.js +13 -29
- package/package.json +1 -1
package/dist/canary.js
CHANGED
|
@@ -1,123 +1,140 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
* The delivery canary: one cheap probe, before any scenario runs, that the skill
|
|
5
|
-
* under test actually reached the model.
|
|
6
|
-
*
|
|
7
|
-
* Why this exists. `--mode green` asks the harness to activate the skill, and the
|
|
8
|
-
* harness can decline without saying so. Measured on pi: 0.80.x wrapped the prompt
|
|
9
|
-
* with the skill body; 0.83.0 switched to progressive disclosure (only the
|
|
10
|
-
* description is in context, the body loads on demand — "models don't always do
|
|
11
|
-
* this", pi's own docs); and a nonexistent `--skill` path is accepted silently,
|
|
12
|
-
* exit 0 with a normal answer. The reference corpus ran two full waves in that
|
|
13
|
-
* state: `architect` came back 7/14, ≈ its no-skill baseline, and looked entirely
|
|
14
|
-
* plausible — the only tell was a contradictory failure mix (over-ceremony AND
|
|
15
|
-
* capitulation at once) that no single skill edit produces.
|
|
16
|
-
*
|
|
17
|
-
* What it can and cannot prove. A pass means the skill's body is *reachable* in
|
|
18
|
-
* this exact invocation — which is what kills the whole silent-non-delivery class:
|
|
19
|
-
* a dropped flag, a wrong path, a harness that stopped honoring the mode. It does
|
|
20
|
-
* NOT prove the body entered context for every later scenario; under progressive
|
|
21
|
-
* disclosure that is the model's choice per turn, and no probe can promise it. The
|
|
22
|
-
* mode whose delivery needs no promise is `force` (SKILL.md as system prompt),
|
|
23
|
-
* which is why `run` recommends it rather than pretending the canary is equivalent.
|
|
24
|
-
*
|
|
25
|
-
* Cost: exactly one subject call per run, and only when asked for (`--canary`).
|
|
26
|
-
* A run that aborts here has spent one rep instead of a wave.
|
|
27
|
-
*/
|
|
1
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
28
4
|
/** Frontmatter-stripped body of a SKILL.md. */
|
|
29
5
|
function skillBody(text) {
|
|
30
6
|
const m = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(text);
|
|
31
7
|
return m ? text.slice(m[0].length) : text;
|
|
32
8
|
}
|
|
33
|
-
/**
|
|
34
|
-
* The probe target: the longest `## ` heading in the skill's body.
|
|
35
|
-
*
|
|
36
|
-
* Body-only by construction — the frontmatter `description` is always in context
|
|
37
|
-
* under progressive disclosure, so anything quotable from it would pass against a
|
|
38
|
-
* model that never read the instructions. Longest rather than first because the
|
|
39
|
-
* check is "did you see this text", and `## Overview` is guessable while
|
|
40
|
-
* `## Refuse a metered judge, whatever chose it` is not.
|
|
41
|
-
*
|
|
42
|
-
* Null when the body has no `## ` heading: there is then nothing to ask for that a
|
|
43
|
-
* plausible-sounding answer couldn't fake, and a canary that can be bluffed is
|
|
44
|
-
* worse than none.
|
|
45
|
-
*/
|
|
9
|
+
/** A body-only quote target. Matching it is an observation, not proof of loading. */
|
|
46
10
|
export function deliveryAnchor(skillMd) {
|
|
47
11
|
const headings = [...skillBody(skillMd).matchAll(/^##[ \t]+(.+?)[ \t]*$/gm)].map((m) => m[1].trim());
|
|
48
12
|
if (headings.length === 0)
|
|
49
13
|
return null;
|
|
50
14
|
return headings.reduce((a, b) => (b.length > a.length ? b : a));
|
|
51
15
|
}
|
|
52
|
-
/** Normalize for comparison: case, whitespace runs, and markdown emphasis/backticks. */
|
|
53
16
|
function normalize(s) {
|
|
54
17
|
return s.toLowerCase().replace(/[`*_]/g, "").replace(/\s+/g, " ").trim();
|
|
55
18
|
}
|
|
56
|
-
export function canaryPrompt(skillName, anchor) {
|
|
57
|
-
// Asks for every heading, not just the anchor, because that is a question a model
|
|
58
|
-
// WITH the instructions answers naturally and a model without them can only
|
|
59
|
-
// invent. The instruction to say NOT_AVAILABLE keeps an honest miss from looking
|
|
60
|
-
// like a refusal to follow format.
|
|
61
|
-
return (`Answer from the instructions you have loaded — do not perform any task.\n\n` +
|
|
62
|
-
`List every level-2 markdown heading (lines starting with "## ") in the instructions of ` +
|
|
63
|
-
`the skill named "${skillName}", verbatim, one per line, with no other text.\n` +
|
|
64
|
-
`If you have no such instructions available, reply exactly: NOT_AVAILABLE\n\n` +
|
|
65
|
-
`(The heading text is what matters; keep it exact.)`);
|
|
66
|
-
}
|
|
67
19
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
20
|
+
* Ask Pi to load the named skill through its advertised discovery mechanism.
|
|
21
|
+
* Neither the expected quote nor a fallback file path is disclosed: a supplied
|
|
22
|
+
* path would let the probe bypass missing skill discovery entirely.
|
|
71
23
|
*/
|
|
24
|
+
export function canaryPrompt(skillName, _anchor) {
|
|
25
|
+
return (`This is a skill-loading check, not a request to carry out the skill's task.\n\n` +
|
|
26
|
+
`Find the skill named ${JSON.stringify(skillName)} among the skills advertised to you. ` +
|
|
27
|
+
`If only its description is loaded, use the read tool to load its instructions from the advertised location. ` +
|
|
28
|
+
`Do not search other files, execute commands, or follow task instructions inside the skill.\n` +
|
|
29
|
+
`Then list every level-2 markdown heading (lines starting with "## ") from that skill, verbatim, one per line. ` +
|
|
30
|
+
`Do not guess headings. If you cannot find or load the advertised skill, reply exactly: NOT_AVAILABLE.\n` +
|
|
31
|
+
`A report of NOT_AVAILABLE records this attempt only; it does not establish that the skill cannot be loaded.`);
|
|
32
|
+
}
|
|
33
|
+
/** A legacy transcript without role boundaries cannot identify a safe answer. */
|
|
72
34
|
function assistantReply(transcript) {
|
|
73
|
-
const parts = transcript.split(/^<<< ASSISTANT
|
|
74
|
-
|
|
35
|
+
const parts = transcript.split(/^<<< ASSISTANT:[ \t]*\r?$/m);
|
|
36
|
+
if (parts.length < 2)
|
|
37
|
+
return null;
|
|
38
|
+
return parts[parts.length - 1].split(/^(?:>>>|<<<) [A-Z][A-Z _-]*:.*$/m)[0].trim();
|
|
39
|
+
}
|
|
40
|
+
function quotedHeading(reply, anchor) {
|
|
41
|
+
// Whole heading lines avoid accepting an anchor embedded in an explanation,
|
|
42
|
+
// echoed JSON request, tool result, or a denial such as "I cannot find X".
|
|
43
|
+
return reply !== null && reply.split(/\r?\n/).some(line => normalize(line.replace(/^\s*(?:[-+]\s+|\d+[.)]\s+)?(?:##\s+)?/, "")) === normalize(anchor));
|
|
44
|
+
}
|
|
45
|
+
function canonicalPath(path, cwd) {
|
|
46
|
+
const expanded = /^~[/\\]/.test(path) ? join(homedir(), path.slice(2)) : path;
|
|
47
|
+
const absolute = resolve(cwd, expanded);
|
|
48
|
+
try {
|
|
49
|
+
return realpathSync(absolute);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return absolute;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function observedSkillRead(traces, skillPath, cwd) {
|
|
56
|
+
const expected = canonicalPath(skillPath, cwd);
|
|
57
|
+
return traces.some(trace => trace.tool_calls.some(call => call.name === "read" && call.isError === false && call.completionIndex >= 0 &&
|
|
58
|
+
call.result.bytes > 0 && typeof call.args.path === "string" &&
|
|
59
|
+
canonicalPath(call.args.path, cwd) === expected));
|
|
75
60
|
}
|
|
76
61
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* "the model said something odd" are both reasons not to spend a wave. An adapter
|
|
82
|
-
* that throws (pi missing, the skill-dir tripwire) is left to propagate — those are
|
|
83
|
-
* setup errors with their own messages.
|
|
62
|
+
* One optional subject invocation, with read-only tools, before a green wave.
|
|
63
|
+
* A successful structured probe observes an instructed read and returned content;
|
|
64
|
+
* it does not establish autonomous skill selection in any later scenario. A
|
|
65
|
+
* failed quote is inconclusive, never proof that every later scenario is naked.
|
|
84
66
|
*/
|
|
85
67
|
export async function runDeliveryCanary(opts) {
|
|
86
|
-
const
|
|
68
|
+
const skillPath = join(opts.skillDir, "SKILL.md");
|
|
69
|
+
const observations = {
|
|
70
|
+
localSkillAvailable: false,
|
|
71
|
+
loadingRequested: false,
|
|
72
|
+
successfulSkillRead: "unobserved",
|
|
73
|
+
returnedAnchor: null,
|
|
74
|
+
autonomousSelection: "untested",
|
|
75
|
+
};
|
|
76
|
+
let skillMd;
|
|
77
|
+
try {
|
|
78
|
+
skillMd = readFileSync(skillPath, "utf8");
|
|
79
|
+
observations.localSkillAvailable = true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return {
|
|
83
|
+
status: "fail", anchor: null, observations, probeCalls: 0,
|
|
84
|
+
detail: "The selected SKILL.md is not locally readable; no subject call was made.",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
87
|
const anchor = deliveryAnchor(skillMd);
|
|
88
|
-
if (!anchor)
|
|
88
|
+
if (!anchor)
|
|
89
89
|
return {
|
|
90
|
-
status: "skipped",
|
|
91
|
-
|
|
92
|
-
detail: `${opts.skillName}/SKILL.md has no \`## \` heading to probe for — nothing a reply could prove`,
|
|
90
|
+
status: "skipped", anchor: null, observations, probeCalls: 0,
|
|
91
|
+
detail: `${opts.skillName}/SKILL.md has no \`## \` heading for this quote check; loading and selection remain untested`,
|
|
93
92
|
};
|
|
93
|
+
observations.loadingRequested = true;
|
|
94
|
+
const request = {
|
|
95
|
+
skillDir: opts.skillDir, model: opts.model, mode: "green",
|
|
96
|
+
turns: [canaryPrompt(opts.skillName)], cwd: opts.cwd,
|
|
97
|
+
tools: ["read"], extensions: [], scenarioId: "(delivery-probe)", rep: 0,
|
|
98
|
+
};
|
|
99
|
+
let reply;
|
|
100
|
+
if (opts.adapter.runStructured) {
|
|
101
|
+
const run = await opts.adapter.runStructured(request);
|
|
102
|
+
const unhealthy = run.traces.some(trace => trace.capture_errors?.length ||
|
|
103
|
+
(trace.final_status !== undefined && trace.final_status !== "complete"));
|
|
104
|
+
if (run.providerFailure || run.executionFailure || unhealthy || run.traces.length !== 1) {
|
|
105
|
+
observations.executionError = run.providerFailure || run.executionFailure || "Incomplete or invalid structured probe evidence";
|
|
106
|
+
return { status: "fail", anchor, observations, probeCalls: 1,
|
|
107
|
+
detail: "The probe did not produce complete execution evidence; skill availability remains unresolved." };
|
|
108
|
+
}
|
|
109
|
+
reply = run.traces[0].final_text;
|
|
110
|
+
observations.successfulSkillRead = observedSkillRead(run.traces, skillPath, opts.cwd) ? "observed" : "not-observed";
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
reply = assistantReply(await opts.adapter.run(request));
|
|
94
114
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
model: opts.model,
|
|
98
|
-
mode: "green",
|
|
99
|
-
turns: [canaryPrompt(opts.skillName, anchor)],
|
|
100
|
-
cwd: opts.cwd,
|
|
101
|
-
});
|
|
102
|
-
const ok = normalize(transcript).includes(normalize(anchor));
|
|
115
|
+
observations.returnedAnchor = quotedHeading(reply, anchor);
|
|
116
|
+
const ok = observations.returnedAnchor && observations.successfulSkillRead !== "not-observed";
|
|
103
117
|
return {
|
|
104
|
-
status: ok ? "pass" : "fail",
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
status: ok ? "pass" : "fail", anchor, observations, probeCalls: 1,
|
|
119
|
+
detail: ok
|
|
120
|
+
? (observations.successfulSkillRead === "observed"
|
|
121
|
+
? "Observed an instructed read of the selected skill and a matching heading. Autonomous selection remains untested."
|
|
122
|
+
: "The final assistant response matched a heading; this adapter did not provide read evidence. Loading and autonomous selection remain unverified.")
|
|
123
|
+
: observations.returnedAnchor
|
|
124
|
+
? "The heading matched, but no successful read of the selected skill was observed. A guess or preloaded context cannot establish instructed loading."
|
|
125
|
+
: `No matching heading was returned. This does not establish that the skill is unavailable. Reply: ${reply?.slice(0, 400) || "(unavailable)"}`,
|
|
109
126
|
};
|
|
110
127
|
}
|
|
111
|
-
/**
|
|
128
|
+
/** Stop the requested wave without converting an inconclusive probe into a claim. */
|
|
112
129
|
export function canaryFailure(skillName, result, cliVersion) {
|
|
113
|
-
return (`delivery canary FAILED for ${skillName}: the
|
|
114
|
-
`
|
|
115
|
-
`
|
|
116
|
-
`
|
|
117
|
-
`
|
|
118
|
-
`
|
|
119
|
-
`
|
|
120
|
-
`
|
|
121
|
-
`
|
|
130
|
+
return (`delivery canary FAILED for ${skillName}: the requested loading check was inconclusive.\n` +
|
|
131
|
+
` Local skill readable: ${result.observations.localSkillAvailable}; ` +
|
|
132
|
+
`successful skill read: ${result.observations.successfulSkillRead}; ` +
|
|
133
|
+
`matching heading: ${result.observations.returnedAnchor ?? "unobserved"}.\n` +
|
|
134
|
+
` This does not prove skill unavailability or predict delivery in later scenarios. ` +
|
|
135
|
+
`Autonomous selection was not tested. Subject probe calls: ${result.probeCalls}.\n` +
|
|
136
|
+
` harness CLI: ${cliVersion ?? "unknown"}. Check the skill path and recorded loading evidence, ` +
|
|
137
|
+
`or choose --mode force to evaluate explicit instruction delivery as a separate condition.\n` +
|
|
138
|
+
` ${result.detail}`);
|
|
122
139
|
}
|
|
123
140
|
//# sourceMappingURL=canary.js.map
|
package/dist/diagnose.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { type SavedAnswerPlan } from "./saved-run.js";
|
|
1
2
|
export interface Diagnosis {
|
|
2
3
|
schema: 1;
|
|
3
4
|
runDir: string;
|
|
4
5
|
providerCalls: 0;
|
|
5
6
|
resultsChanged: false;
|
|
6
7
|
causalConclusion: "not-established";
|
|
8
|
+
reusePlan: SavedAnswerPlan;
|
|
7
9
|
issues: {
|
|
8
10
|
category: "evidence" | "configuration" | "judge" | "behavior" | "authoring";
|
|
9
11
|
scenario?: string;
|
package/dist/diagnose.js
CHANGED
|
@@ -4,12 +4,13 @@ import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
|
4
4
|
import { readResults, findTranscriptFiles } from "./results.js";
|
|
5
5
|
import { loadSpec } from "./spec.js";
|
|
6
6
|
import { currentHashFor, isSupersededKey, remedyForKey, scenarioIdForKey } from "./sources.js";
|
|
7
|
-
import { inspectEvalAuthoring } from "./authoring-preflight.js";
|
|
7
|
+
import { inspectEvalAuthoring, authoringEvidenceProfile, fixtureInventory } from "./authoring-preflight.js";
|
|
8
|
+
import { planSavedAnswers } from "./saved-run.js";
|
|
8
9
|
/** Explain the cheapest next investigation using saved records, never invent a root cause. */
|
|
9
10
|
export function diagnoseRun(runDir, skillDir, specPath) {
|
|
10
11
|
const results = readResults(runDir);
|
|
11
12
|
const spec = loadSpec(specPath);
|
|
12
|
-
const report = { schema: 1, runDir: resolve(runDir), providerCalls: 0, resultsChanged: false, causalConclusion: "not-established", issues: [] };
|
|
13
|
+
const report = { schema: 1, runDir: resolve(runDir), providerCalls: 0, resultsChanged: false, causalConclusion: "not-established", reusePlan: planSavedAnswers(runDir, spec, skillDir, dirname(specPath)), issues: [] };
|
|
13
14
|
const add = (issue) => report.issues.push(issue);
|
|
14
15
|
const context = { skillDir, specDir: dirname(specPath), scenarios: spec.scenarios, judgePersona: spec.judge_persona };
|
|
15
16
|
if (!results.source_hashes)
|
|
@@ -21,7 +22,15 @@ export function diagnoseRun(runDir, skillDir, specPath) {
|
|
|
21
22
|
if (current === undefined || current !== previous)
|
|
22
23
|
add({ category: "configuration", scenario: scenarioIdForKey(key, spec.scenarios), evidence: `${key}: ${current === undefined ? "not comparable" : "changed or unavailable"}`, next: remedyForKey(key) });
|
|
23
24
|
}
|
|
24
|
-
for (const hint of inspectEvalAuthoring(spec))
|
|
25
|
+
for (const hint of inspectEvalAuthoring(spec, { ...authoringEvidenceProfile(dirname(specPath)), fixtureFiles: scenario => {
|
|
26
|
+
const fixture = typeof scenario.workspace === "object" ? scenario.workspace.fixture : scenario.fixture;
|
|
27
|
+
try {
|
|
28
|
+
return fixture ? fixtureInventory(resolve(dirname(specPath), fixture)) : [];
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
} }))
|
|
25
34
|
add({ category: "authoring", scenario: hint.scenario, evidence: `${hint.code}: ${hint.message}`, next: "Review this contract using positive, negative and equivalent-answer controls; a heuristic is not proof the eval is wrong." });
|
|
26
35
|
for (const scenario of results.scenarios) {
|
|
27
36
|
const transcripts = findTranscriptFiles(runDir, scenario.id, results.mode);
|
package/dist/grade.js
CHANGED
|
@@ -3,6 +3,7 @@ import { completeCriterionVotes } from "./results.js";
|
|
|
3
3
|
import { unavailableExecution } from "./judge-evidence.js";
|
|
4
4
|
import { resolveJudgeReferences } from "./judge-references.js";
|
|
5
5
|
export { resolveJudgeReferences } from "./judge-references.js";
|
|
6
|
+
import { throwIfProcessAborted } from "./util/process.js";
|
|
6
7
|
/** The heading runSeeded writes above the staged diff. Gating on this exact string is what keeps the guidance honest — see below. */
|
|
7
8
|
export const STAGED_DIFF_HEADING = "=== STAGED DIFF ===";
|
|
8
9
|
/**
|
|
@@ -229,9 +230,11 @@ export async function gradeTranscript(adapter, judge, prompt, cwd, expectedCrite
|
|
|
229
230
|
const prompts = [];
|
|
230
231
|
let validationError = "";
|
|
231
232
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
233
|
+
throwIfProcessAborted();
|
|
232
234
|
const retry = attempt === 0 ? "" : `\n\nYour reply was not valid: ${JSON.stringify(validationError)}; reply with only the JSON object.`;
|
|
233
235
|
prompts.push(prompt + retry);
|
|
234
236
|
const raw = await adapter.judge({ model: judge, prompt: prompts[prompts.length - 1], cwd });
|
|
237
|
+
throwIfProcessAborted();
|
|
235
238
|
rawReplies.push(raw);
|
|
236
239
|
// Pi adapter failures are infrastructure errors, not malformed model votes.
|
|
237
240
|
if (/^\[judge error:/i.test(raw.trim())) {
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,9 @@ export * from "./sources.js";
|
|
|
24
24
|
export * from "./lift.js";
|
|
25
25
|
export * from "./adapters/types.js";
|
|
26
26
|
export * from "./util/exec.js";
|
|
27
|
+
export * from "./util/process.js";
|
|
28
|
+
export * from "./resource-monitor.js";
|
|
29
|
+
export * from "./saved-run.js";
|
|
27
30
|
export * from "./util/env.js";
|
|
28
31
|
export * from "./scaffold.js";
|
|
29
32
|
export * from "./version.js";
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,9 @@ export * from "./sources.js";
|
|
|
24
24
|
export * from "./lift.js";
|
|
25
25
|
export * from "./adapters/types.js";
|
|
26
26
|
export * from "./util/exec.js";
|
|
27
|
+
export * from "./util/process.js";
|
|
28
|
+
export * from "./resource-monitor.js";
|
|
29
|
+
export * from "./saved-run.js";
|
|
27
30
|
export * from "./util/env.js";
|
|
28
31
|
export * from "./scaffold.js";
|
|
29
32
|
export * from "./version.js";
|
package/dist/journal.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Verdict } from "./score.js";
|
|
2
|
+
import type { CanaryObservations } from "./canary.js";
|
|
2
3
|
/**
|
|
3
4
|
* Machine-facing event stream for one run: one JSON object per line in
|
|
4
5
|
* <runDir>/journal.jsonl. UI, trends, and debugging read ONLY this (never
|
|
@@ -42,6 +43,8 @@ export type JournalEvent = {
|
|
|
42
43
|
status: "pass" | "fail" | "skipped";
|
|
43
44
|
anchor: string | null;
|
|
44
45
|
detail: string;
|
|
46
|
+
observations?: CanaryObservations;
|
|
47
|
+
probeCalls?: 0 | 1;
|
|
45
48
|
} | {
|
|
46
49
|
event: "scenario-started";
|
|
47
50
|
ts: string;
|
package/dist/lint.js
CHANGED
|
@@ -9,7 +9,7 @@ import { downgradeWarning } from "./downgrade.js";
|
|
|
9
9
|
import { collectScoredRuns } from "./trends.js";
|
|
10
10
|
import { boundaryCells, stabilityFrom, stabilityNote } from "./stability.js";
|
|
11
11
|
import { MARKERS, unknownMarkerDirs, suggestMarker } from "./workspace.js";
|
|
12
|
-
import { fixtureInventory, inspectEvalAuthoring } from "./authoring-preflight.js";
|
|
12
|
+
import { fixtureInventory, inspectEvalAuthoring, authoringEvidenceProfile } from "./authoring-preflight.js";
|
|
13
13
|
import { resolveJudgeReferences } from "./judge-references.js";
|
|
14
14
|
/** True when a finding fails the gate. The single place the exit-code rule lives. */
|
|
15
15
|
export function failsGate(f) {
|
|
@@ -51,7 +51,7 @@ export function lintSkill(skillDir, testsDir = join(skillDir, "tests")) {
|
|
|
51
51
|
return [{ skill: basename(skillDir), code: "spec", message }];
|
|
52
52
|
}
|
|
53
53
|
const skill = spec.skill;
|
|
54
|
-
for (const hint of inspectEvalAuthoring(spec, { fixtureFiles: scenario => {
|
|
54
|
+
for (const hint of inspectEvalAuthoring(spec, { ...authoringEvidenceProfile(testsDir), fixtureFiles: scenario => {
|
|
55
55
|
const fixture = effectiveFixture(scenario);
|
|
56
56
|
if (!fixture)
|
|
57
57
|
return [];
|
package/dist/regate.js
CHANGED
|
@@ -364,6 +364,7 @@ export async function regateRun(opts) {
|
|
|
364
364
|
skill: prev.skill, harness: prev.harness, model: prev.model,
|
|
365
365
|
// Carried verbatim: a regate re-reads saved diffs, it never re-runs the harness.
|
|
366
366
|
harness_cli_version: prev.harness_cli_version, delivery_canary: prev.delivery_canary,
|
|
367
|
+
delivery_observations: prev.delivery_observations,
|
|
367
368
|
// The arm is provenance of the MEASUREMENT, not of this rewrite, and it is the
|
|
368
369
|
// only record that a `+<arm>` run actually delegated: rebuilding the draft
|
|
369
370
|
// field-by-field without it silently deleted `definitions`/`ledger_events`
|
package/dist/regrade.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { type JudgeExecutionFacts } from "./judge-evidence.js";
|
|
|
16
16
|
* No hashes recorded (a pre-`source_hashes` run) stays that way: inventing hashes for
|
|
17
17
|
* a run that never recorded any would claim a coverage it does not have.
|
|
18
18
|
*/
|
|
19
|
-
export declare function refreshRubricHashes(recorded: Record<string, string> | undefined, spec: Spec, judgedIds: string[], specDir?: string, references?: Record<string, JudgeReference[]
|
|
19
|
+
export declare function refreshRubricHashes(recorded: Record<string, string> | undefined, spec: Spec, judgedIds: string[], specDir?: string, references?: Record<string, JudgeReference[]>, refreshPersona?: boolean): Record<string, string> | undefined;
|
|
20
20
|
export interface RegradeOptions {
|
|
21
21
|
references?: JudgeReference[];
|
|
22
22
|
runDir: string;
|
|
@@ -84,6 +84,8 @@ export interface RegradeRunOptions {
|
|
|
84
84
|
* cleanly, so spending judge calls on them buys nothing.
|
|
85
85
|
*/
|
|
86
86
|
onlySuspect?: boolean;
|
|
87
|
+
/** Rejudge only these recorded scenarios; untouched verdicts remain historical. */
|
|
88
|
+
only?: string[];
|
|
87
89
|
/** Rejudge only retained repetitions containing ERROR criterion votes, with the recorded judge. */
|
|
88
90
|
onlyUnparsed?: boolean;
|
|
89
91
|
}
|
package/dist/regrade.js
CHANGED
|
@@ -9,6 +9,8 @@ import { appendJournal } from "./journal.js";
|
|
|
9
9
|
import { rubricDigest, personaDigest, RUBRIC_PREFIX, PERSONA_KEY } from "./sources.js";
|
|
10
10
|
import { providerFailureFromTranscript, executionFailureFromTranscript } from "./provider-failure.js";
|
|
11
11
|
import { loadExecutionFacts, transcriptResponses } from "./judge-evidence.js";
|
|
12
|
+
import { withResourceMonitoring } from "./resource-monitor.js";
|
|
13
|
+
import { throwIfProcessAborted } from "./util/process.js";
|
|
12
14
|
/**
|
|
13
15
|
* Carry a run's recorded hashes forward, refreshing only the `rubric:` keys this
|
|
14
16
|
* re-grade actually judged under (plus the persona, which applies to all of them).
|
|
@@ -21,7 +23,7 @@ import { loadExecutionFacts, transcriptResponses } from "./judge-evidence.js";
|
|
|
21
23
|
* No hashes recorded (a pre-`source_hashes` run) stays that way: inventing hashes for
|
|
22
24
|
* a run that never recorded any would claim a coverage it does not have.
|
|
23
25
|
*/
|
|
24
|
-
export function refreshRubricHashes(recorded, spec, judgedIds, specDir, references) {
|
|
26
|
+
export function refreshRubricHashes(recorded, spec, judgedIds, specDir, references, refreshPersona = true) {
|
|
25
27
|
if (!recorded)
|
|
26
28
|
return undefined;
|
|
27
29
|
const next = { ...recorded };
|
|
@@ -33,7 +35,7 @@ export function refreshRubricHashes(recorded, spec, judgedIds, specDir, referenc
|
|
|
33
35
|
if (s && RUBRIC_PREFIX + id in next)
|
|
34
36
|
next[RUBRIC_PREFIX + id] = rubricDigest(s, specDir, references?.[id]);
|
|
35
37
|
}
|
|
36
|
-
if (PERSONA_KEY in next)
|
|
38
|
+
if (refreshPersona && PERSONA_KEY in next)
|
|
37
39
|
next[PERSONA_KEY] = personaDigest(spec.judge_persona);
|
|
38
40
|
return next;
|
|
39
41
|
}
|
|
@@ -177,6 +179,7 @@ function hasUnparsedVotes(panel) {
|
|
|
177
179
|
* ResultsFile. Shared by `cmdGrade` and the pi-extension's `judge` command.
|
|
178
180
|
*/
|
|
179
181
|
export async function regradeRun(opts) {
|
|
182
|
+
throwIfProcessAborted();
|
|
180
183
|
const { runDir, spec, adapter, specDir } = opts;
|
|
181
184
|
const now = opts.now ?? (() => new Date().toISOString());
|
|
182
185
|
const prev = existsSync(join(runDir, "results.yaml")) ? readResults(runDir) : null;
|
|
@@ -208,15 +211,23 @@ export async function regradeRun(opts) {
|
|
|
208
211
|
const specById = new Map(spec.scenarios.map((s) => [s.id, s]));
|
|
209
212
|
const recorded = prev?.scenarios ?? spec.scenarios.map((s) => ({ id: s.id }));
|
|
210
213
|
let targets = recorded.map((s) => s.id);
|
|
214
|
+
if (opts.only !== undefined) {
|
|
215
|
+
if (!opts.only.length || opts.only.some(id => !id.trim()) || new Set(opts.only).size !== opts.only.length)
|
|
216
|
+
throw new Error("Selected scenarios must be a nonempty unique ID list");
|
|
217
|
+
const unknown = opts.only.filter(id => !targets.includes(id));
|
|
218
|
+
if (unknown.length)
|
|
219
|
+
throw new Error(`Unknown recorded scenario(s): ${unknown.join(", ")}`);
|
|
220
|
+
targets = targets.filter(id => opts.only.includes(id));
|
|
221
|
+
}
|
|
211
222
|
if (opts.onlySuspect) {
|
|
212
223
|
if (!prev)
|
|
213
224
|
throw new Error(`--suspect-only needs a prior results.yaml in ${runDir}`);
|
|
214
|
-
targets = prev.scenarios
|
|
225
|
+
targets = prev.scenarios.filter(s => targets.includes(s.id))
|
|
215
226
|
.filter((s) => s.suspect || s.judge_verdict === "JUDGE-AMBIGUOUS")
|
|
216
227
|
.map((s) => s.id);
|
|
217
228
|
}
|
|
218
229
|
if (opts.onlyUnparsed)
|
|
219
|
-
targets = recorded.filter(s => s.rep_judgments?.some(hasUnparsedVotes)).map(s => s.id);
|
|
230
|
+
targets = recorded.filter(s => targets.includes(s.id) && s.rep_judgments?.some(hasUnparsedVotes)).map(s => s.id);
|
|
220
231
|
// Schema-3 delivery is a prerequisite for behavioral judging. A later `grade`
|
|
221
232
|
// cannot turn an undelivered or unauthenticated transcript into product evidence.
|
|
222
233
|
if (prev?.schema === 3) {
|
|
@@ -227,6 +238,8 @@ export async function regradeRun(opts) {
|
|
|
227
238
|
// Nothing judgeable — a no-op, preserving both evidence and judge identity.
|
|
228
239
|
return prev;
|
|
229
240
|
}
|
|
241
|
+
if (prev && targets.length !== recorded.length && (judge.provider !== prev.judge.provider || judge.model !== prev.judge.model))
|
|
242
|
+
throw new Error("Selective regrade must use the recorded judge; regrade the whole run to change judge");
|
|
230
243
|
const completeTranscripts = (record) => {
|
|
231
244
|
const expected = record.reps ?? 1;
|
|
232
245
|
const files = findTranscriptFiles(runDir, record.id, mode);
|
|
@@ -266,75 +279,79 @@ export async function regradeRun(opts) {
|
|
|
266
279
|
}
|
|
267
280
|
}
|
|
268
281
|
const references = Object.fromEntries(targets.map(id => [id, resolveJudgeReferences(specById.get(id), specDir)]));
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
282
|
+
return withResourceMonitoring(runDir, async () => {
|
|
283
|
+
const targetSet = new Set(targets);
|
|
284
|
+
const scenarioResults = [];
|
|
285
|
+
for (const rec of recorded) {
|
|
286
|
+
throwIfProcessAborted();
|
|
287
|
+
const id = rec.id;
|
|
288
|
+
if (!targetSet.has(id)) {
|
|
289
|
+
scenarioResults.push(rec); // clean verdict, carried verbatim (onlySuspect)
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const scenario = specById.get(id); // guaranteed present by the guard above
|
|
293
|
+
const prevScenario = prev?.scenarios.find((s) => s.id === id);
|
|
294
|
+
const threshold = effectiveThreshold(prevScenario, scenario);
|
|
295
|
+
const rr = await regradeScenario({
|
|
296
|
+
runDir, spec, scenario, adapter, judge, specDir, threshold, mode, now,
|
|
297
|
+
references: references[id],
|
|
298
|
+
expectedReps: prevScenario?.reps ?? 1, onlyUnparsed: opts.onlyUnparsed, prior: prevScenario,
|
|
299
|
+
...(prev?.model.includes(":") ? { subject: parseModelRef(prev.model) } : {}), piVersion: prev?.harness_cli_version,
|
|
300
|
+
});
|
|
301
|
+
const carry = overrides.get(id);
|
|
302
|
+
if (prev?.schema === 3)
|
|
303
|
+
rr.criterion_count = scenario.checklist.length;
|
|
304
|
+
rr.metrics = mergeScenarioMetrics(carry?.metrics, rr.metrics);
|
|
305
|
+
rr.rep_judgments = carryRepObjectives(rr.rep_judgments, carry?.rep_judgments);
|
|
306
|
+
// Gates still describe the original subject execution. An adjudication of
|
|
307
|
+
// an untouched repetition also survives, bounded by rebuild's conflict guards.
|
|
308
|
+
const carryAdjudication = opts.onlyUnparsed && carry?.adjudication && !hasUnparsedVotes(carry.rep_judgments?.find(panel => panel.repetition === (carry.adjudication.repetition ?? 0)));
|
|
309
|
+
scenarioResults.push(rebuildScenarioResult(rr, carry, { objective: "carry", adjudication: carryAdjudication ? "carry" : "drop" }));
|
|
276
310
|
}
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
// CURRENT checklist and persona to those transcripts, so "the verdicts reflect
|
|
323
|
-
// today's rubric" is now a true statement about the record, and the hashes should
|
|
324
|
-
// say so. Doctrine narrowed 0.4.0, from "recorded hashes stay" to "recorded
|
|
325
|
-
// *stimulus* hashes stay" — see refreshRubricHashes.
|
|
326
|
-
partial: prev?.partial,
|
|
327
|
-
source_hashes: opts.onlyUnparsed ? prev?.source_hashes : refreshRubricHashes(prev?.source_hashes, spec, targets, opts.specDir, references),
|
|
328
|
-
source_hash_roots: prev?.source_hash_roots,
|
|
329
|
-
scenarios: scenarioResults,
|
|
330
|
-
}, ctx);
|
|
331
|
-
const g = results.effective_grade;
|
|
332
|
-
if (ctx) {
|
|
333
|
-
appendJournal(runDir, {
|
|
334
|
-
event: "score", ts: now(),
|
|
335
|
-
passed: g.passed, total: g.total, pct: g.pct, letter: g.letter, ship: g.ship, note: g.note,
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
return results;
|
|
311
|
+
const ctx = scoreContextFor({ mode, partial: prev?.partial }, spec);
|
|
312
|
+
const results = writeResults(runDir, {
|
|
313
|
+
schema: prev?.schema,
|
|
314
|
+
subject_invocations: prev?.subject_invocations,
|
|
315
|
+
skill: spec.skill,
|
|
316
|
+
harness: prev?.harness ?? "pi",
|
|
317
|
+
// The harness CLI that produced these transcripts, carried verbatim: a re-grade
|
|
318
|
+
// re-asks the judge, it does not re-deliver the skill, so stamping today's pi
|
|
319
|
+
// here would credit the old transcripts to a version that never ran them.
|
|
320
|
+
harness_cli_version: prev?.harness_cli_version,
|
|
321
|
+
delivery_canary: prev?.delivery_canary,
|
|
322
|
+
delivery_observations: prev?.delivery_observations,
|
|
323
|
+
// The arm is provenance of the MEASUREMENT, not of this rewrite, and it is the
|
|
324
|
+
// only record that a `+<arm>` run actually delegated: rebuilding the draft
|
|
325
|
+
// field-by-field without it silently deleted `definitions`/`ledger_events`
|
|
326
|
+
// from any arm run that was ever re-graded, leaving a record
|
|
327
|
+
// indistinguishable from a vacuous arm. Same reason `harness_cli_version`,
|
|
328
|
+
// `delivery_canary` and `source_hashes` are carried here.
|
|
329
|
+
arm: prev?.arm,
|
|
330
|
+
model: prev?.model ?? "unknown",
|
|
331
|
+
judge: { provider: judge.provider, model: judge.model },
|
|
332
|
+
timestamp: prev?.timestamp ?? now(),
|
|
333
|
+
label: prev?.label ?? null,
|
|
334
|
+
mode,
|
|
335
|
+
// A re-grade judges the SAVED transcripts, which were produced by the OLD text —
|
|
336
|
+
// the recorded **stimulus** hashes stay, keeping an honestly-stale run honestly
|
|
337
|
+
// stale. The rubric hashes are a different matter: this re-grade applied the
|
|
338
|
+
// CURRENT checklist and persona to those transcripts, so "the verdicts reflect
|
|
339
|
+
// today's rubric" is now a true statement about the record, and the hashes should
|
|
340
|
+
// say so. Doctrine narrowed 0.4.0, from "recorded hashes stay" to "recorded
|
|
341
|
+
// *stimulus* hashes stay" — see refreshRubricHashes.
|
|
342
|
+
partial: prev?.partial,
|
|
343
|
+
source_hashes: opts.onlyUnparsed ? prev?.source_hashes : refreshRubricHashes(prev?.source_hashes, spec, targets, opts.specDir, references, targets.length === recorded.length),
|
|
344
|
+
source_hash_roots: prev?.source_hash_roots,
|
|
345
|
+
scenarios: scenarioResults,
|
|
346
|
+
}, ctx);
|
|
347
|
+
const g = results.effective_grade;
|
|
348
|
+
if (ctx) {
|
|
349
|
+
appendJournal(runDir, {
|
|
350
|
+
event: "score", ts: now(),
|
|
351
|
+
passed: g.passed, total: g.total, pct: g.pct, letter: g.letter, ship: g.ship, note: g.note,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return results;
|
|
355
|
+
});
|
|
339
356
|
}
|
|
340
357
|
//# sourceMappingURL=regrade.js.map
|
package/dist/rescore.js
CHANGED
|
@@ -95,6 +95,7 @@ export function rescoreRun(opts) {
|
|
|
95
95
|
// threshold to reps the recorded harness already produced.
|
|
96
96
|
harness_cli_version: prev.harness_cli_version,
|
|
97
97
|
delivery_canary: prev.delivery_canary,
|
|
98
|
+
delivery_observations: prev.delivery_observations,
|
|
98
99
|
// The arm is provenance of the MEASUREMENT, not of this rewrite, and it is the
|
|
99
100
|
// only record that a `+<arm>` run actually delegated: rebuilding the draft
|
|
100
101
|
// field-by-field without it silently deleted `definitions`/`ledger_events`
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type ProcessScope } from "./util/process.js";
|
|
2
|
+
/** Per-operation metadata only, with bounded retention and low-frequency atomic snapshots. */
|
|
3
|
+
export declare function withResourceMonitoring<T>(directory: string, work: () => Promise<T>, options?: ProcessScope): Promise<T>;
|