skill-harness 0.23.0 → 0.24.1

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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +144 -31
  3. package/package.json +12 -2
package/README.md CHANGED
@@ -6,6 +6,10 @@ spec it will run each scenario, LLM-judge the transcript, score it against a shi
6
6
  bar, and open an interactive review UI so you can measure a `SKILL.md` edit.
7
7
  Subject, judge, and spec-drafting model calls all run through Pi.
8
8
 
9
+ Fresh subject execution requires **exactly Pi 1.0.4**. Different or unavailable
10
+ versions produce execution ERROR before a subject call. Retained results remain
11
+ readable; the historical Pi 0.83.0 raw parser is not current qualification.
12
+
9
13
  ## Install
10
14
 
11
15
  ```bash
package/dist/index.js CHANGED
@@ -5836,6 +5836,11 @@ function redactValue(value, homeDir, depth) {
5836
5836
  }
5837
5837
 
5838
5838
  // packages/core/dist/execution-trace.js
5839
+ var QUALIFIED_PI_VERSION = "1.0.4";
5840
+ var LEGACY_PI_TRACE_VERSION = "0.83.0";
5841
+ function piQualificationFailure(version) {
5842
+ return version === QUALIFIED_PI_VERSION ? null : `Pi ${version ?? "(version unavailable)"} is unqualified; execution requires exact Pi ${QUALIFIED_PI_VERSION}`;
5843
+ }
5839
5844
  var SKIPPED = /* @__PURE__ */ new Set(["message_update", "tool_execution_update"]);
5840
5845
  var MAX_DETAILS_CHARS = 2e3;
5841
5846
  function parseTrace(lines, meta) {
@@ -5846,6 +5851,13 @@ function parseTrace(lines, meta) {
5846
5851
  let completionCounter = 0;
5847
5852
  let malformedLines = 0;
5848
5853
  let sawTerminal = false;
5854
+ const settledContract = meta.piVersion !== LEGACY_PI_TRACE_VERSION;
5855
+ const qualificationFailure = settledContract ? piQualificationFailure(meta.piVersion) : null;
5856
+ let sawSettled = false;
5857
+ let lastStopReason;
5858
+ let currentAssistantText = "";
5859
+ let eligibleFinal = false;
5860
+ const captureErrors = [];
5849
5861
  let finalText = "";
5850
5862
  let lastAssistantText = "";
5851
5863
  let inputTokens = null;
@@ -5865,13 +5877,35 @@ function parseTrace(lines, meta) {
5865
5877
  malformedLines++;
5866
5878
  continue;
5867
5879
  }
5880
+ if (!ev || typeof ev !== "object" || Array.isArray(ev)) {
5881
+ malformedLines++;
5882
+ continue;
5883
+ }
5868
5884
  const type2 = ev.type;
5869
- if (typeof type2 !== "string" || SKIPPED.has(type2))
5885
+ if (typeof type2 !== "string") {
5886
+ malformedLines++;
5887
+ continue;
5888
+ }
5889
+ if (settledContract && ["agent_start", "turn_start", "message_start", "message_update", "message_end", "tool_execution_start", "tool_execution_end"].includes(type2)) {
5890
+ sawSettled = false;
5891
+ if (type2 !== "tool_execution_end") {
5892
+ currentAssistantText = "";
5893
+ lastStopReason = void 0;
5894
+ eligibleFinal = false;
5895
+ }
5896
+ }
5897
+ if (SKIPPED.has(type2))
5870
5898
  continue;
5871
5899
  if (type2 === "tool_execution_start") {
5872
5900
  const id = str2(ev.toolCallId);
5873
- if (!id)
5901
+ if (!id) {
5902
+ captureErrors.push("tool start without an identity");
5903
+ continue;
5904
+ }
5905
+ if (calls.has(id)) {
5906
+ captureErrors.push("duplicate tool start identity");
5874
5907
  continue;
5908
+ }
5875
5909
  calls.set(id, {
5876
5910
  id,
5877
5911
  name: str2(ev.toolName) ?? "(unknown)",
@@ -5889,11 +5923,19 @@ function parseTrace(lines, meta) {
5889
5923
  }
5890
5924
  if (type2 === "tool_execution_end") {
5891
5925
  const id = str2(ev.toolCallId);
5892
- if (!id)
5926
+ if (!id) {
5927
+ captureErrors.push("tool end without an identity");
5893
5928
  continue;
5929
+ }
5894
5930
  const call = calls.get(id);
5895
- if (!call)
5931
+ if (!call) {
5932
+ captureErrors.push("tool end without a matching start");
5933
+ continue;
5934
+ }
5935
+ if (call.completionIndex >= 0) {
5936
+ captureErrors.push("duplicate tool completion identity");
5896
5937
  continue;
5938
+ }
5897
5939
  if (call.completionIndex < 0)
5898
5940
  activeCalls = Math.max(0, activeCalls - 1);
5899
5941
  call.completionIndex = completionCounter++;
@@ -5906,7 +5948,17 @@ function parseTrace(lines, meta) {
5906
5948
  if (type2 === "message_end") {
5907
5949
  sawTerminal = true;
5908
5950
  const msg = ev.message;
5909
- const at = isoTime(msg?.timestamp);
5951
+ if (!msg || typeof msg !== "object" || Array.isArray(msg) || typeof msg.role !== "string") {
5952
+ captureErrors.push("malformed message_end message");
5953
+ continue;
5954
+ }
5955
+ if (msg.role !== "assistant" && msg.role !== "toolResult")
5956
+ continue;
5957
+ if (!Array.isArray(msg.content) || msg.content.some((block) => !block || typeof block !== "object" || Array.isArray(block) || typeof block.type !== "string" || block.type === "text" && typeof block.text !== "string")) {
5958
+ captureErrors.push("malformed message_end content");
5959
+ continue;
5960
+ }
5961
+ const at = isoTime(msg.timestamp);
5910
5962
  if (msg?.role === "assistant" && at) {
5911
5963
  for (const block of msg.content ?? []) {
5912
5964
  if (block.type !== "toolCall" || typeof block.id !== "string")
@@ -5926,6 +5978,9 @@ function parseTrace(lines, meta) {
5926
5978
  if (msg?.role !== "assistant")
5927
5979
  continue;
5928
5980
  const text = assistantText(msg);
5981
+ eligibleFinal = msg.content.every((block) => block.type === "text" || block.type === "thinking");
5982
+ currentAssistantText = text;
5983
+ lastStopReason = msg.stopReason;
5929
5984
  if (text) {
5930
5985
  lastAssistantText = text;
5931
5986
  if (msg.stopReason === "stop")
@@ -5939,10 +5994,19 @@ function parseTrace(lines, meta) {
5939
5994
  }
5940
5995
  if (type2 === "turn_end" || type2 === "agent_end" || type2 === "agent_settled") {
5941
5996
  sawTerminal = true;
5997
+ if (type2 === "agent_settled")
5998
+ sawSettled = true;
5942
5999
  continue;
5943
6000
  }
5944
6001
  }
5945
6002
  const toolCalls = [...calls.values()].sort((a, b) => a.issueIndex - b.issueIndex);
6003
+ if (malformedLines > 0)
6004
+ captureErrors.push(`pi JSONL contained ${malformedLines} malformed line(s); trace evidence is incomplete`);
6005
+ const finalStatus = qualificationFailure ? "unqualified" : !sawSettled || activeCalls > 0 ? "incomplete" : captureErrors.length > 0 ? "unavailable" : lastStopReason === "error" ? "error" : lastStopReason === "aborted" ? "aborted" : lastStopReason === "length" ? "truncated" : lastStopReason === "stop" && eligibleFinal && currentAssistantText.length > 0 ? "complete" : "unavailable";
6006
+ if (settledContract && finalStatus !== "complete") {
6007
+ const reason = qualificationFailure ?? (finalStatus === "incomplete" ? "current settlement is missing or tool calls remain outstanding" : finalStatus === "truncated" ? "settled assistant reached its output length limit" : finalStatus === "error" || finalStatus === "aborted" ? `settled assistant stop reason is ${lastStopReason}` : "current terminal assistant content is empty, ineligible, or captured evidence is malformed");
6008
+ captureErrors.push(`Pi ${meta.piVersion ?? "(version unavailable)"} final delivery is ${finalStatus}; ${reason}`);
6009
+ }
5946
6010
  const metrics = {
5947
6011
  input_tokens: inputTokens,
5948
6012
  output_tokens: outputTokens,
@@ -5970,7 +6034,9 @@ function parseTrace(lines, meta) {
5970
6034
  // and `smoke-real-pi.sh` asserts no `/home/` survives into a persisted trace
5971
6035
  // — an assertion that used to pass only because the smoke model happened not
5972
6036
  // to echo one.
5973
- final_text: redactText(finalText || lastAssistantText, meta.homeDir),
6037
+ final_text: redactText(settledContract ? currentAssistantText : finalText || lastAssistantText, meta.homeDir),
6038
+ ...settledContract ? { final_status: finalStatus } : {},
6039
+ ...captureErrors.length ? { capture_errors: [...new Set(captureErrors)] } : {},
5974
6040
  tool_calls: toolCalls,
5975
6041
  // `null`, not `[]`: the stream says nothing about the filesystem. The runner
5976
6042
  // overwrites this after observing the workspace. Defaulting to `[]` claimed
@@ -5982,13 +6048,13 @@ function parseTrace(lines, meta) {
5982
6048
  metrics
5983
6049
  };
5984
6050
  trace.trace_sha256 = traceSha256(trace);
5985
- return { trace, isComplete: sawTerminal, malformedLines };
6051
+ return { trace, isComplete: settledContract ? !qualificationFailure && sawSettled && activeCalls === 0 : sawTerminal, malformedLines };
5986
6052
  }
5987
6053
  function addReported(current, value) {
5988
6054
  return typeof value === "number" && value > 0 ? (current ?? 0) + value : current;
5989
6055
  }
5990
6056
  function assistantText(msg) {
5991
- return (msg.content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
6057
+ return (msg.content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
5992
6058
  }
5993
6059
  function resultMeta(result, homeDir) {
5994
6060
  const body = JSON.stringify(result?.content ?? result ?? null);
@@ -6124,6 +6190,15 @@ import { join as join10 } from "node:path";
6124
6190
 
6125
6191
  // packages/core/dist/provider-failure.js
6126
6192
  var PROVIDER_FAILURE_MARKER = "[skill-harness] provider failure:";
6193
+ var EXECUTION_FAILURE_MARKER = "[skill-harness] execution failure:";
6194
+ function withExecutionFailure(transcript, failure) {
6195
+ return failure ? `${EXECUTION_FAILURE_MARKER} ${failure}
6196
+
6197
+ ${transcript}` : transcript;
6198
+ }
6199
+ function executionFailureFromTranscript(transcript) {
6200
+ return failureFromPreamble(transcript, EXECUTION_FAILURE_MARKER);
6201
+ }
6127
6202
  var TURN_HEADER_PREFIX = ">>> ";
6128
6203
  function withProviderFailure(transcript, failure) {
6129
6204
  return failure ? `${PROVIDER_FAILURE_MARKER} ${failure}
