@matamt-dbg/dsh-studio 0.3.0 → 0.5.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/index.js CHANGED
@@ -53,10 +53,11 @@ var LEGACY_PACKAGE_NAME = "@local/dsh-studio-shell";
53
53
  var POLICY_PLUGIN = `${PACKAGE_NAME}/policy`;
54
54
  var ROLE_PLUGIN = `${PACKAGE_NAME}/roles`;
55
55
  var BATCH_PLUGIN = `${PACKAGE_NAME}/batch`;
56
+ var JUDGE_PLUGIN = `${PACKAGE_NAME}/judge`;
56
57
  function currentPluginName(name) {
57
58
  return name === LEGACY_PACKAGE_NAME || name.startsWith(`${LEGACY_PACKAGE_NAME}/`) ? PACKAGE_NAME + name.slice(LEGACY_PACKAGE_NAME.length) : name;
58
59
  }
59
- var STUDIO_VERSION = "0.3.0";
60
+ var STUDIO_VERSION = "0.5.0";
60
61
 
61
62
  // src/delegation.ts
62
63
  var ROLE_TOOL = "subagent_role";
@@ -255,6 +256,7 @@ function studioLayer() {
255
256
  return [
256
257
  plugin("spill-policy"),
257
258
  { id: "studio-batch", name: BATCH_PLUGIN },
259
+ { id: "studio-judge", name: JUDGE_PLUGIN },
258
260
  plugin("tool-cordis"),
259
261
  plugin("tool-plugin-manager", void 0, "plugin-manager/tools")
260
262
  ];
@@ -485,7 +487,7 @@ var MasterExtensions = class {
485
487
  };
486
488
 
487
489
  // src/host/shell.ts
488
- import { dirname as dirname8, join as join7 } from "node:path";
490
+ import { dirname as dirname8, join as join8 } from "node:path";
489
491
  import { randomUUID as randomUUID12 } from "node:crypto";
490
492
  import { Remote as Remote2, TypertRemoteService as TypertRemoteService2 } from "@deepseek-ai/dsh-typert-protocol";
491
493
 
@@ -1607,15 +1609,82 @@ import { dirname as dirname3, resolve as resolve2 } from "node:path";
1607
1609
 
1608
1610
  // src/pixie-gate-task.ts
1609
1611
  var GATE_WHENS = ["user-message", "agent-message", "tool-before", "tool-after", "turn-end"];
1610
- var HOOK_EXIT = { continue: 0, skip: 10 };
1612
+ var HOOK_EXIT = { continue: 0, skip: 10, direct: 11 };
1611
1613
  var enabledFor = (task, preset) => task.enabled && task.presetIds.includes(preset) && !task.disabledPresetIds?.includes(preset);
1612
1614
  function taskProblem(task) {
1613
1615
  if (!task.name.trim()) return "name";
1614
- if (!task.instruction.trim()) return "instruction";
1616
+ if (!task.instruction.trim() && task.advanced.script === void 0) return "instruction";
1615
1617
  if (!(task.advanced.timeoutMs > 0) || !Number.isInteger(task.advanced.maxSteers) || task.advanced.maxSteers < 0) return "limits";
1616
1618
  return void 0;
1617
1619
  }
1618
1620
 
1621
+ // src/pixie-gate-task-contracts.ts
1622
+ var object3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
1623
+ var text = (value) => typeof value === "string" && !!value.trim();
1624
+ function validateGateTask(value) {
1625
+ if (!object3(value) || !text(value.id) || !text(value.name) || typeof value.enabled !== "boolean" || !GATE_WHENS.includes(value.when) || typeof value.instruction !== "string") throw new Error("Invalid gate task");
1626
+ const advanced = value.advanced;
1627
+ if (!object3(advanced) || typeof advanced.confirmReplace !== "boolean" || !["pass", "block"].includes(advanced.onError) || !["root", "child", "any"].includes(advanced.agents) || typeof advanced.timeoutMs !== "number" || !Number.isFinite(advanced.timeoutMs) || typeof advanced.maxSteers !== "number" || advanced.showInChat !== void 0 && typeof advanced.showInChat !== "boolean") throw new Error("Invalid gate advanced options");
1628
+ if (advanced.script !== void 0 && (typeof advanced.script !== "string" || (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(advanced.script) || advanced.script.includes("..")))) throw new Error("Invalid gate script filename");
1629
+ if (!Array.isArray(value.presetIds) || !value.presetIds.every(text)) throw new Error("Invalid gate presetIds");
1630
+ if (value.disabledPresetIds !== void 0 && (!Array.isArray(value.disabledPresetIds) || !value.disabledPresetIds.every(text))) throw new Error("Invalid gate disabledPresetIds");
1631
+ const name = typeof advanced.script === "string" ? advanced.script.replace(/\.[^.]+$/, "") : void 0;
1632
+ for (const match of value.instruction.matchAll(/{{\s*([^{}]+?)\s*}}/g)) {
1633
+ if (!name || ![`hooks.${name}.text`, `hooks.${name}.data`].includes(match[1])) throw new Error(`Unknown prompt variable: ${match[1]}`);
1634
+ }
1635
+ const task = structuredClone(value);
1636
+ delete task.advanced.judge;
1637
+ const problem = taskProblem(task);
1638
+ if (problem) throw new Error(`Invalid gate ${problem}`);
1639
+ task.presetIds = [...new Set(task.presetIds)];
1640
+ const disabled = [...new Set(task.disabledPresetIds ?? [])].filter((id) => task.presetIds.includes(id));
1641
+ if (disabled.length) task.disabledPresetIds = disabled;
1642
+ else delete task.disabledPresetIds;
1643
+ return task;
1644
+ }
1645
+ function validateGateTasks(value) {
1646
+ if (!Array.isArray(value)) throw new Error("Gate tasks must be an array");
1647
+ const ids = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Set();
1648
+ return value.map((raw) => {
1649
+ const task = validateGateTask(raw);
1650
+ if (ids.has(task.id)) throw new Error("Duplicate gate id");
1651
+ ids.add(task.id);
1652
+ for (const preset of task.presetIds) {
1653
+ const key = JSON.stringify([preset, task.when]);
1654
+ if (!enabledFor(task, preset)) continue;
1655
+ if (bindings.has(key)) throw new Error("Only one enabled gate per preset and when is supported");
1656
+ bindings.add(key);
1657
+ }
1658
+ return task;
1659
+ });
1660
+ }
1661
+ function parseGateOutput(text2) {
1662
+ const value = JSON.parse(text2);
1663
+ if (!object3(value) || Object.hasOwn(value, "verdict") && !["pass", "stop"].includes(value.verdict)) throw new Error("Gate output needs a pass/stop verdict");
1664
+ for (const [key, field2] of Object.entries(value)) {
1665
+ if (!["verdict", "reason", "replace", "append", "note"].includes(key) || typeof field2 !== "string") throw new Error(`Invalid gate output field: ${key}`);
1666
+ }
1667
+ return { verdict: "pass", ...value };
1668
+ }
1669
+ function gateOutputPlan(task, output, original) {
1670
+ const message = task.when === "user-message" || task.when === "agent-message";
1671
+ const mutable = message || task.when === "tool-after";
1672
+ const changed = output.replace !== void 0 && output.replace !== original;
1673
+ const confirm = message && task.advanced.confirmReplace && changed;
1674
+ const stop = output.verdict === "stop" || confirm;
1675
+ const notes = output.note === void 0 ? [] : [output.note];
1676
+ if (output.replace !== void 0 && (!mutable || stop && !message)) notes.push(output.replace);
1677
+ if (output.append !== void 0 && (!mutable || stop)) notes.push(output.append);
1678
+ return {
1679
+ stop,
1680
+ notes,
1681
+ ...output.reason !== void 0 ? { reason: output.reason } : {},
1682
+ ...stop && message && output.replace !== void 0 ? { suggestion: output.replace } : {},
1683
+ ...!stop && mutable && changed ? { replacement: output.replace } : {},
1684
+ ...!stop && mutable && output.append !== void 0 ? { addition: output.append } : {}
1685
+ };
1686
+ }
1687
+
1619
1688
  // src/host/pixie-gate-script.ts
1620
1689
  function validScriptName(name) {
1621
1690
  return /^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(name) && !name.includes("..");
@@ -1675,8 +1744,16 @@ async function runHookScript(task, target, signal, deps, context, helperEnv = as
1675
1744
  if (output.aborted) throw new Error("Hook script aborted");
1676
1745
  if (output.stdout.truncated || Buffer.byteLength(output.stdout.text) > 256 * 1024) throw new Error("Hook stdout exceeds 256KB");
1677
1746
  if (output.exitCode === HOOK_EXIT.skip) return { skipped: true, instruction: task.instruction };
1678
- if (output.exitCode !== HOOK_EXIT.continue) throw new Error(`Hook script exit ${output.exitCode}: ${output.stderr.text}`);
1747
+ if (output.exitCode !== HOOK_EXIT.continue && output.exitCode !== HOOK_EXIT.direct) throw new Error(`Hook script exit ${output.exitCode}: ${output.stderr.text}`);
1679
1748
  const text2 = output.stdout.text.replace(/^[\r\n]+|[\r\n]+$/g, "");
1749
+ if (output.exitCode === HOOK_EXIT.direct) {
1750
+ try {
1751
+ return { skipped: false, direct: parseGateOutput(text2) };
1752
+ } catch (error) {
1753
+ throw new Error(`Hook script exit 11 needs a gate output JSON object on stdout: ${error instanceof Error ? error.message : String(error)}`);
1754
+ }
1755
+ }
1756
+ if (!task.instruction.trim()) throw new Error("Hook script asked for Pixie (exit 0) but the gate has no instruction");
1680
1757
  let data = "";
1681
1758
  try {
1682
1759
  const value = JSON.parse(text2);
@@ -1726,6 +1803,7 @@ function threadMessages(thread) {
1726
1803
  var DEFAULT_JUDGE_SYSTEM_PROMPT = [
1727
1804
  "You are a judgment component. Evaluate the given state against each question independently.",
1728
1805
  'Reply with exactly one JSON object and nothing else: {"answers": {<question id>: <answer>}}.',
1806
+ "Each question has instructions (what to decide) and criteria: choice criteria map option names to descriptions; score criteria list ordered level descriptions; noul criteria optionally describe what true and false mean.",
1729
1807
  'choice answer: {"choice": <option name>, "probabilities": {<every option name>: number}} \u2014 probabilities sum to 1.',
1730
1808
  'score answer: {"score": number from 0 to levels-1 (fractional allowed), "probabilities": [one number per level, in order]} \u2014 probabilities sum to 1.',
1731
1809
  'noul answer: {"noul": probability from 0 to 1 that the answer is yes}.'
@@ -1746,16 +1824,18 @@ function validateJudgeRequest(value) {
1746
1824
  for (const [id, raw] of entries) {
1747
1825
  if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(id) || ["__proto__", "constructor", "prototype"].includes(id)) throw new JudgeValidationError("questionId", `Invalid judge question id: ${id}`);
1748
1826
  const question = raw;
1749
- if (question?.type === "choice") {
1750
- const options = question.options;
1751
- if (!options || typeof options !== "object" || Array.isArray(options) || Object.keys(options).length < 2 || Object.values(options).some((item) => typeof item !== "string" || !item.trim())) throw new JudgeValidationError("options", `Choice "${id}" needs at least two options with descriptions`);
1752
- } else if (question?.type === "score") {
1753
- const levels = question.levels;
1754
- if (!Array.isArray(levels) || levels.length < 2 || levels.length > 10 || levels.some((item) => typeof item !== "string")) throw new JudgeValidationError("levels", `Score "${id}" needs 2\u201310 levels`);
1755
- } else if (question?.type === "noul") {
1756
- if (typeof question.question !== "string" || !question.question.trim()) throw new JudgeValidationError("question", `Noul "${id}" needs a question`);
1757
- if (question.criteria !== void 0 && (!question.criteria || typeof question.criteria !== "object" || Array.isArray(question.criteria) || Object.entries(question.criteria).some(([key, value2]) => !["true", "false"].includes(key) || typeof value2 !== "string"))) throw new JudgeValidationError("criteria", `Invalid noul criteria: ${id}`);
1758
- } else throw new JudgeValidationError("questionType", `Question "${id}" must be choice, score, or noul`);
1827
+ if (!question || typeof question !== "object" || Array.isArray(question)) throw new JudgeValidationError("questionType", `Question "${id}" must be choice, score, or noul`);
1828
+ if (!["choice", "score", "noul"].includes(question.type)) throw new JudgeValidationError("questionType", `Question "${id}" must be choice, score, or noul`);
1829
+ if (typeof question.instructions !== "string" || !question.instructions.trim()) throw new JudgeValidationError("instructions", `Question "${id}" needs instructions`);
1830
+ const criteria = question.criteria;
1831
+ if (question.type === "choice") {
1832
+ const names3 = criteria && typeof criteria === "object" && !Array.isArray(criteria) ? Object.keys(criteria) : [];
1833
+ if (names3.length < 2 || names3.length > 255 || names3.some((name) => !name.trim() || typeof criteria[name] !== "string")) throw new JudgeValidationError("criteria", `Choice "${id}" needs 2\u2013255 named options with string descriptions`);
1834
+ } else if (question.type === "score") {
1835
+ if (!Array.isArray(criteria) || criteria.length < 2 || criteria.length > 10 || criteria.some((item) => typeof item !== "string" || !item.trim())) throw new JudgeValidationError("criteria", `Score "${id}" needs 2\u201310 level descriptions`);
1836
+ } else if (criteria !== void 0 && (!criteria || typeof criteria !== "object" || Array.isArray(criteria) || Object.entries(criteria).some(([key, value2]) => !["true", "false"].includes(key) || typeof value2 !== "string"))) {
1837
+ throw new JudgeValidationError("criteria", `Invalid noul criteria: ${id}`);
1838
+ }
1759
1839
  }
1760
1840
  return { state, questions };
1761
1841
  }
@@ -1879,7 +1959,7 @@ var PixieGateStore = class {
1879
1959
  if (error.code === "ENOENT") return { sessionId, runs: [] };
1880
1960
  throw error;
1881
1961
  }
1882
- if (!record5 || typeof record5 !== "object" || Array.isArray(record5) || record5.sessionId !== sessionId || !Array.isArray(record5.runs) || record5.runs.some((run) => !run || typeof run !== "object" || Array.isArray(run) || run.sessionId !== sessionId)) {
1962
+ if (!record5 || typeof record5 !== "object" || Array.isArray(record5) || record5.sessionId !== sessionId || !Array.isArray(record5.runs) || record5.runs.some((run) => !run || typeof run !== "object" || Array.isArray(run) || run.sessionId !== sessionId) || record5.disabledGateIds !== void 0 && (!Array.isArray(record5.disabledGateIds) || record5.disabledGateIds.some((id) => typeof id !== "string"))) {
1883
1963
  throw new Error(`Invalid Pixie Gate record for session ${sessionId}`);
1884
1964
  }
1885
1965
  for (const run of record5.runs) if (run.status === "running" && !this.owned.has(run.id)) run.status = "cancelled";
@@ -1896,6 +1976,7 @@ var PixieGateStore = class {
1896
1976
  point: snapshot.point,
1897
1977
  ...snapshot.when ? { when: snapshot.when } : {},
1898
1978
  sessionId,
1979
+ ...snapshot.advanced ? { showInChat: snapshot.advanced.showInChat !== false } : {},
1899
1980
  startedAt: Date.now(),
1900
1981
  original,
1901
1982
  finalText: original,
@@ -1932,6 +2013,7 @@ var PixieGateStore = class {
1932
2013
  if (value.reason !== void 0) run.reason = value.reason;
1933
2014
  if (value.turn !== void 0) run.turn = value.turn;
1934
2015
  if (value.tool !== void 0) run.tool = value.tool;
2016
+ if (value.messageId !== void 0) run.messageId = value.messageId;
1935
2017
  if (value.steerCount !== void 0) run.steerCount = value.steerCount;
1936
2018
  if (value.result) {
1937
2019
  run.result = value.result;
@@ -1960,6 +2042,26 @@ var PixieGateStore = class {
1960
2042
  return structuredClone(run);
1961
2043
  });
1962
2044
  }
2045
+ /** Gates switched off for this session; a missing record means none. */
2046
+ async disabledGates(sessionId) {
2047
+ return [...(await this.read(sessionId)).disabledGateIds ?? []];
2048
+ }
2049
+ /** Switch one gate for this session only. Shares the run queue, so it never races a run commit. */
2050
+ setGateEnabled(sessionId, gateId, enabled) {
2051
+ if (typeof gateId !== "string" || !gateId) throw new Error("Gate id is required");
2052
+ return this.enqueue(sessionId, async () => {
2053
+ const record5 = await this.read(sessionId);
2054
+ const current = new Set(record5.disabledGateIds ?? []);
2055
+ if (enabled) current.delete(gateId);
2056
+ else current.add(gateId);
2057
+ const next = [...current];
2058
+ if (next.length) record5.disabledGateIds = next;
2059
+ else delete record5.disabledGateIds;
2060
+ await this.save(sessionId, record5);
2061
+ for (const listener of this.listeners.get(sessionId) ?? []) listener({ type: "session", disabledGateIds: [...next] });
2062
+ return next;
2063
+ });
2064
+ }
1963
2065
  /** Sync means refetch durable history; only committed running executions are replayed. No await between snapshot and subscribe. */
1964
2066
  async *follow(sessionId, signal) {
1965
2067
  this.file(sessionId);
@@ -2411,6 +2513,14 @@ function parsePixieSettings(value) {
2411
2513
  if (typeof raw.judgeSystemPrompt !== "string") throw new Error("Pixie settings judgeSystemPrompt must be a string");
2412
2514
  settings.judgeSystemPrompt = raw.judgeSystemPrompt;
2413
2515
  }
2516
+ if (raw.judgeEngine !== void 0) {
2517
+ if (raw.judgeEngine !== "llm" && raw.judgeEngine !== "jev") throw new Error("Pixie settings judgeEngine must be llm or jev");
2518
+ settings.judgeEngine = raw.judgeEngine;
2519
+ }
2520
+ if (raw.jevModel !== void 0) {
2521
+ if (typeof raw.jevModel !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(raw.jevModel)) throw new Error("Pixie settings jevModel must be a model ID");
2522
+ settings.jevModel = raw.jevModel;
2523
+ }
2414
2524
  return settings;
2415
2525
  }
2416
2526
  function parsePixieModel(value) {
@@ -2461,75 +2571,6 @@ var PixieSettingsStore = class {
2461
2571
  import { mkdir as mkdir5, readFile as readFile6, rename as rename5, writeFile as writeFile5 } from "node:fs/promises";
2462
2572
  import { dirname as dirname5 } from "node:path";
2463
2573
  import { randomUUID as randomUUID6 } from "node:crypto";
2464
-
2465
- // src/pixie-gate-task-contracts.ts
2466
- var object3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
2467
- var text = (value) => typeof value === "string" && !!value.trim();
2468
- function validateGateTask(value) {
2469
- if (!object3(value) || !text(value.id) || !text(value.name) || typeof value.enabled !== "boolean" || !GATE_WHENS.includes(value.when) || typeof value.instruction !== "string") throw new Error("Invalid gate task");
2470
- const advanced = value.advanced;
2471
- if (!object3(advanced) || typeof advanced.confirmReplace !== "boolean" || !["pass", "block"].includes(advanced.onError) || !["root", "child", "any"].includes(advanced.agents) || typeof advanced.timeoutMs !== "number" || !Number.isFinite(advanced.timeoutMs) || typeof advanced.maxSteers !== "number") throw new Error("Invalid gate advanced options");
2472
- if (advanced.script !== void 0 && (typeof advanced.script !== "string" || (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(advanced.script) || advanced.script.includes("..")))) throw new Error("Invalid gate script filename");
2473
- if (!Array.isArray(value.presetIds) || !value.presetIds.every(text)) throw new Error("Invalid gate presetIds");
2474
- if (value.disabledPresetIds !== void 0 && (!Array.isArray(value.disabledPresetIds) || !value.disabledPresetIds.every(text))) throw new Error("Invalid gate disabledPresetIds");
2475
- const name = typeof advanced.script === "string" ? advanced.script.replace(/\.[^.]+$/, "") : void 0;
2476
- for (const match of value.instruction.matchAll(/{{\s*([^{}]+?)\s*}}/g)) {
2477
- if (!name || ![`hooks.${name}.text`, `hooks.${name}.data`].includes(match[1])) throw new Error(`Unknown prompt variable: ${match[1]}`);
2478
- }
2479
- const task = structuredClone(value);
2480
- delete task.advanced.judge;
2481
- const problem = taskProblem(task);
2482
- if (problem) throw new Error(`Invalid gate ${problem}`);
2483
- task.presetIds = [...new Set(task.presetIds)];
2484
- const disabled = [...new Set(task.disabledPresetIds ?? [])].filter((id) => task.presetIds.includes(id));
2485
- if (disabled.length) task.disabledPresetIds = disabled;
2486
- else delete task.disabledPresetIds;
2487
- return task;
2488
- }
2489
- function validateGateTasks(value) {
2490
- if (!Array.isArray(value)) throw new Error("Gate tasks must be an array");
2491
- const ids = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Set();
2492
- return value.map((raw) => {
2493
- const task = validateGateTask(raw);
2494
- if (ids.has(task.id)) throw new Error("Duplicate gate id");
2495
- ids.add(task.id);
2496
- for (const preset of task.presetIds) {
2497
- const key = JSON.stringify([preset, task.when]);
2498
- if (!enabledFor(task, preset)) continue;
2499
- if (bindings.has(key)) throw new Error("Only one enabled gate per preset and when is supported");
2500
- bindings.add(key);
2501
- }
2502
- return task;
2503
- });
2504
- }
2505
- function parseGateOutput(text2) {
2506
- const value = JSON.parse(text2);
2507
- if (!object3(value) || Object.hasOwn(value, "verdict") && !["pass", "stop"].includes(value.verdict)) throw new Error("Gate output needs a pass/stop verdict");
2508
- for (const [key, field2] of Object.entries(value)) {
2509
- if (!["verdict", "reason", "replace", "append", "note"].includes(key) || typeof field2 !== "string") throw new Error(`Invalid gate output field: ${key}`);
2510
- }
2511
- return { verdict: "pass", ...value };
2512
- }
2513
- function gateOutputPlan(task, output, original) {
2514
- const message = task.when === "user-message" || task.when === "agent-message";
2515
- const mutable = message || task.when === "tool-after";
2516
- const changed = output.replace !== void 0 && output.replace !== original;
2517
- const confirm = message && task.advanced.confirmReplace && changed;
2518
- const stop = output.verdict === "stop" || confirm;
2519
- const notes = output.note === void 0 ? [] : [output.note];
2520
- if (output.replace !== void 0 && (!mutable || stop && !message)) notes.push(output.replace);
2521
- if (output.append !== void 0 && (!mutable || stop)) notes.push(output.append);
2522
- return {
2523
- stop,
2524
- notes,
2525
- ...output.reason !== void 0 ? { reason: output.reason } : {},
2526
- ...stop && message && output.replace !== void 0 ? { suggestion: output.replace } : {},
2527
- ...!stop && mutable && changed ? { replacement: output.replace } : {},
2528
- ...!stop && mutable && output.append !== void 0 ? { addition: output.append } : {}
2529
- };
2530
- }
2531
-
2532
- // src/host/pixie-gate-definitions.ts
2533
2574
  var diagnostic = (error, code) => ({ code: typeof error?.code === "string" ? error.code : code, message: error instanceof Error ? error.message : String(error) });
2534
2575
  function decode(text2) {
2535
2576
  let value;
@@ -2599,6 +2640,15 @@ async function readRuntimeGateDefinitions(source) {
2599
2640
  return [];
2600
2641
  }
2601
2642
  }
2643
+ async function readSessionGateDefinitions(source, sessionId) {
2644
+ const [rows, disabled] = await Promise.all([readRuntimeGateDefinitions(source), source.disabledGates?.(sessionId).catch((error) => {
2645
+ source.warn?.(`studio pixie: session gate switches unreadable for ${sessionId}: ${String(error)}`);
2646
+ return [];
2647
+ }) ?? Promise.resolve([])]);
2648
+ if (!disabled.length) return rows;
2649
+ const off = new Set(disabled);
2650
+ return rows.filter((row) => !off.has(row.id));
2651
+ }
2602
2652
 
2603
2653
  // src/host/pixie-gate-input.ts
2604
2654
  import { randomUUID as randomUUID7 } from "node:crypto";
@@ -2622,6 +2672,7 @@ async function executeTask(task, original, signal, runner, context) {
2622
2672
  records: execution.helperCalls ?? [],
2623
2673
  hooks: {},
2624
2674
  appliedPolicy: task.advanced.onError,
2675
+ ...execution.concludedBy ? { concludedBy: execution.concludedBy } : {},
2625
2676
  ...plan.stop || execution.error ? { forwardError: { verdict: execution.error ? "error" : "fail", reason: plan.reason ?? "Revision requires confirmation", ...plan.suggestion !== void 0 ? { text: plan.suggestion } : {} } } : {},
2626
2677
  forward: plan.stop ? { kind: "none" } : plan.replacement !== void 0 ? { kind: "replace", text: plan.replacement } : plan.addition !== void 0 ? { kind: "append", text: original, contexts: [plan.addition], source: "studio-pixie-gate" } : { kind: "original", text: original }
2627
2678
  };
@@ -2649,7 +2700,7 @@ function gateInput(deps) {
2649
2700
  if (decision.kind === "reject") return decision;
2650
2701
  const preset = deps.preset(payload.agent);
2651
2702
  if (!preset) return decision;
2652
- const rows = await readRuntimeGateDefinitions(deps);
2703
+ const rows = await readSessionGateDefinitions(deps, payload.agent.id);
2653
2704
  const root = deps.isRoot(payload.agent);
2654
2705
  const targets = decision.messages.flatMap((message) => {
2655
2706
  const when = message.source.kind === "user" ? "user-message" : message.source.kind === "agent-message" ? "agent-message" : void 0;
@@ -2668,7 +2719,7 @@ function gateInput(deps) {
2668
2719
  deps.emit("pixie-gate/start", structuredClone(run));
2669
2720
  const execution = await executeTask(gate, original, payload.signal, deps.runner, () => deps.hookContext?.(payload.agent, payload.turn) ?? { sessionId: payload.agent.id, cwd: payload.agent.session.header.cwd, turn: payload.turn, agent: root ? "root" : "child" });
2670
2721
  const { result, plan } = execution;
2671
- const item = { run, execution, result, applied: execution.skipped ? "none" : "pass" };
2722
+ const item = { messageId: String(message.id), run, execution, result, applied: execution.skipped ? "none" : "pass" };
2672
2723
  pending.push(item);
2673
2724
  if (payload.signal.aborted || result.verdict === "cancelled") break;
2674
2725
  const suggestion = plan.suggestion;
@@ -2704,9 +2755,9 @@ function gateInput(deps) {
2704
2755
  addition: !cancelled && !rejected && item.execution.plan.addition !== void 0,
2705
2756
  withheld: !cancelled && rejected
2706
2757
  });
2707
- await deps.recorder.finish(payload.agent.id, item.run.id, { status: cancelled ? "cancelled" : item.result.verdict === "error" ? "error" : "done", result: item.result, output: item.execution.output, usage: item.execution.usage, applied, hooksInput, ...applied === "rejected" ? { rejection: item.rejection } : {} });
2758
+ await deps.recorder.finish(payload.agent.id, item.run.id, { status: cancelled ? "cancelled" : item.result.verdict === "error" ? "error" : "done", result: item.result, output: item.execution.output, usage: item.execution.usage, applied, hooksInput, messageId: item.messageId, ...applied === "rejected" ? { rejection: item.rejection } : {} });
2708
2759
  const usage = item.execution.usage;
2709
- deps.emit("pixie-gate/end", { ...item.run, result: item.result, output: item.execution.output, finalText: item.result.subject, applied, hooksInput, usage, endedAt: Date.now(), status: cancelled ? "cancelled" : item.result.verdict === "error" ? "error" : "done", ...applied === "rejected" && item.rejection ? { rejection: { ...item.rejection, original: item.run.original, status: "pending" } } : {} });
2760
+ deps.emit("pixie-gate/end", { ...item.run, messageId: item.messageId, result: item.result, output: item.execution.output, finalText: item.result.subject, applied, hooksInput, usage, endedAt: Date.now(), status: cancelled ? "cancelled" : item.result.verdict === "error" ? "error" : "done", ...applied === "rejected" && item.rejection ? { rejection: { ...item.rejection, original: item.run.original, status: "pending" } } : {} });
2710
2761
  }
2711
2762
  return committed;
2712
2763
  };
@@ -2720,7 +2771,7 @@ function gateTurn(deps) {
2720
2771
  return async ({ agent, turn, signal }) => {
2721
2772
  const preset = deps.preset(agent);
2722
2773
  if (!preset) return;
2723
- const gate = (await readRuntimeGateDefinitions(deps)).find((row) => row.when === "turn-end" && taskMatches(row, preset, deps.isRoot(agent)));
2774
+ const gate = (await readSessionGateDefinitions(deps, agent.id)).find((row) => row.when === "turn-end" && taskMatches(row, preset, deps.isRoot(agent)));
2724
2775
  if (!gate) return;
2725
2776
  const original = [...agent.session.deriveMessages()].reverse().find((message2) => message2.role === "assistant");
2726
2777
  const subject = original ? textOf3(original) : "";
@@ -2759,7 +2810,7 @@ function gateTools(deps) {
2759
2810
  if (!agent) return decision;
2760
2811
  const preset = deps.preset(agent);
2761
2812
  if (!preset) return decision;
2762
- const gate = (await readRuntimeGateDefinitions(deps)).find((row) => row.when === point && taskMatches(row, preset, deps.isRoot(agent)));
2813
+ const gate = (await readSessionGateDefinitions(deps, agent.id)).find((row) => row.when === point && taskMatches(row, preset, deps.isRoot(agent)));
2763
2814
  if (!gate) return decision;
2764
2815
  const turn = [...agent.session.snapshotEvents()].reverse().find((event) => event.type === "turn/start")?.data.turn ?? 0;
2765
2816
  const contexts = "additionalContexts" in decision ? decision.additionalContexts : void 0;
@@ -2822,89 +2873,184 @@ function gateTools(deps) {
2822
2873
  };
2823
2874
  }
2824
2875
 
2825
- // src/host/pixie-judge.ts
2826
- var TOLERANCE = 0.02;
2827
- var isProbability = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
2828
- var sumsToOne = (values) => Math.abs(values.reduce((sum, value) => sum + value, 0) - 1) <= TOLERANCE;
2829
- function checkAnswer(id, question, raw) {
2830
- const answer = raw;
2831
- if (!answer || typeof answer !== "object") throw new Error(`Missing answer for "${id}"`);
2832
- if (question.type === "choice") {
2833
- const names3 = Object.keys(question.options);
2834
- const probabilities = answer.probabilities;
2835
- if (typeof answer.choice !== "string" || !names3.includes(answer.choice)) throw new Error(`Answer "${id}" chose an unknown option`);
2836
- if (!probabilities || names3.some((name) => !isProbability(probabilities[name])) || Object.keys(probabilities).some((name) => !names3.includes(name))) throw new Error(`Answer "${id}" needs a probability per option`);
2837
- const values = names3.map((name) => probabilities[name]);
2838
- if (!sumsToOne(values)) throw new Error(`Answer "${id}" probabilities must sum to 1`);
2839
- return { type: "choice", choice: answer.choice, probabilities: Object.fromEntries(names3.map((name) => [name, probabilities[name]])), confidence: Math.max(...values) };
2840
- }
2841
- if (question.type === "score") {
2842
- const probabilities = answer.probabilities;
2843
- const top = question.levels.length - 1;
2844
- if (typeof answer.score !== "number" || !Number.isFinite(answer.score) || answer.score < 0 || answer.score > top) throw new Error(`Answer "${id}" score must be within 0..${top}`);
2845
- if (!Array.isArray(probabilities) || probabilities.length !== question.levels.length || !probabilities.every(isProbability) || !sumsToOne(probabilities)) throw new Error(`Answer "${id}" needs one probability per level summing to 1`);
2846
- return { type: "score", score: answer.score, probabilities, confidence: Math.max(...probabilities) };
2847
- }
2848
- if (!isProbability(answer.noul)) throw new Error(`Answer "${id}" noul must be a probability in 0..1`);
2849
- return { type: "noul", noul: answer.noul };
2876
+ // src/host/pixie-gate-helper.ts
2877
+ import { createHash, randomBytes } from "node:crypto";
2878
+ import { chmod, mkdir as mkdir6, rename as rename6, writeFile as writeFile6 } from "node:fs/promises";
2879
+ import { join as join5 } from "node:path";
2880
+ var HELPER_ROUTE = "/studio/pixie-gate/helper";
2881
+ var HELPER_LIMITS = { calls: 20, bodyBytes: 256 * 1024 };
2882
+ function helperJudgement(request, answer, meta = {}) {
2883
+ const extra = { ...meta.engine ? { engine: meta.engine } : {}, ...meta.model ? { model: meta.model } : {} };
2884
+ if (answer.type === "noul") {
2885
+ const pass = answer.noul >= request.threshold;
2886
+ return { exit: pass ? 0 : 1, text: String(answer.noul), json: { type: "bool", label: pass ? "yes" : "no", answer, threshold: request.threshold, ...extra } };
2887
+ }
2888
+ if (answer.type === "choice") return { exit: 0, text: answer.choice, json: { type: "select", label: answer.choice, answer, ...extra } };
2889
+ return { exit: 0, text: String(answer.score), json: { type: "score", label: String(Math.round(answer.score)), answer, ...extra } };
2850
2890
  }
2851
- function parseJudgeOutput(text2, questions) {
2852
- const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/.exec(text2.trim());
2853
- let parsed;
2854
- try {
2855
- parsed = JSON.parse(fenced ? fenced[1] : text2.trim());
2856
- } catch {
2857
- throw new Error("Judge model did not return JSON");
2858
- }
2859
- const answers = parsed?.answers;
2860
- if (!answers || typeof answers !== "object") throw new Error("Judge model output has no answers object");
2861
- return Object.fromEntries(Object.entries(questions).map(([id, question]) => [id, checkAnswer(id, question, answers[id])]));
2891
+ function helperJudgeRequest(form) {
2892
+ const type = form.get("type") ?? "bool";
2893
+ if (!["bool", "select", "score"].includes(type)) throw new Error("type must be bool, select, or score");
2894
+ const instructions2 = form.get("question")?.trim();
2895
+ if (!instructions2) throw new Error("question is required");
2896
+ const raw = form.get("threshold"), threshold = raw === null ? 0.5 : Number(raw);
2897
+ if (!(threshold >= 0 && threshold <= 1)) throw new Error("threshold must be between 0 and 1");
2898
+ let question;
2899
+ if (type === "bool") {
2900
+ const yes = form.get("true"), no = form.get("false");
2901
+ question = { type: "noul", instructions: instructions2, ...yes !== null || no !== null ? { criteria: { ...yes !== null ? { true: yes } : {}, ...no !== null ? { false: no } : {} } } : {} };
2902
+ } else if (type === "select") {
2903
+ const criteria = {};
2904
+ for (const option of form.getAll("option")) {
2905
+ const at = option.indexOf("=");
2906
+ const name = (at < 0 ? option : option.slice(0, at)).trim();
2907
+ if (!name || Object.hasOwn(criteria, name)) throw new Error(`option names must be unique and non-empty: ${option}`);
2908
+ criteria[name] = at < 0 ? name : option.slice(at + 1);
2909
+ }
2910
+ question = { type: "choice", instructions: instructions2, criteria };
2911
+ } else question = { type: "score", instructions: instructions2, criteria: form.getAll("level") };
2912
+ validateJudgeRequest({ state: "", questions: { helper: question } });
2913
+ return { type, question, threshold };
2862
2914
  }
2863
- var LlmJudgeEngine = class {
2864
- constructor(llm, model, system = DEFAULT_JUDGE_SYSTEM_PROMPT) {
2865
- this.llm = llm;
2866
- this.model = model;
2867
- this.system = system;
2915
+ var HELPER_SCRIPT = `#!/usr/bin/env bash
2916
+ # Pixie Gate hook helper, generated by DSH Studio. Valid only inside one hook script run.
2917
+ # pixie ask "<instruction>" [--input]
2918
+ # pixie judge [--type bool|select|score] --question "<question>" [--threshold 0.5] [--true ..] [--false ..]
2919
+ # [--option name=desc ..] [--level desc ..] [--json] [--input]
2920
+ # --input reads the helper input from stdin; otherwise the gate target is used.
2921
+ set -u
2922
+ fail() { printf 'pixie: %s\\n' "$1" >&2; exit 2; }
2923
+ [ -n "\${PIXIE_URL:-}" ] && [ -n "\${PIXIE_TOKEN:-}" ] || fail 'helper is not available outside a Pixie Gate hook script run'
2924
+ command -v curl >/dev/null 2>&1 || fail 'curl is required'
2925
+ command="\${1:-}"; [ $# -gt 0 ] && shift
2926
+ input=0; args=()
2927
+ case "$command" in
2928
+ ask)
2929
+ [ $# -ge 1 ] || fail 'usage: pixie ask "<instruction>" [--input]'
2930
+ args+=(--data-urlencode "instruction=$1"); shift
2931
+ while [ $# -gt 0 ]; do case "$1" in --input) input=1 ;; *) fail "unknown option: $1" ;; esac; shift; done ;;
2932
+ judge)
2933
+ question=''; json=0
2934
+ while [ $# -gt 0 ]; do case "$1" in
2935
+ --type|--question|--threshold|--true|--false|--option|--level)
2936
+ [ $# -ge 2 ] || fail "$1 needs a value"
2937
+ [ "$1" = --question ] && question="$2"
2938
+ args+=(--data-urlencode "\${1#--}=$2"); shift ;;
2939
+ --json) json=1 ;;
2940
+ --input) input=1 ;;
2941
+ *) fail "unknown option: $1" ;;
2942
+ esac; shift; done
2943
+ [ -n "$question" ] || fail 'usage: pixie judge [--type bool|select|score] --question "<question>" [--threshold 0.5] [--true ..] [--false ..] [--option name=desc ..] [--level desc ..] [--json] [--input]' ;;
2944
+ *) fail 'usage: pixie ask|judge ...' ;;
2945
+ esac
2946
+ [ "$input" = 1 ] && args+=(--data-urlencode 'input@-')
2947
+ # The token travels in a config on a file descriptor, never on the command line.
2948
+ out=$(curl -sS --noproxy '*' -X POST -K <(printf 'header = "Authorization: Bearer %s"\\n' "$PIXIE_TOKEN") -w '\\n%{http_code}' "\${args[@]}" "$PIXIE_URL/$command") || fail "request failed"
2949
+ code=\${out##*$'\\n'}; body=\${out%$'\\n'*}
2950
+ [ "$code" = 200 ] || fail "HTTP $code: $body"
2951
+ if [ "$command" = ask ]; then printf '%s\\n' "$body"; exit 0; fi
2952
+ # judge body: exit code, plain answer, then the JSON line.
2953
+ status=\${body%%$'\\n'*}; rest=\${body#*$'\\n'}; text=\${rest%%$'\\n'*}; line=\${rest#*$'\\n'}
2954
+ if [ "$json" = 1 ]; then printf '%s\\n' "$line"; else printf '%s\\n' "$text"; fi
2955
+ exit "$status"
2956
+ `;
2957
+ var hash = (token) => createHash("sha256").update(token).digest("hex");
2958
+ var send = (res, status, text2) => {
2959
+ if (res.headersSent) return;
2960
+ res.writeHead(status, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
2961
+ res.end(text2);
2962
+ };
2963
+ var PixieGateHelper = class {
2964
+ constructor(options) {
2965
+ this.options = options;
2966
+ this.limits = options.limits ?? HELPER_LIMITS;
2868
2967
  }
2869
- kind = "judge";
2870
- async judge(request, signal) {
2871
- let usage;
2872
- try {
2873
- const { state, questions } = validateJudgeRequest(request);
2874
- const prompt = `State:
2875
- ${typeof state === "string" ? state : JSON.stringify(state, null, 2)}
2876
-
2877
- Questions:
2878
- ${JSON.stringify(questions, null, 2)}`;
2879
- let text2 = "";
2880
- let finished = false;
2881
- for await (const chunk of untilAborted(this.llm.stream({
2882
- provider: this.model.provider,
2883
- model: this.model.model,
2884
- ...this.model.reasoningEffort ? { reasoningEffort: this.model.reasoningEffort } : {},
2885
- ...this.model.maxTokens ? { maxTokens: this.model.maxTokens } : {},
2886
- system: this.system,
2887
- messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
2888
- signal
2889
- }), signal)) {
2890
- if (chunk.type === "text-delta" && typeof chunk.text === "string") text2 += chunk.text;
2891
- if (chunk.type === "usage" && chunk.usage && typeof chunk.usage === "object") {
2892
- const raw = chunk.usage;
2893
- usage = { inputTokens: typeof raw.inputTokens === "number" ? raw.inputTokens : 0, outputTokens: typeof raw.outputTokens === "number" ? raw.outputTokens : 0 };
2894
- for (const key of ["cacheReadTokens", "cacheWriteTokens", "reasoningTokens"]) if (typeof raw[key] === "number") usage[key] = raw[key];
2895
- }
2896
- if (chunk.type === "finish") {
2897
- const reason = chunk.reason;
2898
- if (reason?.kind !== "stop") throw new Error(reason?.failure?.message ?? `Judge model ended with ${reason?.kind ?? "unknown"}`);
2899
- finished = true;
2900
- }
2968
+ grants = /* @__PURE__ */ new Map();
2969
+ installed;
2970
+ limits;
2971
+ /** Environment for one script run. Without a reachable route only PATH is given and `pixie` explains why. */
2972
+ async issue(grant) {
2973
+ await (this.installed ??= this.install().catch((error) => {
2974
+ this.installed = void 0;
2975
+ throw error;
2976
+ }));
2977
+ const env = { PATH: `${this.options.binDirectory}:${process.env.PATH ?? "/usr/bin:/bin"}` };
2978
+ const url = this.options.url();
2979
+ if (!url) return { env, revoke() {
2980
+ } };
2981
+ const token = randomBytes(32).toString("hex"), key = hash(token);
2982
+ const entry = { grant, calls: 0, revoked: new AbortController() };
2983
+ this.grants.set(key, entry);
2984
+ return { env: { ...env, PIXIE_URL: url, PIXIE_TOKEN: token }, revoke: () => {
2985
+ this.grants.delete(key);
2986
+ entry.revoked.abort(new Error("Pixie helper token revoked"));
2987
+ } };
2988
+ }
2989
+ handle = async (req, res) => {
2990
+ const action = new URL(req.url ?? "/", "http://x").pathname.slice(HELPER_ROUTE.length);
2991
+ if (action !== "/ask" && action !== "/judge") return send(res, 404, "unknown helper");
2992
+ if (req.method !== "POST") return send(res, 405, "POST only");
2993
+ const header = req.headers.authorization;
2994
+ const entry = typeof header === "string" && header.startsWith("Bearer ") ? this.grants.get(hash(header.slice(7))) : void 0;
2995
+ if (!entry) return send(res, 401, "invalid or expired token");
2996
+ if (entry.calls >= this.limits.calls) return send(res, 429, `helper call limit (${this.limits.calls}) reached`);
2997
+ const body = await this.read(req);
2998
+ if (body === void 0) return send(res, 413, `helper request exceeds ${this.limits.bodyBytes} bytes`);
2999
+ if (!this.grants.has(hash(header.slice(7)))) return send(res, 401, "invalid or expired token");
3000
+ const form = new URLSearchParams(body);
3001
+ const input = form.get("input") ?? entry.grant.target;
3002
+ const closed = new AbortController();
3003
+ res.on("close", () => {
3004
+ if (!res.writableFinished) closed.abort(new Error("Helper client disconnected"));
3005
+ });
3006
+ const signal = AbortSignal.any([entry.grant.signal, entry.revoked.signal, closed.signal]);
3007
+ let call;
3008
+ if (action === "/ask") {
3009
+ const instruction = form.get("instruction")?.trim();
3010
+ if (!instruction) return send(res, 400, "instruction is required");
3011
+ entry.calls++;
3012
+ call = entry.grant.ask(instruction, input, signal);
3013
+ } else {
3014
+ let request;
3015
+ try {
3016
+ request = helperJudgeRequest(form);
3017
+ } catch (error) {
3018
+ return send(res, 400, error instanceof Error ? error.message : String(error));
2901
3019
  }
2902
- signal.throwIfAborted();
2903
- if (!finished) throw new Error("Judge model stream ended without a finish");
2904
- return { ok: true, answers: parseJudgeOutput(text2, questions), ...usage ? { usage } : {} };
3020
+ entry.calls++;
3021
+ call = entry.grant.judge(request, input, signal).then((result) => `${result.exit}
3022
+ ${result.text.replace(/\n/g, " ")}
3023
+ ${JSON.stringify(result.json)}`);
3024
+ }
3025
+ try {
3026
+ send(res, 200, await call);
2905
3027
  } catch (error) {
2906
- return { ok: false, error: error instanceof Error ? error.message : String(error), ...usage ? { usage } : {} };
3028
+ send(res, signal.aborted ? 503 : 502, error instanceof Error ? error.message : String(error));
2907
3029
  }
3030
+ };
3031
+ /** Body text, or undefined above the size limit (the rest is drained, then the socket closed past 4×). */
3032
+ read(req) {
3033
+ return new Promise((resolve6, reject) => {
3034
+ const chunks = [];
3035
+ let size = 0;
3036
+ req.on("data", (chunk) => {
3037
+ size += chunk.length;
3038
+ if (size <= this.limits.bodyBytes) chunks.push(chunk);
3039
+ else if (size > this.limits.bodyBytes * 4) {
3040
+ req.destroy();
3041
+ resolve6(void 0);
3042
+ }
3043
+ });
3044
+ req.on("end", () => resolve6(size > this.limits.bodyBytes ? void 0 : Buffer.concat(chunks).toString("utf8")));
3045
+ req.on("error", reject);
3046
+ });
3047
+ }
3048
+ async install() {
3049
+ await mkdir6(this.options.binDirectory, { recursive: true });
3050
+ const path = join5(this.options.binDirectory, "pixie"), temp = `${path}.${process.pid}.tmp`;
3051
+ await writeFile6(temp, HELPER_SCRIPT, { mode: 493 });
3052
+ await chmod(temp, 493);
3053
+ await rename6(temp, path);
2908
3054
  }
2909
3055
  };
2910
3056
 
@@ -2959,7 +3105,8 @@ var GateTaskRunner = class {
2959
3105
  } finally {
2960
3106
  session?.revoke();
2961
3107
  }
2962
- if (hook.skipped) return { status: "done", skipped: true, output: { verdict: "pass", note: "skipped by hook" }, usage };
3108
+ if (hook.skipped) return { status: "done", skipped: true, concludedBy: "skipped", output: { verdict: "pass", note: "skipped by hook" }, usage };
3109
+ if ("direct" in hook) return { status: "done", concludedBy: "script", output: hook.direct, usage };
2963
3110
  instruction = hook.instruction;
2964
3111
  }
2965
3112
  if (!this.deps.model) throw new Error("No Pixie model configured");
@@ -2990,7 +3137,7 @@ Instruction: ${instruction}`,
2990
3137
  signal.throwIfAborted();
2991
3138
  if (!finished) throw new Error("Gate model stream ended without a finish");
2992
3139
  const output = parseGateOutput(text2);
2993
- return { status: "done", output, usage };
3140
+ return { status: "done", concludedBy: "pixie", output, usage };
2994
3141
  } catch (error) {
2995
3142
  return { status: parent.aborted ? "cancelled" : "error", error: String(signal.aborted ? signal.reason : error), usage };
2996
3143
  } finally {
@@ -3034,168 +3181,34 @@ Instruction: ${instruction}`,
3034
3181
  record5("ask", input, { verdict: "note", text: text2, reason: instruction, ...usage ? { usage } : {} });
3035
3182
  return text2;
3036
3183
  },
3037
- judge: async (question, threshold, input, callSignal) => {
3038
- const model = this.deps.model;
3039
- if (!model) {
3040
- record5("judge", input, { verdict: "error", error: "No Pixie model configured", reason: question });
3041
- throw new Error("No Pixie model configured");
3184
+ judge: async (request, input, callSignal) => {
3185
+ const reason = request.question.instructions;
3186
+ const engine = this.deps.judge;
3187
+ if (!engine) {
3188
+ record5("judge", input, { verdict: "error", error: "No judge engine configured", reason });
3189
+ throw new Error("No judge engine configured");
3042
3190
  }
3043
- const result = await new LlmJudgeEngine(this.deps.llm, model).judge({ state: input, questions: { helper: { type: "noul", question } } }, callSignal);
3191
+ const result = await engine.judge({ state: input, questions: { helper: request.question } }, callSignal);
3044
3192
  const usage = result.usage ? { usage: result.usage } : {};
3045
3193
  const answer = result.ok ? result.answers.helper : void 0;
3046
- if (answer?.type !== "noul") {
3047
- const error = result.ok ? "Judge answer is not noul" : result.error;
3048
- record5("judge", input, { verdict: "error", error, reason: question, ...usage });
3194
+ if (!answer) {
3195
+ const error = result.ok ? "Judge returned no answer" : result.error;
3196
+ record5("judge", input, { verdict: "error", error, reason, data: { type: request.type, engine: engine.label }, ...usage });
3049
3197
  throw new Error(error);
3050
3198
  }
3051
- const pass = answer.noul >= threshold;
3052
- record5("judge", input, { verdict: pass ? "pass" : "fail", reason: question, data: { noul: answer.noul, threshold }, ...usage });
3053
- return { noul: answer.noul, pass };
3199
+ const judgement = helperJudgement(request, answer, { engine: engine.label });
3200
+ record5("judge", input, {
3201
+ verdict: judgement.exit === 0 ? "pass" : "fail",
3202
+ reason,
3203
+ ...usage,
3204
+ data: { type: request.type, label: judgement.json.label, engine: engine.label, ...request.type === "bool" && answer.type === "noul" ? { noul: answer.noul, threshold: request.threshold } : { answer } }
3205
+ });
3206
+ return judgement;
3054
3207
  }
3055
3208
  };
3056
3209
  }
3057
3210
  };
3058
3211
 
3059
- // src/host/pixie-gate-helper.ts
3060
- import { createHash, randomBytes } from "node:crypto";
3061
- import { chmod, mkdir as mkdir6, rename as rename6, writeFile as writeFile6 } from "node:fs/promises";
3062
- import { join as join5 } from "node:path";
3063
- var HELPER_ROUTE = "/studio/pixie-gate/helper";
3064
- var HELPER_LIMITS = { calls: 20, bodyBytes: 256 * 1024 };
3065
- var HELPER_SCRIPT = `#!/usr/bin/env bash
3066
- # Pixie Gate hook helper, generated by DSH Studio. Valid only inside one hook script run.
3067
- # pixie ask "<instruction>" [--input]
3068
- # pixie judge --question "<question>" [--threshold 0.5] [--input]
3069
- # --input reads the helper input from stdin; otherwise the gate target is used.
3070
- set -u
3071
- fail() { printf 'pixie: %s\\n' "$1" >&2; exit 2; }
3072
- [ -n "\${PIXIE_URL:-}" ] && [ -n "\${PIXIE_TOKEN:-}" ] || fail 'helper is not available outside a Pixie Gate hook script run'
3073
- command -v curl >/dev/null 2>&1 || fail 'curl is required'
3074
- command="\${1:-}"; [ $# -gt 0 ] && shift
3075
- input=0; args=()
3076
- case "$command" in
3077
- ask)
3078
- [ $# -ge 1 ] || fail 'usage: pixie ask "<instruction>" [--input]'
3079
- args+=(--data-urlencode "instruction=$1"); shift
3080
- while [ $# -gt 0 ]; do case "$1" in --input) input=1 ;; *) fail "unknown option: $1" ;; esac; shift; done ;;
3081
- judge)
3082
- question=''; threshold=''
3083
- while [ $# -gt 0 ]; do case "$1" in
3084
- --question) [ $# -ge 2 ] || fail '--question needs a value'; question="$2"; shift ;;
3085
- --threshold) [ $# -ge 2 ] || fail '--threshold needs a value'; threshold="$2"; shift ;;
3086
- --input) input=1 ;;
3087
- *) fail "unknown option: $1" ;;
3088
- esac; shift; done
3089
- [ -n "$question" ] || fail 'usage: pixie judge --question "<question>" [--threshold 0.5] [--input]'
3090
- args+=(--data-urlencode "question=$question")
3091
- [ -z "$threshold" ] || args+=(--data-urlencode "threshold=$threshold") ;;
3092
- *) fail 'usage: pixie ask|judge ...' ;;
3093
- esac
3094
- [ "$input" = 1 ] && args+=(--data-urlencode 'input@-')
3095
- # The token travels in a config on a file descriptor, never on the command line.
3096
- out=$(curl -sS --noproxy '*' -X POST -K <(printf 'header = "Authorization: Bearer %s"\\n' "$PIXIE_TOKEN") -w '\\n%{http_code}' "\${args[@]}" "$PIXIE_URL/$command") || fail "request failed"
3097
- code=\${out##*$'\\n'}; body=\${out%$'\\n'*}
3098
- [ "$code" = 200 ] || fail "HTTP $code: $body"
3099
- if [ "$command" = ask ]; then printf '%s\\n' "$body"; exit 0; fi
3100
- verdict=\${body%% *}; printf '%s\\n' "\${body#* }"
3101
- [ "$verdict" = pass ]
3102
- `;
3103
- var hash = (token) => createHash("sha256").update(token).digest("hex");
3104
- var send = (res, status, text2) => {
3105
- if (res.headersSent) return;
3106
- res.writeHead(status, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
3107
- res.end(text2);
3108
- };
3109
- var PixieGateHelper = class {
3110
- constructor(options) {
3111
- this.options = options;
3112
- this.limits = options.limits ?? HELPER_LIMITS;
3113
- }
3114
- grants = /* @__PURE__ */ new Map();
3115
- installed;
3116
- limits;
3117
- /** Environment for one script run. Without a reachable route only PATH is given and `pixie` explains why. */
3118
- async issue(grant) {
3119
- await (this.installed ??= this.install().catch((error) => {
3120
- this.installed = void 0;
3121
- throw error;
3122
- }));
3123
- const env = { PATH: `${this.options.binDirectory}:${process.env.PATH ?? "/usr/bin:/bin"}` };
3124
- const url = this.options.url();
3125
- if (!url) return { env, revoke() {
3126
- } };
3127
- const token = randomBytes(32).toString("hex"), key = hash(token);
3128
- const entry = { grant, calls: 0, revoked: new AbortController() };
3129
- this.grants.set(key, entry);
3130
- return { env: { ...env, PIXIE_URL: url, PIXIE_TOKEN: token }, revoke: () => {
3131
- this.grants.delete(key);
3132
- entry.revoked.abort(new Error("Pixie helper token revoked"));
3133
- } };
3134
- }
3135
- handle = async (req, res) => {
3136
- const action = new URL(req.url ?? "/", "http://x").pathname.slice(HELPER_ROUTE.length);
3137
- if (action !== "/ask" && action !== "/judge") return send(res, 404, "unknown helper");
3138
- if (req.method !== "POST") return send(res, 405, "POST only");
3139
- const header = req.headers.authorization;
3140
- const entry = typeof header === "string" && header.startsWith("Bearer ") ? this.grants.get(hash(header.slice(7))) : void 0;
3141
- if (!entry) return send(res, 401, "invalid or expired token");
3142
- if (entry.calls >= this.limits.calls) return send(res, 429, `helper call limit (${this.limits.calls}) reached`);
3143
- const body = await this.read(req);
3144
- if (body === void 0) return send(res, 413, `helper request exceeds ${this.limits.bodyBytes} bytes`);
3145
- if (!this.grants.has(hash(header.slice(7)))) return send(res, 401, "invalid or expired token");
3146
- const form = new URLSearchParams(body);
3147
- const input = form.get("input") ?? entry.grant.target;
3148
- const closed = new AbortController();
3149
- res.on("close", () => {
3150
- if (!res.writableFinished) closed.abort(new Error("Helper client disconnected"));
3151
- });
3152
- const signal = AbortSignal.any([entry.grant.signal, entry.revoked.signal, closed.signal]);
3153
- let call;
3154
- if (action === "/ask") {
3155
- const instruction = form.get("instruction")?.trim();
3156
- if (!instruction) return send(res, 400, "instruction is required");
3157
- entry.calls++;
3158
- call = entry.grant.ask(instruction, input, signal);
3159
- } else {
3160
- const question = form.get("question")?.trim();
3161
- const raw = form.get("threshold"), threshold = raw === null ? 0.5 : Number(raw);
3162
- if (!question) return send(res, 400, "question is required");
3163
- if (!(threshold >= 0 && threshold <= 1)) return send(res, 400, "threshold must be between 0 and 1");
3164
- entry.calls++;
3165
- call = entry.grant.judge(question, threshold, input, signal).then((result) => `${result.pass ? "pass" : "fail"} ${result.noul}`);
3166
- }
3167
- try {
3168
- send(res, 200, await call);
3169
- } catch (error) {
3170
- send(res, signal.aborted ? 503 : 502, error instanceof Error ? error.message : String(error));
3171
- }
3172
- };
3173
- /** Body text, or undefined above the size limit (the rest is drained, then the socket closed past 4×). */
3174
- read(req) {
3175
- return new Promise((resolve6, reject) => {
3176
- const chunks = [];
3177
- let size = 0;
3178
- req.on("data", (chunk) => {
3179
- size += chunk.length;
3180
- if (size <= this.limits.bodyBytes) chunks.push(chunk);
3181
- else if (size > this.limits.bodyBytes * 4) {
3182
- req.destroy();
3183
- resolve6(void 0);
3184
- }
3185
- });
3186
- req.on("end", () => resolve6(size > this.limits.bodyBytes ? void 0 : Buffer.concat(chunks).toString("utf8")));
3187
- req.on("error", reject);
3188
- });
3189
- }
3190
- async install() {
3191
- await mkdir6(this.options.binDirectory, { recursive: true });
3192
- const path = join5(this.options.binDirectory, "pixie"), temp = `${path}.${process.pid}.tmp`;
3193
- await writeFile6(temp, HELPER_SCRIPT, { mode: 493 });
3194
- await chmod(temp, 493);
3195
- await rename6(temp, path);
3196
- }
3197
- };
3198
-
3199
3212
  // src/host/pixie-gate-settings.ts
3200
3213
  import { randomUUID as randomUUID10 } from "node:crypto";
3201
3214
  import { mkdir as mkdir7, readFile as readFile7, rename as rename7, writeFile as writeFile7 } from "node:fs/promises";
@@ -3256,17 +3269,203 @@ async function trialGate(payload, deps, signal) {
3256
3269
  return { output: execution.output, plan: gateOutputPlan(task, execution.output, raw.text), usage: execution.usage };
3257
3270
  }
3258
3271
 
3272
+ // src/host/pixie-judge.ts
3273
+ var TOLERANCE = 0.02;
3274
+ var isProbability = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
3275
+ var sumsToOne = (values) => Math.abs(values.reduce((sum, value) => sum + value, 0) - 1) <= TOLERANCE;
3276
+ function checkAnswer(id, question, raw) {
3277
+ const answer = raw;
3278
+ if (!answer || typeof answer !== "object") throw new Error(`Missing answer for "${id}"`);
3279
+ if (question.type === "choice") {
3280
+ const names3 = Object.keys(question.criteria);
3281
+ const probabilities = answer.probabilities;
3282
+ if (typeof answer.choice !== "string" || !names3.includes(answer.choice)) throw new Error(`Answer "${id}" chose an unknown option`);
3283
+ if (!probabilities || names3.some((name) => !isProbability(probabilities[name])) || Object.keys(probabilities).some((name) => !names3.includes(name))) throw new Error(`Answer "${id}" needs a probability per option`);
3284
+ const values = names3.map((name) => probabilities[name]);
3285
+ if (!sumsToOne(values)) throw new Error(`Answer "${id}" probabilities must sum to 1`);
3286
+ return { type: "choice", choice: answer.choice, probabilities: Object.fromEntries(names3.map((name) => [name, probabilities[name]])), confidence: isProbability(answer.confidence) ? answer.confidence : Math.max(...values) };
3287
+ }
3288
+ if (question.type === "score") {
3289
+ const probabilities = answer.probabilities;
3290
+ const top = question.criteria.length - 1;
3291
+ if (typeof answer.score !== "number" || !Number.isFinite(answer.score) || answer.score < 0 || answer.score > top) throw new Error(`Answer "${id}" score must be within 0..${top}`);
3292
+ if (!Array.isArray(probabilities) || probabilities.length !== question.criteria.length || !probabilities.every(isProbability) || !sumsToOne(probabilities)) throw new Error(`Answer "${id}" needs one probability per level summing to 1`);
3293
+ return { type: "score", score: answer.score, probabilities, confidence: isProbability(answer.confidence) ? answer.confidence : Math.max(...probabilities) };
3294
+ }
3295
+ if (!isProbability(answer.noul)) throw new Error(`Answer "${id}" noul must be a probability in 0..1`);
3296
+ return { type: "noul", noul: answer.noul };
3297
+ }
3298
+ function parseJudgeOutput(text2, questions) {
3299
+ const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/.exec(text2.trim());
3300
+ let parsed;
3301
+ try {
3302
+ parsed = JSON.parse(fenced ? fenced[1] : text2.trim());
3303
+ } catch {
3304
+ throw new Error("Judge model did not return JSON");
3305
+ }
3306
+ const answers = parsed?.answers;
3307
+ if (!answers || typeof answers !== "object") throw new Error("Judge model output has no answers object");
3308
+ return Object.fromEntries(Object.entries(questions).map(([id, question]) => [id, checkAnswer(id, question, answers[id])]));
3309
+ }
3310
+ var LlmJudgeEngine = class {
3311
+ constructor(llm, model, system = DEFAULT_JUDGE_SYSTEM_PROMPT) {
3312
+ this.llm = llm;
3313
+ this.model = model;
3314
+ this.system = system;
3315
+ }
3316
+ kind = "judge";
3317
+ label = "llm";
3318
+ async judge(request, signal) {
3319
+ let usage;
3320
+ try {
3321
+ const { state, questions } = validateJudgeRequest(request);
3322
+ const prompt = `State:
3323
+ ${typeof state === "string" ? state : JSON.stringify(state, null, 2)}
3324
+
3325
+ Questions:
3326
+ ${JSON.stringify(questions, null, 2)}`;
3327
+ let text2 = "";
3328
+ let finished = false;
3329
+ for await (const chunk of untilAborted(this.llm.stream({
3330
+ provider: this.model.provider,
3331
+ model: this.model.model,
3332
+ ...this.model.reasoningEffort ? { reasoningEffort: this.model.reasoningEffort } : {},
3333
+ ...this.model.maxTokens ? { maxTokens: this.model.maxTokens } : {},
3334
+ system: this.system,
3335
+ messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
3336
+ signal
3337
+ }), signal)) {
3338
+ if (chunk.type === "text-delta" && typeof chunk.text === "string") text2 += chunk.text;
3339
+ if (chunk.type === "usage" && chunk.usage && typeof chunk.usage === "object") {
3340
+ const raw = chunk.usage;
3341
+ usage = { inputTokens: typeof raw.inputTokens === "number" ? raw.inputTokens : 0, outputTokens: typeof raw.outputTokens === "number" ? raw.outputTokens : 0 };
3342
+ for (const key of ["cacheReadTokens", "cacheWriteTokens", "reasoningTokens"]) if (typeof raw[key] === "number") usage[key] = raw[key];
3343
+ }
3344
+ if (chunk.type === "finish") {
3345
+ const reason = chunk.reason;
3346
+ if (reason?.kind !== "stop") throw new Error(reason?.failure?.message ?? `Judge model ended with ${reason?.kind ?? "unknown"}`);
3347
+ finished = true;
3348
+ }
3349
+ }
3350
+ signal.throwIfAborted();
3351
+ if (!finished) throw new Error("Judge model stream ended without a finish");
3352
+ return { ok: true, answers: parseJudgeOutput(text2, questions), ...usage ? { usage } : {} };
3353
+ } catch (error) {
3354
+ return { ok: false, error: error instanceof Error ? error.message : String(error), ...usage ? { usage } : {} };
3355
+ }
3356
+ }
3357
+ };
3358
+
3359
+ // src/host/jev-judge.ts
3360
+ var JEV_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
3361
+ var JEV_KEY_REF = "TYPESAFE_API_KEY";
3362
+ var DEFAULT_JEV_MODEL = "jev-latest";
3363
+ var RETRIES = 2;
3364
+ var MAX_WAIT_MS = 4e3;
3365
+ function fromWire(question, raw) {
3366
+ if (question.type !== "score" || !raw || typeof raw !== "object") return raw;
3367
+ const answer = raw;
3368
+ const probabilities = answer.probabilities;
3369
+ if (!probabilities || typeof probabilities !== "object" || Array.isArray(probabilities)) return raw;
3370
+ const keyed = probabilities;
3371
+ if (Object.keys(keyed).length !== question.criteria.length) return { ...answer, probabilities: void 0 };
3372
+ return { ...answer, probabilities: question.criteria.map((_, index) => keyed[String(index)]) };
3373
+ }
3374
+ var wait = (ms, signal) => new Promise((resolve6, reject) => {
3375
+ if (signal.aborted) {
3376
+ reject(signal.reason);
3377
+ return;
3378
+ }
3379
+ const timer = setTimeout(() => {
3380
+ signal.removeEventListener("abort", abort);
3381
+ resolve6();
3382
+ }, ms);
3383
+ const abort = () => {
3384
+ clearTimeout(timer);
3385
+ reject(signal.reason);
3386
+ };
3387
+ signal.addEventListener("abort", abort, { once: true });
3388
+ });
3389
+ function retryDelay(retryAfter, attempt) {
3390
+ const seconds = retryAfter === null ? NaN : Number(retryAfter);
3391
+ return Math.min(Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : 500 * 2 ** attempt, MAX_WAIT_MS);
3392
+ }
3393
+ var JevJudgeEngine = class {
3394
+ constructor(deps) {
3395
+ this.deps = deps;
3396
+ }
3397
+ kind = "judge";
3398
+ label = "jev";
3399
+ async judge(request, signal) {
3400
+ let usage;
3401
+ try {
3402
+ const { state, questions } = validateJudgeRequest(request);
3403
+ if (!this.deps.credentials) throw new Error("DSH credentials service is unavailable");
3404
+ const key = (await this.deps.credentials.resolve(JEV_KEY_REF))?.value;
3405
+ if (!key) throw new Error(`Jev API key is not configured (${JEV_KEY_REF})`);
3406
+ signal.throwIfAborted();
3407
+ const send2 = this.deps.fetch ?? globalThis.fetch;
3408
+ const body = JSON.stringify({ model: this.deps.model, state, questions });
3409
+ for (let attempt = 0; ; attempt++) {
3410
+ const response = await send2(JEV_ENDPOINT, {
3411
+ method: "POST",
3412
+ signal,
3413
+ body,
3414
+ headers: { authorization: `Bearer ${key}`, "content-type": "application/json" }
3415
+ });
3416
+ const text2 = await response.text();
3417
+ if ((response.status === 429 || response.status === 529) && attempt < RETRIES) {
3418
+ await wait(retryDelay(response.headers.get("retry-after"), attempt), signal);
3419
+ continue;
3420
+ }
3421
+ if (response.status !== 200) throw new Error(`Jev request failed: HTTP ${response.status}${text2 ? `: ${text2.slice(0, 500)}` : ""}`);
3422
+ let parsed;
3423
+ try {
3424
+ parsed = JSON.parse(text2);
3425
+ } catch {
3426
+ throw new Error("Jev response is not JSON");
3427
+ }
3428
+ const raw = parsed.usage;
3429
+ if (raw && typeof raw.input_tokens === "number" && typeof raw.output_tokens === "number") usage = { inputTokens: raw.input_tokens, outputTokens: raw.output_tokens };
3430
+ const answers = parsed.answers;
3431
+ if (!answers || typeof answers !== "object") throw new Error("Jev response has no answers object");
3432
+ const typed = Object.fromEntries(Object.entries(questions).map(([id, question]) => [id, checkAnswer(id, question, fromWire(question, answers[id]))]));
3433
+ return { ok: true, answers: typed, ...usage ? { usage } : {} };
3434
+ }
3435
+ } catch (error) {
3436
+ const message = signal.aborted ? String(signal.reason instanceof Error ? signal.reason.message : signal.reason ?? "aborted") : error instanceof Error ? error.message : String(error);
3437
+ return { ok: false, error: message, ...usage ? { usage } : {} };
3438
+ }
3439
+ }
3440
+ };
3441
+ function judgeEngineFor(settings, deps) {
3442
+ if (settings.judgeEngine === "jev") return new JevJudgeEngine({ credentials: deps.credentials, model: settings.jevModel ?? DEFAULT_JEV_MODEL, ...deps.fetch ? { fetch: deps.fetch } : {} });
3443
+ const model = settings.judgeModel ?? settings.model;
3444
+ return model ? new LlmJudgeEngine(deps.llm, model, settings.judgeSystemPrompt || void 0) : void 0;
3445
+ }
3446
+
3259
3447
  // src/host/pixie-service.ts
3260
3448
  var PRUNE_INTERVAL_MS = 10 * 60 * 1e3;
3261
3449
  var json2 = (payload) => JSON.parse(payload);
3450
+ async function credentialWrite(write) {
3451
+ try {
3452
+ await write();
3453
+ } catch (error) {
3454
+ const message = error instanceof Error ? error.message : String(error);
3455
+ if (/Expected YAML collection at refs\b/.test(message)) throw new Error(`[pixie/credentials-refs] ${message}`);
3456
+ throw error;
3457
+ }
3458
+ }
3262
3459
  var llmOf = (ctx) => ctx.get("llm");
3263
- var _models_dec, _saveSettings_dec, _readSettings_dec, _followGates_dec, _resolveGateRejection_dec, _gateRuns_dec, _saveGateDefinitions_dec, _gateDefinitions_dec, _saveGateScriptDirectory_dec, _gateScripts_dec, _trialGate_dec, _follow_dec, _cancel_dec, _ask_dec, _commandTurn_dec, _deleteThread_dec, _activate_dec, _createThread_dec, _view_dec, _a, _init;
3264
- var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote], _createThread_dec = [Remote], _activate_dec = [Remote], _deleteThread_dec = [Remote], _commandTurn_dec = [Remote], _ask_dec = [Remote], _cancel_dec = [Remote], _follow_dec = [Remote({ mode: "stream" })], _trialGate_dec = [Remote], _gateScripts_dec = [Remote], _saveGateScriptDirectory_dec = [Remote], _gateDefinitions_dec = [Remote], _saveGateDefinitions_dec = [Remote], _gateRuns_dec = [Remote], _resolveGateRejection_dec = [Remote], _followGates_dec = [Remote({ mode: "stream" })], _readSettings_dec = [Remote], _saveSettings_dec = [Remote], _models_dec = [Remote], _a) {
3460
+ var _models_dec, _deleteJevKey_dec, _saveJevKey_dec, _jevKeyStatus_dec, _saveSettings_dec, _readSettings_dec, _followGates_dec, _setSessionGate_dec, _sessionGates_dec, _resolveGateRejection_dec, _gateRuns_dec, _saveGateDefinitions_dec, _gateDefinitions_dec, _saveGateScriptDirectory_dec, _gateScripts_dec, _trialGate_dec, _follow_dec, _cancel_dec, _ask_dec, _commandTurn_dec, _deleteThread_dec, _activate_dec, _createThread_dec, _view_dec, _a, _init;
3461
+ var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote], _createThread_dec = [Remote], _activate_dec = [Remote], _deleteThread_dec = [Remote], _commandTurn_dec = [Remote], _ask_dec = [Remote], _cancel_dec = [Remote], _follow_dec = [Remote({ mode: "stream" })], _trialGate_dec = [Remote], _gateScripts_dec = [Remote], _saveGateScriptDirectory_dec = [Remote], _gateDefinitions_dec = [Remote], _saveGateDefinitions_dec = [Remote], _gateRuns_dec = [Remote], _resolveGateRejection_dec = [Remote], _sessionGates_dec = [Remote], _setSessionGate_dec = [Remote], _followGates_dec = [Remote({ mode: "stream" })], _readSettings_dec = [Remote], _saveSettings_dec = [Remote], _jevKeyStatus_dec = [Remote], _saveJevKey_dec = [Remote], _deleteJevKey_dec = [Remote], _models_dec = [Remote], _a) {
3265
3462
  constructor(ctx, dataDir) {
3266
3463
  super(ctx, "studioPixie");
3267
3464
  __runInitializers(_init, 5, this);
3268
3465
  __publicField(this, "defaultScriptDirectory");
3269
3466
  __publicField(this, "gateSettings");
3467
+ /** Optional: the DSH credentials service, read per call so a later mount applies. */
3468
+ __publicField(this, "credentials", () => this.ctx.get("credentials"));
3270
3469
  __publicField(this, "scriptSupported", () => process.platform !== "win32" && !!this.ctx.get("shell"));
3271
3470
  __publicField(this, "store");
3272
3471
  __publicField(this, "settings");
@@ -3297,6 +3496,7 @@ var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote]
3297
3496
  });
3298
3497
  const gateDeps = {
3299
3498
  definitions: () => this.definitions.read(),
3499
+ disabledGates: (sessionId) => this.gates.disabledGates(sessionId),
3300
3500
  warn: (message) => ctx.logger.warn(message),
3301
3501
  preset: (agent) => ctx.get("agentPresets")?.composedPreset(agent.ctx),
3302
3502
  isRoot: (agent) => ctx.get("agents")?.roots().some((root) => root.id === agent.id) ?? false,
@@ -3312,14 +3512,17 @@ var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote]
3312
3512
  };
3313
3513
  },
3314
3514
  runner: async () => {
3315
- const [{ model }, scriptDirectory] = await Promise.all([this.settings.read(), this.scriptDirectory()]);
3515
+ const [settings, scriptDirectory] = await Promise.all([this.settings.read(), this.scriptDirectory()]);
3516
+ const llm = { stream: (options) => llmOf(ctx).stream(options) };
3517
+ const judge = judgeEngineFor(settings, { llm, credentials: this.credentials() });
3316
3518
  return new GateTaskRunner({
3317
3519
  scriptDirectory,
3318
3520
  scriptSupported: this.scriptSupported(),
3319
3521
  shell: ctx.get("shell"),
3320
3522
  helper: this.helper,
3321
- llm: { stream: (options) => llmOf(ctx).stream(options) },
3322
- ...model ? { model } : {}
3523
+ llm,
3524
+ ...settings.model ? { model: settings.model } : {},
3525
+ ...judge ? { judge } : {}
3323
3526
  });
3324
3527
  },
3325
3528
  recorder: this.gates,
@@ -3469,6 +3672,24 @@ var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote]
3469
3672
  async resolveGateRejection(sessionId, runId, status) {
3470
3673
  return this.gates.resolveRejection(sessionId, runId, status);
3471
3674
  }
3675
+ async sessionGates(sessionId) {
3676
+ const agent = this.ctx.get("agents")?.get(sessionId);
3677
+ const preset = agent ? this.ctx.get("agentPresets")?.composedPreset(agent.ctx) : void 0;
3678
+ if (!preset) return { gates: [] };
3679
+ const [read, disabled] = await Promise.all([this.definitions.read(), this.gates.disabledGates(sessionId)]);
3680
+ const off = new Set(disabled);
3681
+ return { preset, gates: read.entries.filter((task) => enabledFor(task, preset)).map((task) => ({
3682
+ id: task.id,
3683
+ name: task.name,
3684
+ when: task.when,
3685
+ ...task.advanced.script ? { script: task.advanced.script } : {},
3686
+ sessionEnabled: !off.has(task.id)
3687
+ })) };
3688
+ }
3689
+ async setSessionGate(sessionId, gateId, enabled) {
3690
+ if (typeof enabled !== "boolean") throw new Error("enabled must be a boolean");
3691
+ return this.gates.setGateEnabled(sessionId, gateId, enabled);
3692
+ }
3472
3693
  async *followGates(sessionId, signal) {
3473
3694
  yield* this.gates.follow(sessionId, signal);
3474
3695
  }
@@ -3478,6 +3699,26 @@ var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote]
3478
3699
  async saveSettings(payload) {
3479
3700
  return this.settings.save(json2(payload));
3480
3701
  }
3702
+ async jevKeyStatus() {
3703
+ const credentials = this.credentials();
3704
+ if (!credentials) return { available: false, configured: false, writable: false };
3705
+ const info = await credentials.describe(JEV_KEY_REF);
3706
+ return { available: true, configured: info.configured, writable: info.writable, ...info.source ? { source: info.source } : {} };
3707
+ }
3708
+ async saveJevKey(payload) {
3709
+ const value = json2(payload).value;
3710
+ if (typeof value !== "string" || !value.trim()) throw new Error("Jev API key is required");
3711
+ const credentials = this.credentials();
3712
+ if (!credentials) throw new Error("DSH credentials service is unavailable");
3713
+ await credentialWrite(() => credentials.set(JEV_KEY_REF, value.trim()));
3714
+ return this.jevKeyStatus();
3715
+ }
3716
+ async deleteJevKey() {
3717
+ const credentials = this.credentials();
3718
+ if (!credentials) throw new Error("DSH credentials service is unavailable");
3719
+ await credentialWrite(() => credentials.unset(JEV_KEY_REF));
3720
+ return this.jevKeyStatus();
3721
+ }
3481
3722
  async models() {
3482
3723
  return this.ctx.get("sessionController").modelCatalog();
3483
3724
  }
@@ -3498,16 +3739,91 @@ __decorateElement(_init, 1, "gateDefinitions", _gateDefinitions_dec, PixieServic
3498
3739
  __decorateElement(_init, 1, "saveGateDefinitions", _saveGateDefinitions_dec, PixieService);
3499
3740
  __decorateElement(_init, 1, "gateRuns", _gateRuns_dec, PixieService);
3500
3741
  __decorateElement(_init, 1, "resolveGateRejection", _resolveGateRejection_dec, PixieService);
3742
+ __decorateElement(_init, 1, "sessionGates", _sessionGates_dec, PixieService);
3743
+ __decorateElement(_init, 1, "setSessionGate", _setSessionGate_dec, PixieService);
3501
3744
  __decorateElement(_init, 1, "followGates", _followGates_dec, PixieService);
3502
3745
  __decorateElement(_init, 1, "readSettings", _readSettings_dec, PixieService);
3503
3746
  __decorateElement(_init, 1, "saveSettings", _saveSettings_dec, PixieService);
3747
+ __decorateElement(_init, 1, "jevKeyStatus", _jevKeyStatus_dec, PixieService);
3748
+ __decorateElement(_init, 1, "saveJevKey", _saveJevKey_dec, PixieService);
3749
+ __decorateElement(_init, 1, "deleteJevKey", _deleteJevKey_dec, PixieService);
3504
3750
  __decorateElement(_init, 1, "models", _models_dec, PixieService);
3505
3751
  __decoratorMetadata(_init, PixieService);
3506
3752
  __publicField(PixieService, "inject", ["llm", "sessionController", "commands"]);
3507
3753
 
3754
+ // src/host/skill-install.ts
3755
+ import { createHash as createHash2 } from "node:crypto";
3756
+ import { mkdir as mkdir8, readFile as readFile8, rename as rename8, writeFile as writeFile8 } from "node:fs/promises";
3757
+ import { join as join7 } from "node:path";
3758
+ var MARKER = "studio-managed";
3759
+ var MANIFEST = ".studio-managed.json";
3760
+ var digest = (text2) => createHash2("sha256").update(text2).digest("hex");
3761
+ async function installSkill(skillsRoot, skill) {
3762
+ const directory = join7(skillsRoot, skill.name);
3763
+ await mkdir8(directory, { recursive: true });
3764
+ const manifestPath = join7(directory, MANIFEST);
3765
+ let recorded = {};
3766
+ try {
3767
+ const parsed = JSON.parse(await readFile8(manifestPath, "utf8"));
3768
+ if (parsed.kind === MARKER && parsed.files && typeof parsed.files === "object") recorded = parsed.files;
3769
+ } catch {
3770
+ }
3771
+ const results = {};
3772
+ const next = {};
3773
+ for (const [relative, content] of Object.entries(skill.files)) {
3774
+ const path = join7(directory, relative);
3775
+ const wanted = digest(content);
3776
+ let current;
3777
+ try {
3778
+ current = digest(await readFile8(path, "utf8"));
3779
+ } catch {
3780
+ current = void 0;
3781
+ }
3782
+ if (current === wanted) {
3783
+ results[relative] = "current";
3784
+ next[relative] = wanted;
3785
+ continue;
3786
+ }
3787
+ if (current !== void 0 && current !== recorded[relative]) {
3788
+ results[relative] = "kept-user-edit";
3789
+ continue;
3790
+ }
3791
+ const temporary2 = `${path}.${process.pid}.tmp`;
3792
+ await writeFile8(temporary2, content);
3793
+ await rename8(temporary2, path);
3794
+ results[relative] = "installed";
3795
+ next[relative] = wanted;
3796
+ }
3797
+ const temporary = `${manifestPath}.${process.pid}.tmp`;
3798
+ await writeFile8(temporary, `${JSON.stringify({ kind: MARKER, files: next }, null, 2)}
3799
+ `);
3800
+ await rename8(temporary, manifestPath);
3801
+ return results;
3802
+ }
3803
+
3804
+ // src/host/pixie-hook-skill.ts
3805
+ import { readFileSync } from "node:fs";
3806
+ import { fileURLToPath } from "node:url";
3807
+ function pixieHookSkill() {
3808
+ const name = "studio-pixie-hook-guide";
3809
+ const read = (packaged, source) => {
3810
+ for (const candidate of [new URL(packaged, import.meta.url), new URL(source, import.meta.url), new URL(source.replace("release/docs/", "docs/"), import.meta.url)]) {
3811
+ try {
3812
+ return readFileSync(fileURLToPath(candidate), "utf8");
3813
+ } catch {
3814
+ }
3815
+ }
3816
+ throw new Error(`Studio skill file missing: ${packaged}`);
3817
+ };
3818
+ return { name, files: {
3819
+ "SKILL.md": read(`./skills/${name}/SKILL.md`, "../../release/docs/appendix/studio-pixie-hook-guide/SKILL.md"),
3820
+ "guide.md": read(`./skills/${name}/guide.md`, "../../release/docs/appendix/pixie-gate.md")
3821
+ } };
3822
+ }
3823
+
3508
3824
  // src/host/catalog-baseline.ts
3509
3825
  import { randomUUID as randomUUID11 } from "node:crypto";
3510
- import { mkdir as mkdir8, readFile as readFile8, rename as rename8, unlink as unlink2, writeFile as writeFile8 } from "node:fs/promises";
3826
+ import { mkdir as mkdir9, readFile as readFile9, rename as rename9, unlink as unlink2, writeFile as writeFile9 } from "node:fs/promises";
3511
3827
  import { dirname as dirname7, resolve as resolve5 } from "node:path";
3512
3828
  var valid = (value) => {
3513
3829
  if (!value || typeof value !== "object") return false;
@@ -3524,7 +3840,7 @@ var CatalogBaseline = class {
3524
3840
  await this.tail;
3525
3841
  let content;
3526
3842
  try {
3527
- content = await readFile8(this.file, "utf8");
3843
+ content = await readFile9(this.file, "utf8");
3528
3844
  } catch (error) {
3529
3845
  if (error.code === "ENOENT") return void 0;
3530
3846
  throw error;
@@ -3536,11 +3852,11 @@ var CatalogBaseline = class {
3536
3852
  save(snapshot) {
3537
3853
  const content = JSON.stringify(snapshot, null, 2);
3538
3854
  const operation = this.tail.then(async () => {
3539
- await mkdir8(dirname7(this.file), { recursive: true, mode: 448 });
3855
+ await mkdir9(dirname7(this.file), { recursive: true, mode: 448 });
3540
3856
  const temp = `${this.file}.${randomUUID11()}.tmp`;
3541
3857
  try {
3542
- await writeFile8(temp, content, { mode: 384, flag: "wx" });
3543
- await rename8(temp, this.file);
3858
+ await writeFile9(temp, content, { mode: 384, flag: "wx" });
3859
+ await rename9(temp, this.file);
3544
3860
  } finally {
3545
3861
  await unlink2(temp).catch(() => {
3546
3862
  });
@@ -3582,10 +3898,18 @@ var StudioCatalog = class extends (_a2 = TypertRemoteService2, _listPresets_dec
3582
3898
  /** The newest complete collection and what it changes against the confirmed baseline. */
3583
3899
  __publicField(this, "pending");
3584
3900
  const storePath = studioStorePath(process.env, ctx.get("profileContext"));
3585
- this.extensions = new MasterExtensions(join7(dirname8(storePath), "master-extensions.json"));
3586
- this.baseline = new CatalogBaseline(join7(dirname8(storePath), "catalog-snapshot.json"));
3901
+ this.extensions = new MasterExtensions(join8(dirname8(storePath), "master-extensions.json"));
3902
+ this.baseline = new CatalogBaseline(join8(dirname8(storePath), "catalog-snapshot.json"));
3587
3903
  this.store = new PresetStore(ctx, storePath);
3588
3904
  ctx.plugin(PixieService, dirname8(storePath));
3905
+ const home = ctx.get("profileContext")?.home;
3906
+ if (home) void Promise.resolve().then(() => {
3907
+ const skill = pixieHookSkill();
3908
+ return installSkill(join8(home, "skills"), skill).then((results) => {
3909
+ const kept = Object.entries(results).filter(([, result]) => result === "kept-user-edit").map(([file]) => file);
3910
+ if (kept.length) ctx.logger?.info?.(`studio: kept user-edited ${skill.name}/${kept.join(", ")}`);
3911
+ });
3912
+ }).catch((error) => ctx.logger?.warn?.(`studio: skill install skipped: ${String(error)}`));
3589
3913
  this.snapshot = new Promise((resolve6) => {
3590
3914
  setImmediate(resolve6);
3591
3915
  }).then(() => this.capture());