skill-harness 0.25.1 → 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.
Files changed (2) hide show
  1. package/dist/index.js +350 -34
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -10234,8 +10234,8 @@ function normalizePiDaddyRecordLedgerV12(raw) {
10234
10234
  const record = parsed;
10235
10235
  if (record.seq !== index + 1) throw new Error(`${where}: sequence gap`);
10236
10236
  if (record.prev !== previous) throw new Error(`${where}: previous line hash mismatch`);
10237
- const { digest: digest2, ...unsigned } = record;
10238
- if (sha4(canonical3(unsigned)) !== digest2) throw new Error(`${where}: record digest mismatch`);
10237
+ const { digest: digest3, ...unsigned } = record;
10238
+ if (sha4(canonical3(unsigned)) !== digest3) throw new Error(`${where}: record digest mismatch`);
10239
10239
  if (ids.has(record.id)) throw new Error(`${where}: duplicate record identity`);
10240
10240
  ids.add(record.id);
10241
10241
  validate2(PI_DADDY_RECORD_V1_GOVERNANCE_SCHEMA2, record.body, `${where} governance body`);
@@ -12933,7 +12933,7 @@ function normalizeLegacyGrant(record, index) {
12933
12933
  const effective = record.effective;
12934
12934
  const denied = record.denied;
12935
12935
  const gated = record.gatedBlocked;
12936
- const digest2 = object3(record.definitionDigest);
12936
+ const digest3 = object3(record.definitionDigest);
12937
12937
  const common2 = {
12938
12938
  event_version: TRAJECTORY_EVENT_VERSION,
12939
12939
  source: "pi-daddy-0.17",
@@ -12952,7 +12952,7 @@ function normalizeLegacyGrant(record, index) {
12952
12952
  gate_outcome: record.gateOutcome,
12953
12953
  human_denied: record.humanDenied === true,
12954
12954
  reason: record.reason,
12955
- definition_name: digest2?.name,
12955
+ definition_name: digest3?.name,
12956
12956
  legacy_schema: "pi-daddy-grant-ledger/0.17"
12957
12957
  });
12958
12958
  const refusal = record.blocked ? legacyRefusalCode(record) : void 0;
@@ -12962,7 +12962,7 @@ function normalizeLegacyGrant(record, index) {
12962
12962
  requested_capabilities: requested,
12963
12963
  effective_capabilities: effective,
12964
12964
  refusal_code: refusal,
12965
- digests: anyDefined({ definition: string(digest2?.sha256) }),
12965
+ digests: anyDefined({ definition: string(digest3?.sha256) }),
12966
12966
  attributes
12967
12967
  });
12968
12968
  const events = [
@@ -13570,7 +13570,7 @@ var init_src = __esm({
13570
13570
 
13571
13571
  // packages/pi-extension/src/index.ts
13572
13572
  import { fileURLToPath as fileURLToPath2 } from "node:url";
13573
- import { basename as basename4, dirname as dirname10, join as join34 } from "node:path";
13573
+ import { basename as basename4, dirname as dirname10, join as join35 } from "node:path";
13574
13574
 
13575
13575
  // packages/pi-extension/src/commands.ts
13576
13576
  init_dist();
@@ -15960,7 +15960,7 @@ async function runViaExtension(opts) {
15960
15960
  }
15961
15961
 
15962
15962
  // packages/pi-extension/src/commands.ts
15963
- var USAGE = "usage: /skill-harness run [skill] [--model p:m] [--reps N] [--mode red|green|force] [--canary] [--judge p:m] | judge [run-dir] | review [skill] | coverage [skill] | jev enable|run|status|disable";
15963
+ var USAGE = "usage: /skill-harness run [skill] [--model p:m] [--reps N] [--mode red|green|force] [--canary] [--judge p:m] | judge [run-dir] | review [skill] | coverage [skill] | jev enable [workflow]|run|status|disable";
15964
15964
  function parse(argstr) {
15965
15965
  const tokens = argstr.trim().length ? argstr.trim().split(/\s+/) : [];
15966
15966
  const [sub = "", ...rest] = tokens;
@@ -17368,7 +17368,7 @@ var RESEARCH_OPTIONAL = {
17368
17368
  "run-pi": ["pi-node", "auth-path", "timeout-ms"]
17369
17369
  };
17370
17370
  async function researchCommand(command, flags, helpers, options) {
17371
- const { cases, jsonFile: jsonFile2, textFile: textFile2, writeNew: writeNew2, readLegacyRun } = helpers;
17371
+ const { cases, jsonFile: jsonFile2, textFile: textFile2, writeNew: writeNew3, readLegacyRun } = helpers;
17372
17372
  const { emit = console.log, piRunner } = options;
17373
17373
  if (command === "fixtures") {
17374
17374
  const f = createLearningFixtures({});
@@ -17387,7 +17387,7 @@ async function researchCommand(command, flags, helpers, options) {
17387
17387
  "label-evidence.json": refs,
17388
17388
  "consents.json": []
17389
17389
  }))
17390
- await writeNew2(join32(dir, name), value);
17390
+ await writeNew3(join32(dir, name), value);
17391
17391
  emit(
17392
17392
  JSON.stringify({
17393
17393
  saved: dir,
@@ -17405,7 +17405,7 @@ async function researchCommand(command, flags, helpers, options) {
17405
17405
  sessionConsent: await jsonFile2(flags.consent),
17406
17406
  experimentEntry: await jsonFile2(flags.entry)
17407
17407
  });
17408
- await writeNew2(flags.out, result);
17408
+ await writeNew3(flags.out, result);
17409
17409
  emit(JSON.stringify({ saved: resolve15(flags.out), trainingReady: false }));
17410
17410
  return;
17411
17411
  }
@@ -17502,7 +17502,7 @@ async function researchCommand(command, flags, helpers, options) {
17502
17502
  }
17503
17503
  const report = compareRuns(cases, labels, manifest, runs);
17504
17504
  report.labelEvidenceReview = evidence;
17505
- await writeNew2(flags.out, report);
17505
+ await writeNew3(flags.out, report);
17506
17506
  emit(
17507
17507
  JSON.stringify({
17508
17508
  saved: resolve15(flags.out),
@@ -17690,8 +17690,9 @@ function normalize3(payload) {
17690
17690
  }
17691
17691
  function deadline(signal) {
17692
17692
  return new Promise((_, reject) => {
17693
- if (signal.aborted) return reject(new Error("provider request timed out"));
17694
- signal.addEventListener("abort", () => reject(new Error("provider request timed out")), { once: true });
17693
+ const fail = () => reject(signal.reason instanceof Error ? signal.reason : new Error("provider request timed out"));
17694
+ if (signal.aborted) return fail();
17695
+ signal.addEventListener("abort", fail, { once: true });
17695
17696
  });
17696
17697
  }
17697
17698
  async function readBounded(response, timedOut) {
@@ -17727,7 +17728,7 @@ async function readBounded(response, timedOut) {
17727
17728
  return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
17728
17729
  }
17729
17730
  function failure(startedAt, error) {
17730
- const publicErrors = ["invalid provider response", "provider request timed out", "provider response exceeded limit"];
17731
+ const publicErrors = ["invalid provider response", "provider request timed out", "provider response exceeded limit", "provider request cancelled"];
17731
17732
  const message = error instanceof Error && publicErrors.includes(error.message) ? error.message : "provider request failed";
17732
17733
  return {
17733
17734
  status: "error",
@@ -17742,6 +17743,7 @@ async function callProvider(provider, model, example, options = {}) {
17742
17743
  const startedAt = performance.now();
17743
17744
  let controller;
17744
17745
  let timer;
17746
+ let cancel;
17745
17747
  try {
17746
17748
  const request = makeRequest(provider, model, example);
17747
17749
  requiredString(options.apiKey, "apiKey", 16 * 1024);
@@ -17752,8 +17754,14 @@ async function callProvider(provider, model, example, options = {}) {
17752
17754
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3e5) {
17753
17755
  throw new TypeError("invalid timeout");
17754
17756
  }
17757
+ if (options.signal !== void 0 && !(options.signal instanceof AbortSignal)) {
17758
+ throw new TypeError("signal must be an AbortSignal");
17759
+ }
17760
+ if (options.signal?.aborted) throw new Error("provider request cancelled");
17755
17761
  controller = new AbortController();
17756
- timer = setTimeout(() => controller.abort(), timeoutMs);
17762
+ cancel = () => controller.abort(new Error("provider request cancelled"));
17763
+ options.signal?.addEventListener("abort", cancel, { once: true });
17764
+ timer = setTimeout(() => controller.abort(new Error("provider request timed out")), timeoutMs);
17757
17765
  const timedOut = deadline(controller.signal);
17758
17766
  const response = await Promise.race([
17759
17767
  (options.fetchImpl ?? fetch)(request.url, {
@@ -17778,6 +17786,7 @@ async function callProvider(provider, model, example, options = {}) {
17778
17786
  return failure(startedAt, error);
17779
17787
  } finally {
17780
17788
  if (timer !== void 0) clearTimeout(timer);
17789
+ if (cancel) options.signal?.removeEventListener("abort", cancel);
17781
17790
  controller?.abort();
17782
17791
  }
17783
17792
  }
@@ -18630,33 +18639,264 @@ async function main(argv, {
18630
18639
  );
18631
18640
  }
18632
18641
 
18642
+ // packages/pi-extension/src/jev-packet.ts
18643
+ import { createHash as createHash19 } from "node:crypto";
18644
+ import { closeSync as closeSync2, fsyncSync as fsyncSync2, lstatSync, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync4, openSync as openSync2, writeFileSync as writeFileSync8 } from "node:fs";
18645
+ import { homedir as homedir6 } from "node:os";
18646
+ import { join as join34 } from "node:path";
18647
+ var JEV_PROVIDER = "jev";
18648
+ var JEV_MODEL = "typesafe/jev-1.13";
18649
+ var JEV_WORKFLOW_LIMIT = 3;
18650
+ var JEV_QUESTION = "Does this handoff account for every explicitly required acceptance check with successful evidence tied to the reported candidate?";
18651
+ var JEV_SOURCE_KIND = "skill-harness-selected-handoff-v1";
18652
+ var digest2 = (text3) => createHash19("sha256").update(text3, "utf8").digest("hex");
18653
+ function requireIdentity(value, name) {
18654
+ if (typeof value !== "string" || !value.trim() || value.length > 256)
18655
+ throw new Error(`${name} must be a nonempty runtime identity of at most 256 characters`);
18656
+ }
18657
+ function prepareHandoff(value) {
18658
+ for (const field of ["candidate", "requirements", "evidence"]) {
18659
+ if (typeof value[field] !== "string" || !value[field].trim() || value[field].length > 32e3)
18660
+ throw new Error(`${field} must be a nonempty bounded string`);
18661
+ if (/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(value[field]))
18662
+ throw new Error(`${field} must contain valid Unicode`);
18663
+ }
18664
+ const input = JSON.stringify({
18665
+ candidate: value.candidate,
18666
+ requirements: value.requirements,
18667
+ evidence: value.evidence
18668
+ });
18669
+ if (Array.from(input).length > 16e3)
18670
+ throw new Error("The complete selected handoff packet must not exceed 16000 characters");
18671
+ return Object.freeze({ input, question: JEV_QUESTION, inputSha256: digest2(input) });
18672
+ }
18673
+ function createHandoffSource(options) {
18674
+ requireIdentity(options.sessionId, "Pi sessionId");
18675
+ requireIdentity(options.toolCallId, "Pi toolCallId");
18676
+ const source = {
18677
+ schema: 1,
18678
+ kind: JEV_SOURCE_KIND,
18679
+ sessionId: options.sessionId,
18680
+ toolCallId: options.toolCallId,
18681
+ frozenAt: (/* @__PURE__ */ new Date()).toISOString(),
18682
+ input: options.packet.input,
18683
+ inputSha256: options.packet.inputSha256,
18684
+ question: JEV_QUESTION,
18685
+ provider: JEV_PROVIDER,
18686
+ model: JEV_MODEL,
18687
+ consent: options.consent,
18688
+ authorization: options.authorization,
18689
+ provenance: "tool-selected-input",
18690
+ candidateIdentity: "caller-claimed",
18691
+ sourceBinding: "unassessed",
18692
+ redaction: "unassessed",
18693
+ rights: "unassessed",
18694
+ labelStatus: "unlabeled",
18695
+ trainingEligible: false,
18696
+ exportEligible: false,
18697
+ publicCaptureVerified: false
18698
+ };
18699
+ const bytes = JSON.stringify(source, null, 2) + "\n";
18700
+ return Object.freeze({ source, bytes, sha256: digest2(bytes) });
18701
+ }
18702
+ function privateDirectory(path) {
18703
+ try {
18704
+ mkdirSync5(path, { mode: 448 });
18705
+ } catch (error) {
18706
+ if (error.code !== "EEXIST") throw error;
18707
+ }
18708
+ const stat4 = lstatSync(path);
18709
+ if (!stat4.isDirectory() || stat4.isSymbolicLink())
18710
+ throw new Error("JEV storage must be a private directory, not a link");
18711
+ if (process.platform !== "win32" && ((stat4.mode & 63) !== 0 || process.getuid && stat4.uid !== process.getuid()))
18712
+ throw new Error("JEV storage directory must be owned by the current user with mode 0700");
18713
+ }
18714
+ function writeNew2(path, bytes) {
18715
+ const fd = openSync2(path, "wx", 384);
18716
+ try {
18717
+ writeFileSync8(fd, bytes);
18718
+ fsyncSync2(fd);
18719
+ } finally {
18720
+ closeSync2(fd);
18721
+ }
18722
+ }
18723
+ function retainHandoffSource(source, assertCurrent, storageHome = join34(homedir6(), ".skill-harness")) {
18724
+ assertCurrent();
18725
+ privateDirectory(storageHome);
18726
+ const root = join34(storageHome, "jev-workflow");
18727
+ privateDirectory(root);
18728
+ const directory = mkdtempSync4(join34(root, "selection-"));
18729
+ const path = join34(directory, "selection.json");
18730
+ writeNew2(path, source.bytes);
18731
+ return {
18732
+ path,
18733
+ writeOutcome(outcome) {
18734
+ assertCurrent();
18735
+ writeNew2(join34(directory, "outcome.json"), JSON.stringify({
18736
+ schema: 1,
18737
+ kind: "skill-harness-handoff-advice-v1",
18738
+ source: { path: "selection.json", sha256: source.sha256 },
18739
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
18740
+ outcome,
18741
+ trainingEligible: false,
18742
+ exportEligible: false,
18743
+ labelStatus: "unlabeled"
18744
+ }, null, 2) + "\n");
18745
+ }
18746
+ };
18747
+ }
18748
+
18633
18749
  // packages/pi-extension/src/jev-session.ts
18634
- function createJevSessionHandler(pi, run = main) {
18750
+ var unmeasuredProvider = () => ({
18751
+ provider: JEV_PROVIDER,
18752
+ requestedModel: JEV_MODEL,
18753
+ resolvedModel: null,
18754
+ usage: { inputTokens: null, outputTokens: null, costUsd: null },
18755
+ latencyMs: null
18756
+ });
18757
+ function createJevController(pi, options = {}) {
18758
+ const run = options.run ?? main;
18635
18759
  let epoch = 0;
18636
18760
  let active = null;
18637
18761
  const clearSession = () => {
18638
18762
  epoch++;
18763
+ active?.abort.abort();
18639
18764
  active = null;
18640
18765
  };
18641
18766
  pi.on("session_start", clearSession);
18642
18767
  pi.on("session_shutdown", clearSession);
18643
- return async (args, ctx) => {
18768
+ const currentId = (ctx) => {
18769
+ const id = ctx.sessionManager?.getSessionId();
18770
+ if (active && active.sessionId !== id) clearSession();
18771
+ return id;
18772
+ };
18773
+ const hasKey = () => !!(options.env ?? process.env).OPENROUTER_API_KEY?.trim();
18774
+ const status = (ctx) => {
18775
+ const id = currentId(ctx);
18776
+ const availability = !id ? "missing-session" : !active ? "disabled" : active.mode === "manual" ? "manual-only" : active.blocked ?? (active.busy ? "busy" : active.used >= JEV_WORKFLOW_LIMIT ? "limit-reached" : !hasKey() ? "missing-key" : "ready");
18777
+ return {
18778
+ enabled: active !== null,
18779
+ mode: active?.mode ?? "disabled",
18780
+ storage: active?.consent.decision ?? null,
18781
+ remaining: active?.mode === "workflow" ? JEV_WORKFLOW_LIMIT - active.used : 0,
18782
+ availability,
18783
+ advisory: true
18784
+ };
18785
+ };
18786
+ const evaluate = async (value, toolCallId, signal, ctx) => {
18787
+ requireIdentity(toolCallId, "Pi toolCallId");
18788
+ const packet = prepareHandoff(value);
18789
+ const id = currentId(ctx);
18790
+ const unavailable = (reason) => ({
18791
+ advisory: true,
18792
+ status: "unavailable",
18793
+ probability: null,
18794
+ reason,
18795
+ ...unmeasuredProvider(),
18796
+ remaining: active?.mode === "workflow" ? JEV_WORKFLOW_LIMIT - active.used : 0
18797
+ });
18798
+ if (!id || !active || active.mode !== "workflow")
18799
+ return unavailable("Explicit /skill-harness jev enable workflow activation is required in this Pi session.");
18800
+ const bound = active;
18801
+ const assertCurrent = () => {
18802
+ if (active !== bound || ctx.sessionManager?.getSessionId() !== bound.sessionId || bound.abort.signal.aborted || signal?.aborted)
18803
+ throw new Error("Session, authorization or tool execution changed");
18804
+ };
18805
+ if (signal?.aborted) return unavailable("Tool execution was cancelled.");
18806
+ const cached = bound.cache.get(packet.inputSha256);
18807
+ if (cached) return { ...cached, remaining: JEV_WORKFLOW_LIMIT - bound.used, reused: true };
18808
+ if (bound.blocked) return unavailable("Workflow advice is suppressed after an error; explicit new activation is required.");
18809
+ if (bound.busy) return unavailable("An advisory request is already in flight; no additional call was made.");
18810
+ if (bound.used >= JEV_WORKFLOW_LIMIT) return unavailable("This activation's advisory call limit is reached.");
18811
+ if (!hasKey()) return unavailable("OPENROUTER_API_KEY is unavailable; no call was made.");
18812
+ const abort = new AbortController();
18813
+ const cancel = () => abort.abort();
18814
+ const signals = [bound.abort.signal, signal].filter((s) => s !== void 0);
18815
+ for (const s of signals) {
18816
+ if (s.aborted) cancel();
18817
+ else s.addEventListener("abort", cancel, { once: true });
18818
+ }
18819
+ bound.busy = true;
18820
+ bound.used++;
18821
+ let receipt;
18822
+ try {
18823
+ assertCurrent();
18824
+ const source = createHandoffSource({
18825
+ packet,
18826
+ sessionId: bound.sessionId,
18827
+ toolCallId,
18828
+ consent: bound.consent,
18829
+ authorization: bound.authorization
18830
+ });
18831
+ const retained = bound.consent.decision === "granted" ? retainHandoffSource(source, assertCurrent, options.storageHome) : void 0;
18832
+ receipt = {
18833
+ kind: JEV_SOURCE_KIND,
18834
+ sha256: source.sha256,
18835
+ sessionId: bound.sessionId,
18836
+ toolCallId,
18837
+ retained: !!retained,
18838
+ ...retained ? { path: retained.path } : {}
18839
+ };
18840
+ assertCurrent();
18841
+ const result = await (options.provider ?? callProvider)(JEV_PROVIDER, JEV_MODEL, packet, {
18842
+ apiKey: (options.env ?? process.env).OPENROUTER_API_KEY,
18843
+ signal: abort.signal
18844
+ });
18845
+ assertCurrent();
18846
+ if (result.status !== "answered") bound.blocked = "provider-error";
18847
+ const advice = {
18848
+ advisory: true,
18849
+ status: result.status === "answered" ? "answered" : "unavailable",
18850
+ probability: result.status === "answered" ? result.probability : null,
18851
+ provider: JEV_PROVIDER,
18852
+ requestedModel: JEV_MODEL,
18853
+ resolvedModel: result.resolvedModel,
18854
+ usage: result.usage,
18855
+ latencyMs: result.latencyMs,
18856
+ ...result.status === "answered" ? {} : { reason: "Provider unavailable; further workflow calls require explicit new activation." },
18857
+ remaining: JEV_WORKFLOW_LIMIT - bound.used,
18858
+ inputSha256: packet.inputSha256,
18859
+ source: receipt
18860
+ };
18861
+ try {
18862
+ retained?.writeOutcome(advice);
18863
+ } catch {
18864
+ assertCurrent();
18865
+ bound.blocked = "storage-error";
18866
+ advice.reason = "Provider outcome received, but outcome retention failed; further workflow calls require explicit new activation.";
18867
+ }
18868
+ bound.cache.set(packet.inputSha256, Object.freeze(advice));
18869
+ return advice;
18870
+ } catch {
18871
+ bound.blocked = "execution-error";
18872
+ return {
18873
+ advisory: true,
18874
+ status: "unavailable",
18875
+ probability: null,
18876
+ ...unmeasuredProvider(),
18877
+ reason: "Advice unavailable or cancelled; no approval is implied. Explicit new activation is required for further workflow calls.",
18878
+ remaining: JEV_WORKFLOW_LIMIT - bound.used,
18879
+ inputSha256: packet.inputSha256,
18880
+ ...receipt ? { source: receipt } : {}
18881
+ };
18882
+ } finally {
18883
+ bound.busy = false;
18884
+ for (const s of signals) s.removeEventListener("abort", cancel);
18885
+ }
18886
+ };
18887
+ const command = async (args, ctx) => {
18644
18888
  const action = args.trim() || "status";
18645
- const sessionId = ctx.sessionManager?.getSessionId();
18889
+ const sessionId = currentId(ctx);
18646
18890
  if (!sessionId) throw Error("JEV requires the current Pi session identity");
18647
- if (active && active.sessionId !== sessionId) {
18648
- epoch++;
18649
- active = null;
18650
- }
18891
+ requireIdentity(sessionId, "Pi sessionId");
18651
18892
  if (action === "status") {
18652
18893
  ctx.ui.notify(
18653
- active ? "JEV enabled; LoRA storage " + active.consent.decision + "." : "JEV disabled for this session."
18894
+ JSON.stringify(status(ctx))
18654
18895
  );
18655
18896
  return;
18656
18897
  }
18657
18898
  if (action === "disable") {
18658
- epoch++;
18659
- active = null;
18899
+ clearSession();
18660
18900
  ctx.ui.notify(
18661
18901
  "JEV disabled. Existing selected records remain local; disable storage in their review before export if needed."
18662
18902
  );
@@ -18666,15 +18906,35 @@ function createJevSessionHandler(pi, run = main) {
18666
18906
  throw Error(
18667
18907
  "Interactive JEV activation requires a per-session storage answer. Use the explicit CLI storage option in noninteractive mode."
18668
18908
  );
18669
- if (action === "enable") {
18670
- const activation = ++epoch;
18671
- active = null;
18909
+ if (action === "enable" || action === "enable workflow") {
18910
+ clearSession();
18911
+ const activation = epoch;
18912
+ const workflow = action === "enable workflow";
18913
+ let authorization = null;
18914
+ if (workflow) {
18915
+ if (!ctx.ui.confirm) throw new Error("Workflow JEV activation requires interactive paid-scope confirmation");
18916
+ const confirmed = await ctx.ui.confirm(
18917
+ "Enable optional JEV handoff advice for THIS Pi session?",
18918
+ `Allow up to ${JEV_WORKFLOW_LIMIT} automatic workflow paid calls to ${JEV_MODEL} through OpenRouter using OPENROUTER_API_KEY. The coordinator may send selected candidate, acceptance requirements and evidence (at most 16000 characters per packet) for the fixed handoff-readiness question when uncertainty remains after deterministic checks, before independent review. Advice never grants approval. This is separate from your Pi subscription; no retry or fallback. Manual jev run calls remain separately confirmed outside this workflow limit.`
18919
+ );
18920
+ if (activation !== epoch || ctx.sessionManager?.getSessionId() !== sessionId || confirmed !== true) return;
18921
+ authorization = {
18922
+ kind: "jev-workflow-paid-scope",
18923
+ interactionId: randomUUID3(),
18924
+ sessionId,
18925
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
18926
+ provider: JEV_PROVIDER,
18927
+ model: JEV_MODEL,
18928
+ question: JEV_QUESTION,
18929
+ maximumCalls: JEV_WORKFLOW_LIMIT
18930
+ };
18931
+ }
18672
18932
  const choices = [
18673
18933
  "No \u2014 use JEV without retaining data for LoRA",
18674
18934
  "Yes \u2014 retain selected decision data for later LoRA review"
18675
18935
  ];
18676
18936
  const answer = await ctx.ui.select(
18677
- "Store selected data from THIS session for future LoRA dataset review? Storage does not grant training permission.",
18937
+ "Store selected data from THIS session for future LoRA dataset review? Workflow selections are saved under ~/.skill-harness/jev-workflow. Storage does not grant training permission; No does not change Pi native session history.",
18678
18938
  choices
18679
18939
  );
18680
18940
  if (activation !== epoch || ctx.sessionManager?.getSessionId() !== sessionId)
@@ -18687,14 +18947,24 @@ function createJevSessionHandler(pi, run = main) {
18687
18947
  recordedAt: (/* @__PURE__ */ new Date()).toISOString()
18688
18948
  });
18689
18949
  pi.appendEntry?.("skill-harness-jev-storage-choice", consent);
18690
- active = { sessionId, consent };
18950
+ active = {
18951
+ sessionId,
18952
+ consent,
18953
+ authorization,
18954
+ mode: workflow ? "workflow" : "manual",
18955
+ abort: new AbortController(),
18956
+ used: 0,
18957
+ busy: false,
18958
+ blocked: null,
18959
+ cache: /* @__PURE__ */ new Map()
18960
+ };
18691
18961
  ctx.ui.notify(
18692
- "JEV enabled for explicitly selected paid calls. LoRA storage " + consent.decision + " for this session only."
18962
+ (workflow ? `JEV workflow advice enabled (up to ${JEV_WORKFLOW_LIMIT} paid calls). LoRA storage ` : "JEV enabled for explicitly selected paid calls. LoRA storage ") + consent.decision + " for this session only."
18693
18963
  );
18694
18964
  return;
18695
18965
  }
18696
18966
  if (action !== "run")
18697
- throw Error("usage: /skill-harness jev enable | status | disable | run");
18967
+ throw Error("usage: /skill-harness jev enable [workflow] | status | disable | run");
18698
18968
  if (!active)
18699
18969
  throw Error(
18700
18970
  "Enable JEV in this session first: /skill-harness jev enable"
@@ -18784,13 +19054,59 @@ function createJevSessionHandler(pi, run = main) {
18784
19054
  }
18785
19055
  );
18786
19056
  };
19057
+ return { command, status, evaluate };
19058
+ }
19059
+
19060
+ // packages/pi-extension/src/jev-advice.ts
19061
+ import { Type as Type3 } from "typebox";
19062
+ function createJevAdviceTool(controller) {
19063
+ return {
19064
+ name: "jev_advice",
19065
+ label: "JEV handoff advice",
19066
+ description: "Check session-local JEV workflow status without a provider call, or evaluate selected handoff evidence against the fixed acceptance-readiness question. Evaluation requires the user's explicit /skill-harness jev enable workflow activation. Advisory probability never grants approval or replaces tests/review. Select only relevant public or already-redacted evidence; do not include secrets, transcripts, reviewer verdicts or hidden reasoning.",
19067
+ promptGuidelines: [
19068
+ "Use jev_advice action=status to discover whether JEV workflow advice is enabled.",
19069
+ "When enabled and useful uncertainty remains after deterministic checks, optionally evaluate a selected handoff before independent review. Do not call for every feature or retry unavailable advice.",
19070
+ "Keep JEV advice out of the independent reviewer's inputs; act on independently verified issues rather than treating its probability as a verdict."
19071
+ ],
19072
+ parameters: Type3.Object({
19073
+ action: Type3.Union([Type3.Literal("status"), Type3.Literal("evaluate")]),
19074
+ candidate: Type3.Optional(Type3.String({ minLength: 1, maxLength: 16e3, description: "Reported candidate identity; this is a claim, not an authenticated Git binding." })),
19075
+ requirements: Type3.Optional(Type3.String({ minLength: 1, maxLength: 16e3, description: "Explicit acceptance checks, before seeing independent review or JEV outcomes." })),
19076
+ evidence: Type3.Optional(Type3.String({ minLength: 1, maxLength: 16e3, description: "Selected decision-time handoff evidence. Complete JSON packet is limited to 16000 characters." }))
19077
+ }, { additionalProperties: false }),
19078
+ async execute(id, params, signal, _onUpdate, ctx) {
19079
+ requireIdentity(id, "Pi toolCallId");
19080
+ if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("Invalid jev_advice parameters");
19081
+ const p = params;
19082
+ const expected = p.action === "status" ? ["action"] : ["action", "candidate", "requirements", "evidence"];
19083
+ if (Object.keys(p).length !== expected.length || expected.some((key) => !Object.hasOwn(p, key)))
19084
+ throw new Error("jev_advice accepts only its documented status or evaluate fields");
19085
+ let details;
19086
+ if (p.action === "status") details = controller.status(ctx);
19087
+ else if (p.action === "evaluate")
19088
+ details = await controller.evaluate(
19089
+ { candidate: p.candidate, requirements: p.requirements, evidence: p.evidence },
19090
+ id,
19091
+ signal,
19092
+ ctx
19093
+ );
19094
+ else throw new Error("jev_advice action must be status or evaluate");
19095
+ return { content: [{ type: "text", text: JSON.stringify(details) }], details };
19096
+ }
19097
+ };
19098
+ }
19099
+ function registerJevAdvice(pi, controller) {
19100
+ pi.registerTool(createJevAdviceTool(controller));
18787
19101
  }
18788
19102
 
18789
19103
  // packages/pi-extension/src/index.ts
18790
19104
  function index_default(pi) {
18791
19105
  const moduleDir = dirname10(fileURLToPath2(import.meta.url));
18792
- const assetsDir = basename4(dirname10(moduleDir)) === "skill-harness" ? join34(moduleDir, "..", "assets") : join34(moduleDir, "..", "..", "..", "assets");
18793
- registerCommand(pi, assetsDir, createJevSessionHandler(pi));
19106
+ const assetsDir = basename4(dirname10(moduleDir)) === "skill-harness" ? join35(moduleDir, "..", "assets") : join35(moduleDir, "..", "..", "..", "assets");
19107
+ const jev = createJevController(pi);
19108
+ registerCommand(pi, assetsDir, jev.command);
19109
+ registerJevAdvice(pi, jev);
18794
19110
  registerTool(pi);
18795
19111
  pi.on("session_shutdown", async () => {
18796
19112
  closeReview();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-harness",
3
- "version": "0.25.1",
3
+ "version": "0.26.0",
4
4
  "description": "Test/optimize loop for agent skills \u2014 run spec'd scenarios on pi, LLM-judge, score, review, re-run",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -31,7 +31,7 @@
31
31
  "README.md"
32
32
  ],
33
33
  "dependencies": {
34
- "@skill-harness/cli": "0.25.1"
34
+ "@skill-harness/cli": "0.26.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "typebox": "*"