@@ -6154,11 +6229,14 @@ function providerFailureFromJsonLine(line) {
6154
6229
  return null;
6155
6230
  }
6156
6231
  function providerFailureFromTranscript(transcript) {
6232
+ return failureFromPreamble(transcript, PROVIDER_FAILURE_MARKER);
6233
+ }
6234
+ function failureFromPreamble(transcript, marker) {
6157
6235
  for (const line of transcript.split("\n")) {
6158
6236
  if (line.startsWith(TURN_HEADER_PREFIX))
6159
6237
  return null;
6160
- if (line.startsWith(PROVIDER_FAILURE_MARKER))
6161
- return line.slice(PROVIDER_FAILURE_MARKER.length).trim();
6238
+ if (line.startsWith(marker))
6239
+ return line.slice(marker.length).trim();
6162
6240
  }
6163
6241
  return null;
6164
6242
  }
@@ -6183,8 +6261,9 @@ async function judgeOneRep(opts) {
6183
6261
  const startedAt = performance.now();
6184
6262
  const repField = rep === void 0 ? {} : { rep };
6185
6263
  const providerFailure = providerFailureFromTranscript(transcript);
6186
- if (providerFailure) {
6187
- const reason = `provider failure \u2014 ${providerFailure}`;
6264
+ const executionFailure = executionFailureFromTranscript(transcript);
6265
+ if (providerFailure || executionFailure) {
6266
+ const reason = providerFailure ? `provider failure \u2014 ${providerFailure}` : `execution failure \u2014 ${executionFailure}`;
6188
6267
  appendJournal(runDir, { event: "judge-verdict", ts: now(), id: scenario.id, verdict: "ERROR", reason, suspect: false, ...repField });
6189
6268
  return {
6190
6269
  verdict: "ERROR",
@@ -6945,6 +7024,7 @@ async function runRep(scenario, rep, repCount, ctx) {
6945
7024
  }
6946
7025
  infrastructureFailure = null;
6947
7026
  adapterFailure = null;
7027
+ let executionUnavailable = false;
6948
7028
  try {
6949
7029
  if (scenario.mode === "seeded") {
6950
7030
  const r = await runSeeded(scenario, {
@@ -6996,8 +7076,11 @@ async function runRep(scenario, rep, repCount, ctx) {
6996
7076
  const structured = await ctx.adapter.runStructured({ ...req, scenarioId: scenario.id, rep });
6997
7077
  transcript = structured.transcript;
6998
7078
  traces = structured.traces;
7079
+ executionUnavailable = Boolean(structured.executionFailure);
6999
7080
  if (structured.providerFailure)
7000
7081
  infrastructureFailure = `provider failure \u2014 ${structured.providerFailure}`;
7082
+ else if (structured.executionFailure)
7083
+ infrastructureFailure = `execution failure \u2014 ${structured.executionFailure}`;
7001
7084
  } else {
7002
7085
  transcript = await ctx.adapter.run(req);
7003
7086
  }
@@ -7013,9 +7096,16 @@ async function runRep(scenario, rep, repCount, ctx) {
7013
7096
  const provider = providerFailureFromTranscript(transcript);
7014
7097
  if (provider)
7015
7098
  infrastructureFailure = `provider failure \u2014 ${provider}`;
7099
+ else {
7100
+ const execution = executionFailureFromTranscript(transcript);
7101
+ if (execution)
7102
+ infrastructureFailure = `execution failure \u2014 ${execution}`;
7103
+ }
7016
7104
  }
7017
- noResponse = hasEmptyAssistantTurn(transcript);
7018
- if (!noResponse && !adapterFailure)
7105
+ executionUnavailable ||= executionFailureFromTranscript(transcript) !== null;
7106
+ const deliveredText = traces.length > 0 && traces.every((trace) => trace.final_status === "complete" && trace.final_text.length > 0 && !trace.capture_errors?.length);
7107
+ noResponse = !deliveredText && hasEmptyAssistantTurn(transcript);
7108
+ if (executionUnavailable || !noResponse && !adapterFailure)
7019
7109
  break;
7020
7110
  }
7021
7111
  if (adapterFailure && !infrastructureFailure) {
@@ -7574,8 +7664,17 @@ function runPiJson(opts) {
7574
7664
  rl.on("line", (line) => {
7575
7665
  if (!line.trim())
7576
7666
  return;
7577
- if (SKIPPED_TYPE_RE.test(line))
7667
+ if (SKIPPED_TYPE_RE.test(line)) {
7668
+ try {
7669
+ const event = JSON.parse(line);
7670
+ if (event.type === "message_update" && kept.at(-1) !== '{"type":"message_update"}') {
7671
+ kept.push('{"type":"message_update"}');
7672
+ }
7673
+ } catch {
7674
+ kept.push("null");
7675
+ }
7578
7676
  return;
7677
+ }
7579
7678
  kept.push(line);
7580
7679
  if (providerFailure === null)
7581
7680
  providerFailure = providerFailureFromJsonLine(line);
@@ -7749,12 +7848,12 @@ var piAdapter = {
7749
7848
  * results.yaml as `harness_cli_version`.
7750
7849
  *
7751
7850
  * Null on any failure — a non-zero exit, empty output, or pi missing entirely.
7752
- * A run must not abort because provenance was unavailable, and a fabricated
7753
- * version would be worse than an absent one.
7851
+ * The probe never fabricates a version. Fresh subject execution refuses an
7852
+ * unavailable or unqualified result before starting the model.
7754
7853
  */
7755
- async version() {
7854
+ async version(options) {
7756
7855
  try {
7757
- const r = await exec("pi", ["--version"], { timeoutMs: 3e4 });
7856
+ const r = await exec("pi", ["--version"], { timeoutMs: 3e4, ...options });
7758
7857
  const line = r.stdout.split("\n")[0]?.trim() ?? "";
7759
7858
  if (r.code !== 0 || line === "")
7760
7859
  return null;
@@ -7779,9 +7878,12 @@ var piAdapter = {
7779
7878
  req.model.model
7780
7879
  ];
7781
7880
  const flags = req.systemPromptFile ? ["--no-skills", "--append-system-prompt", readFileSync15(req.systemPromptFile, "utf8")] : skillFlags(req.mode, req.skillDir);
7881
+ const env = req.armEnv ? { ...process.env, ...req.armEnv } : void 0;
7882
+ const qualificationFailure = piQualificationFailure(await this.version({ cwd: req.cwd, env }));
7883
+ if (qualificationFailure)
7884
+ return withExecutionFailure("", qualificationFailure);
7782
7885
  const total = req.turns.length;
7783
7886
  const parts = [];
7784
- const env = req.armEnv ? { ...process.env, ...req.armEnv } : void 0;
7785
7887
  let providerFailure = null;
7786
7888
  if (total === 1) {
7787
7889
  const args = [...flags, ...common2, "--no-session", "-p", req.turns[0]];
@@ -7842,13 +7944,21 @@ ${r.stderr.trim()}
7842
7944
  req.model.model
7843
7945
  ];
7844
7946
  const flags = req.systemPromptFile ? ["--no-skills", "--append-system-prompt", readFileSync15(req.systemPromptFile, "utf8")] : skillFlags(req.mode, req.skillDir);
7845
- const piVersion = await this.version();
7947
+ const env = req.armEnv ? { ...process.env, ...req.armEnv } : void 0;
7948
+ const piVersion = await this.version({ cwd: req.cwd, env });
7949
+ const qualificationFailure = piQualificationFailure(piVersion);
7950
+ if (qualificationFailure)
7951
+ return {
7952
+ transcript: withExecutionFailure("", qualificationFailure),
7953
+ traces: [],
7954
+ executionFailure: qualificationFailure
7955
+ };
7846
7956
  const total = req.turns.length;
7847
7957
  const traces = [];
7848
7958
  const parts = [];
7849
7959
  const session = total === 1 ? null : mkdtempSync2(join22(tmpdir2(), "sc-pi-session-"));
7850
7960
  let providerFailure = null;
7851
- const env = req.armEnv ? { ...process.env, ...req.armEnv } : void 0;
7961
+ let executionFailure = null;
7852
7962
  for (let i = 0; i < total; i++) {
7853
7963
  const turnFlags = session === null ? ["--no-session"] : i === 0 ? ["--session-dir", session] : ["--session-dir", session, "-c"];
7854
7964
  const args = [...flags, ...common2, "--mode", "json", ...turnFlags, "-p", req.turns[i]];
@@ -7865,30 +7975,33 @@ ${r.stderr.trim()}
7865
7975
  homeDir: homedir2(),
7866
7976
  env
7867
7977
  });
7868
- if (!r.isComplete) {
7869
- throw new Error(`pi --mode json produced no terminal events for turn ${i + 1}/${total} (exit ${r.code}${r.malformedLines ? `, ${r.malformedLines} malformed line(s)` : ""})` + (r.stderr.trim() ? `: ${r.stderr.trim()}` : ""));
7978
+ if (r.trace.final_status !== void 0 && (r.trace.final_status !== "complete" || r.trace.capture_errors?.length || r.code !== 0)) {
7979
+ executionFailure = `Pi ${piVersion} turn ${i + 1}/${total}: final delivery ${r.trace.final_status} (exit ${r.code}); ${r.trace.capture_errors?.join("; ") || "successful process completion was not established"}`;
7870
7980
  }
7871
- if (r.malformedLines > 0) {
7872
- r.trace.capture_errors = [`pi JSONL contained ${r.malformedLines} malformed line(s); absence-based trace assertions are unsafe`];
7873
- r.trace.trace_sha256 = traceSha256(r.trace);
7981
+ if (!r.isComplete && !executionFailure) {
7982
+ throw new Error(`pi --mode json produced no terminal events for turn ${i + 1}/${total} (exit ${r.code}${r.malformedLines ? `, ${r.malformedLines} malformed line(s)` : ""})` + (r.stderr.trim() ? `: ${r.stderr.trim()}` : ""));
7874
7983
  }
7875
- if (providerFailure === null && r.providerFailure)
7984
+ if (providerFailure === null && r.providerFailure && r.trace.final_status !== "complete") {
7876
7985
  providerFailure = r.providerFailure;
7986
+ }
7877
7987
  const pricedTrace = priceSubjectUsage(r.trace);
7878
7988
  traces.push(pricedTrace);
7879
7989
  parts.push(header(i + 1, total, req.turns[i]));
7880
7990
  parts.push(`<<< ASSISTANT:
7881
- ${pricedTrace.final_text.trim()}
7991
+ ${pricedTrace.final_text}
7882
7992
  `);
7883
7993
  if (r.code !== 0)
7884
7994
  parts.push(`[pi exited ${r.code} on turn ${i + 1}]
7885
7995
  ${r.stderr.trim()}
7886
7996
  `);
7997
+ if (executionFailure || providerFailure)
7998
+ break;
7887
7999
  }
7888
8000
  return {
7889
- transcript: withProviderFailure(parts.join("\n"), providerFailure),
8001
+ transcript: withProviderFailure(withExecutionFailure(parts.join("\n"), executionFailure), providerFailure),
7890
8002
  traces,
7891
- ...providerFailure ? { providerFailure } : {}
8003
+ ...providerFailure ? { providerFailure } : {},
8004
+ ...executionFailure ? { executionFailure } : {}
7892
8005
  };
7893
8006
  },
7894
8007
  /** Run the judge through Pi: no skills, context files, extensions or session. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-harness",
3
- "version": "0.23.0",
3
+ "version": "0.24.1",
4
4
  "description": "Test/optimize loop for agent skills — run spec'd scenarios on pi, LLM-judge, score, review, re-run",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -31,7 +31,17 @@
31
31
  "README.md"
32
32
  ],
33
33
  "dependencies": {
34
- "@skill-harness/cli": "0.23.0",
34
+ "@skill-harness/cli": "0.24.1"
35
+ },
36
+ "peerDependencies": {
37
+ "typebox": "*"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "typebox": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "devDependencies": {
35
45
  "typebox": "^1.1.38"
36
46
  },
37
47
  "repository": {