@matamt-dbg/dsh-studio 0.3.0 → 0.4.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.4.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") 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";
@@ -1960,6 +2040,26 @@ var PixieGateStore = class {
1960
2040
  return structuredClone(run);
1961
2041
  });
1962
2042
  }
2043
+ /** Gates switched off for this session; a missing record means none. */
2044
+ async disabledGates(sessionId) {
2045
+ return [...(await this.read(sessionId)).disabledGateIds ?? []];
2046
+ }
2047
+ /** Switch one gate for this session only. Shares the run queue, so it never races a run commit. */
2048
+ setGateEnabled(sessionId, gateId, enabled) {
2049
+ if (typeof gateId !== "string" || !gateId) throw new Error("Gate id is required");
2050
+ return this.enqueue(sessionId, async () => {
2051
+ const record5 = await this.read(sessionId);
2052
+ const current = new Set(record5.disabledGateIds ?? []);
2053
+ if (enabled) current.delete(gateId);
2054
+ else current.add(gateId);
2055
+ const next = [...current];
2056
+ if (next.length) record5.disabledGateIds = next;
2057
+ else delete record5.disabledGateIds;
2058
+ await this.save(sessionId, record5);
2059
+ for (const listener of this.listeners.get(sessionId) ?? []) listener({ type: "session", disabledGateIds: [...next] });
2060
+ return next;
2061
+ });
2062
+ }
1963
2063
  /** Sync means refetch durable history; only committed running executions are replayed. No await between snapshot and subscribe. */
1964
2064
  async *follow(sessionId, signal) {
1965
2065
  this.file(sessionId);
@@ -2411,6 +2511,14 @@ function parsePixieSettings(value) {
2411
2511
  if (typeof raw.judgeSystemPrompt !== "string") throw new Error("Pixie settings judgeSystemPrompt must be a string");
2412
2512
  settings.judgeSystemPrompt = raw.judgeSystemPrompt;
2413
2513
  }
2514
+ if (raw.judgeEngine !== void 0) {
2515
+ if (raw.judgeEngine !== "llm" && raw.judgeEngine !== "jev") throw new Error("Pixie settings judgeEngine must be llm or jev");
2516
+ settings.judgeEngine = raw.judgeEngine;
2517
+ }
2518
+ if (raw.jevModel !== void 0) {
2519
+ 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");
2520
+ settings.jevModel = raw.jevModel;
2521
+ }
2414
2522
  return settings;
2415
2523
  }
2416
2524
  function parsePixieModel(value) {
@@ -2461,75 +2569,6 @@ var PixieSettingsStore = class {
2461
2569
  import { mkdir as mkdir5, readFile as readFile6, rename as rename5, writeFile as writeFile5 } from "node:fs/promises";
2462
2570
  import { dirname as dirname5 } from "node:path";
2463
2571
  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
2572
  var diagnostic = (error, code) => ({ code: typeof error?.code === "string" ? error.code : code, message: error instanceof Error ? error.message : String(error) });
2534
2573
  function decode(text2) {
2535
2574
  let value;
@@ -2599,6 +2638,15 @@ async function readRuntimeGateDefinitions(source) {
2599
2638
  return [];
2600
2639
  }
2601
2640
  }
2641
+ async function readSessionGateDefinitions(source, sessionId) {
2642
+ const [rows, disabled] = await Promise.all([readRuntimeGateDefinitions(source), source.disabledGates?.(sessionId).catch((error) => {
2643
+ source.warn?.(`studio pixie: session gate switches unreadable for ${sessionId}: ${String(error)}`);
2644
+ return [];
2645
+ }) ?? Promise.resolve([])]);
2646
+ if (!disabled.length) return rows;
2647
+ const off = new Set(disabled);
2648
+ return rows.filter((row) => !off.has(row.id));
2649
+ }
2602
2650
 
2603
2651
  // src/host/pixie-gate-input.ts
2604
2652
  import { randomUUID as randomUUID7 } from "node:crypto";
@@ -2622,6 +2670,7 @@ async function executeTask(task, original, signal, runner, context) {
2622
2670
  records: execution.helperCalls ?? [],
2623
2671
  hooks: {},
2624
2672
  appliedPolicy: task.advanced.onError,
2673
+ ...execution.concludedBy ? { concludedBy: execution.concludedBy } : {},
2625
2674
  ...plan.stop || execution.error ? { forwardError: { verdict: execution.error ? "error" : "fail", reason: plan.reason ?? "Revision requires confirmation", ...plan.suggestion !== void 0 ? { text: plan.suggestion } : {} } } : {},
2626
2675
  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
2676
  };
@@ -2649,7 +2698,7 @@ function gateInput(deps) {
2649
2698
  if (decision.kind === "reject") return decision;
2650
2699
  const preset = deps.preset(payload.agent);
2651
2700
  if (!preset) return decision;
2652
- const rows = await readRuntimeGateDefinitions(deps);
2701
+ const rows = await readSessionGateDefinitions(deps, payload.agent.id);
2653
2702
  const root = deps.isRoot(payload.agent);
2654
2703
  const targets = decision.messages.flatMap((message) => {
2655
2704
  const when = message.source.kind === "user" ? "user-message" : message.source.kind === "agent-message" ? "agent-message" : void 0;
@@ -2720,7 +2769,7 @@ function gateTurn(deps) {
2720
2769
  return async ({ agent, turn, signal }) => {
2721
2770
  const preset = deps.preset(agent);
2722
2771
  if (!preset) return;
2723
- const gate = (await readRuntimeGateDefinitions(deps)).find((row) => row.when === "turn-end" && taskMatches(row, preset, deps.isRoot(agent)));
2772
+ const gate = (await readSessionGateDefinitions(deps, agent.id)).find((row) => row.when === "turn-end" && taskMatches(row, preset, deps.isRoot(agent)));
2724
2773
  if (!gate) return;
2725
2774
  const original = [...agent.session.deriveMessages()].reverse().find((message2) => message2.role === "assistant");
2726
2775
  const subject = original ? textOf3(original) : "";
@@ -2759,7 +2808,7 @@ function gateTools(deps) {
2759
2808
  if (!agent) return decision;
2760
2809
  const preset = deps.preset(agent);
2761
2810
  if (!preset) return decision;
2762
- const gate = (await readRuntimeGateDefinitions(deps)).find((row) => row.when === point && taskMatches(row, preset, deps.isRoot(agent)));
2811
+ const gate = (await readSessionGateDefinitions(deps, agent.id)).find((row) => row.when === point && taskMatches(row, preset, deps.isRoot(agent)));
2763
2812
  if (!gate) return decision;
2764
2813
  const turn = [...agent.session.snapshotEvents()].reverse().find((event) => event.type === "turn/start")?.data.turn ?? 0;
2765
2814
  const contexts = "additionalContexts" in decision ? decision.additionalContexts : void 0;
@@ -2822,89 +2871,184 @@ function gateTools(deps) {
2822
2871
  };
2823
2872
  }
2824
2873
 
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 };
2874
+ // src/host/pixie-gate-helper.ts
2875
+ import { createHash, randomBytes } from "node:crypto";
2876
+ import { chmod, mkdir as mkdir6, rename as rename6, writeFile as writeFile6 } from "node:fs/promises";
2877
+ import { join as join5 } from "node:path";
2878
+ var HELPER_ROUTE = "/studio/pixie-gate/helper";
2879
+ var HELPER_LIMITS = { calls: 20, bodyBytes: 256 * 1024 };
2880
+ function helperJudgement(request, answer, meta = {}) {
2881
+ const extra = { ...meta.engine ? { engine: meta.engine } : {}, ...meta.model ? { model: meta.model } : {} };
2882
+ if (answer.type === "noul") {
2883
+ const pass = answer.noul >= request.threshold;
2884
+ return { exit: pass ? 0 : 1, text: String(answer.noul), json: { type: "bool", label: pass ? "yes" : "no", answer, threshold: request.threshold, ...extra } };
2885
+ }
2886
+ if (answer.type === "choice") return { exit: 0, text: answer.choice, json: { type: "select", label: answer.choice, answer, ...extra } };
2887
+ return { exit: 0, text: String(answer.score), json: { type: "score", label: String(Math.round(answer.score)), answer, ...extra } };
2850
2888
  }
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])]));
2889
+ function helperJudgeRequest(form) {
2890
+ const type = form.get("type") ?? "bool";
2891
+ if (!["bool", "select", "score"].includes(type)) throw new Error("type must be bool, select, or score");
2892
+ const instructions2 = form.get("question")?.trim();
2893
+ if (!instructions2) throw new Error("question is required");
2894
+ const raw = form.get("threshold"), threshold = raw === null ? 0.5 : Number(raw);
2895
+ if (!(threshold >= 0 && threshold <= 1)) throw new Error("threshold must be between 0 and 1");
2896
+ let question;
2897
+ if (type === "bool") {
2898
+ const yes = form.get("true"), no = form.get("false");
2899
+ question = { type: "noul", instructions: instructions2, ...yes !== null || no !== null ? { criteria: { ...yes !== null ? { true: yes } : {}, ...no !== null ? { false: no } : {} } } : {} };
2900
+ } else if (type === "select") {
2901
+ const criteria = {};
2902
+ for (const option of form.getAll("option")) {
2903
+ const at = option.indexOf("=");
2904
+ const name = (at < 0 ? option : option.slice(0, at)).trim();
2905
+ if (!name || Object.hasOwn(criteria, name)) throw new Error(`option names must be unique and non-empty: ${option}`);
2906
+ criteria[name] = at < 0 ? name : option.slice(at + 1);
2907
+ }
2908
+ question = { type: "choice", instructions: instructions2, criteria };
2909
+ } else question = { type: "score", instructions: instructions2, criteria: form.getAll("level") };
2910
+ validateJudgeRequest({ state: "", questions: { helper: question } });
2911
+ return { type, question, threshold };
2862
2912
  }
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;
2913
+ var HELPER_SCRIPT = `#!/usr/bin/env bash
2914
+ # Pixie Gate hook helper, generated by DSH Studio. Valid only inside one hook script run.
2915
+ # pixie ask "<instruction>" [--input]
2916
+ # pixie judge [--type bool|select|score] --question "<question>" [--threshold 0.5] [--true ..] [--false ..]
2917
+ # [--option name=desc ..] [--level desc ..] [--json] [--input]
2918
+ # --input reads the helper input from stdin; otherwise the gate target is used.
2919
+ set -u
2920
+ fail() { printf 'pixie: %s\\n' "$1" >&2; exit 2; }
2921
+ [ -n "\${PIXIE_URL:-}" ] && [ -n "\${PIXIE_TOKEN:-}" ] || fail 'helper is not available outside a Pixie Gate hook script run'
2922
+ command -v curl >/dev/null 2>&1 || fail 'curl is required'
2923
+ command="\${1:-}"; [ $# -gt 0 ] && shift
2924
+ input=0; args=()
2925
+ case "$command" in
2926
+ ask)
2927
+ [ $# -ge 1 ] || fail 'usage: pixie ask "<instruction>" [--input]'
2928
+ args+=(--data-urlencode "instruction=$1"); shift
2929
+ while [ $# -gt 0 ]; do case "$1" in --input) input=1 ;; *) fail "unknown option: $1" ;; esac; shift; done ;;
2930
+ judge)
2931
+ question=''; json=0
2932
+ while [ $# -gt 0 ]; do case "$1" in
2933
+ --type|--question|--threshold|--true|--false|--option|--level)
2934
+ [ $# -ge 2 ] || fail "$1 needs a value"
2935
+ [ "$1" = --question ] && question="$2"
2936
+ args+=(--data-urlencode "\${1#--}=$2"); shift ;;
2937
+ --json) json=1 ;;
2938
+ --input) input=1 ;;
2939
+ *) fail "unknown option: $1" ;;
2940
+ esac; shift; done
2941
+ [ -n "$question" ] || fail 'usage: pixie judge [--type bool|select|score] --question "<question>" [--threshold 0.5] [--true ..] [--false ..] [--option name=desc ..] [--level desc ..] [--json] [--input]' ;;
2942
+ *) fail 'usage: pixie ask|judge ...' ;;
2943
+ esac
2944
+ [ "$input" = 1 ] && args+=(--data-urlencode 'input@-')
2945
+ # The token travels in a config on a file descriptor, never on the command line.
2946
+ 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"
2947
+ code=\${out##*$'\\n'}; body=\${out%$'\\n'*}
2948
+ [ "$code" = 200 ] || fail "HTTP $code: $body"
2949
+ if [ "$command" = ask ]; then printf '%s\\n' "$body"; exit 0; fi
2950
+ # judge body: exit code, plain answer, then the JSON line.
2951
+ status=\${body%%$'\\n'*}; rest=\${body#*$'\\n'}; text=\${rest%%$'\\n'*}; line=\${rest#*$'\\n'}
2952
+ if [ "$json" = 1 ]; then printf '%s\\n' "$line"; else printf '%s\\n' "$text"; fi
2953
+ exit "$status"
2954
+ `;
2955
+ var hash = (token) => createHash("sha256").update(token).digest("hex");
2956
+ var send = (res, status, text2) => {
2957
+ if (res.headersSent) return;
2958
+ res.writeHead(status, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
2959
+ res.end(text2);
2960
+ };
2961
+ var PixieGateHelper = class {
2962
+ constructor(options) {
2963
+ this.options = options;
2964
+ this.limits = options.limits ?? HELPER_LIMITS;
2868
2965
  }
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
- }
2966
+ grants = /* @__PURE__ */ new Map();
2967
+ installed;
2968
+ limits;
2969
+ /** Environment for one script run. Without a reachable route only PATH is given and `pixie` explains why. */
2970
+ async issue(grant) {
2971
+ await (this.installed ??= this.install().catch((error) => {
2972
+ this.installed = void 0;
2973
+ throw error;
2974
+ }));
2975
+ const env = { PATH: `${this.options.binDirectory}:${process.env.PATH ?? "/usr/bin:/bin"}` };
2976
+ const url = this.options.url();
2977
+ if (!url) return { env, revoke() {
2978
+ } };
2979
+ const token = randomBytes(32).toString("hex"), key = hash(token);
2980
+ const entry = { grant, calls: 0, revoked: new AbortController() };
2981
+ this.grants.set(key, entry);
2982
+ return { env: { ...env, PIXIE_URL: url, PIXIE_TOKEN: token }, revoke: () => {
2983
+ this.grants.delete(key);
2984
+ entry.revoked.abort(new Error("Pixie helper token revoked"));
2985
+ } };
2986
+ }
2987
+ handle = async (req, res) => {
2988
+ const action = new URL(req.url ?? "/", "http://x").pathname.slice(HELPER_ROUTE.length);
2989
+ if (action !== "/ask" && action !== "/judge") return send(res, 404, "unknown helper");
2990
+ if (req.method !== "POST") return send(res, 405, "POST only");
2991
+ const header = req.headers.authorization;
2992
+ const entry = typeof header === "string" && header.startsWith("Bearer ") ? this.grants.get(hash(header.slice(7))) : void 0;
2993
+ if (!entry) return send(res, 401, "invalid or expired token");
2994
+ if (entry.calls >= this.limits.calls) return send(res, 429, `helper call limit (${this.limits.calls}) reached`);
2995
+ const body = await this.read(req);
2996
+ if (body === void 0) return send(res, 413, `helper request exceeds ${this.limits.bodyBytes} bytes`);
2997
+ if (!this.grants.has(hash(header.slice(7)))) return send(res, 401, "invalid or expired token");
2998
+ const form = new URLSearchParams(body);
2999
+ const input = form.get("input") ?? entry.grant.target;
3000
+ const closed = new AbortController();
3001
+ res.on("close", () => {
3002
+ if (!res.writableFinished) closed.abort(new Error("Helper client disconnected"));
3003
+ });
3004
+ const signal = AbortSignal.any([entry.grant.signal, entry.revoked.signal, closed.signal]);
3005
+ let call;
3006
+ if (action === "/ask") {
3007
+ const instruction = form.get("instruction")?.trim();
3008
+ if (!instruction) return send(res, 400, "instruction is required");
3009
+ entry.calls++;
3010
+ call = entry.grant.ask(instruction, input, signal);
3011
+ } else {
3012
+ let request;
3013
+ try {
3014
+ request = helperJudgeRequest(form);
3015
+ } catch (error) {
3016
+ return send(res, 400, error instanceof Error ? error.message : String(error));
2901
3017
  }
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 } : {} };
3018
+ entry.calls++;
3019
+ call = entry.grant.judge(request, input, signal).then((result) => `${result.exit}
3020
+ ${result.text.replace(/\n/g, " ")}
3021
+ ${JSON.stringify(result.json)}`);
3022
+ }
3023
+ try {
3024
+ send(res, 200, await call);
2905
3025
  } catch (error) {
2906
- return { ok: false, error: error instanceof Error ? error.message : String(error), ...usage ? { usage } : {} };
3026
+ send(res, signal.aborted ? 503 : 502, error instanceof Error ? error.message : String(error));
2907
3027
  }
3028
+ };
3029
+ /** Body text, or undefined above the size limit (the rest is drained, then the socket closed past 4×). */
3030
+ read(req) {
3031
+ return new Promise((resolve6, reject) => {
3032
+ const chunks = [];
3033
+ let size = 0;
3034
+ req.on("data", (chunk) => {
3035
+ size += chunk.length;
3036
+ if (size <= this.limits.bodyBytes) chunks.push(chunk);
3037
+ else if (size > this.limits.bodyBytes * 4) {
3038
+ req.destroy();
3039
+ resolve6(void 0);
3040
+ }
3041
+ });
3042
+ req.on("end", () => resolve6(size > this.limits.bodyBytes ? void 0 : Buffer.concat(chunks).toString("utf8")));
3043
+ req.on("error", reject);
3044
+ });
3045
+ }
3046
+ async install() {
3047
+ await mkdir6(this.options.binDirectory, { recursive: true });
3048
+ const path = join5(this.options.binDirectory, "pixie"), temp = `${path}.${process.pid}.tmp`;
3049
+ await writeFile6(temp, HELPER_SCRIPT, { mode: 493 });
3050
+ await chmod(temp, 493);
3051
+ await rename6(temp, path);
2908
3052
  }
2909
3053
  };
2910
3054
 
@@ -2959,7 +3103,8 @@ var GateTaskRunner = class {
2959
3103
  } finally {
2960
3104
  session?.revoke();
2961
3105
  }
2962
- if (hook.skipped) return { status: "done", skipped: true, output: { verdict: "pass", note: "skipped by hook" }, usage };
3106
+ if (hook.skipped) return { status: "done", skipped: true, concludedBy: "skipped", output: { verdict: "pass", note: "skipped by hook" }, usage };
3107
+ if ("direct" in hook) return { status: "done", concludedBy: "script", output: hook.direct, usage };
2963
3108
  instruction = hook.instruction;
2964
3109
  }
2965
3110
  if (!this.deps.model) throw new Error("No Pixie model configured");
@@ -2990,7 +3135,7 @@ Instruction: ${instruction}`,
2990
3135
  signal.throwIfAborted();
2991
3136
  if (!finished) throw new Error("Gate model stream ended without a finish");
2992
3137
  const output = parseGateOutput(text2);
2993
- return { status: "done", output, usage };
3138
+ return { status: "done", concludedBy: "pixie", output, usage };
2994
3139
  } catch (error) {
2995
3140
  return { status: parent.aborted ? "cancelled" : "error", error: String(signal.aborted ? signal.reason : error), usage };
2996
3141
  } finally {
@@ -3034,168 +3179,34 @@ Instruction: ${instruction}`,
3034
3179
  record5("ask", input, { verdict: "note", text: text2, reason: instruction, ...usage ? { usage } : {} });
3035
3180
  return text2;
3036
3181
  },
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");
3182
+ judge: async (request, input, callSignal) => {
3183
+ const reason = request.question.instructions;
3184
+ const engine = this.deps.judge;
3185
+ if (!engine) {
3186
+ record5("judge", input, { verdict: "error", error: "No judge engine configured", reason });
3187
+ throw new Error("No judge engine configured");
3042
3188
  }
3043
- const result = await new LlmJudgeEngine(this.deps.llm, model).judge({ state: input, questions: { helper: { type: "noul", question } } }, callSignal);
3189
+ const result = await engine.judge({ state: input, questions: { helper: request.question } }, callSignal);
3044
3190
  const usage = result.usage ? { usage: result.usage } : {};
3045
3191
  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 });
3192
+ if (!answer) {
3193
+ const error = result.ok ? "Judge returned no answer" : result.error;
3194
+ record5("judge", input, { verdict: "error", error, reason, data: { type: request.type, engine: engine.label }, ...usage });
3049
3195
  throw new Error(error);
3050
3196
  }
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 };
3197
+ const judgement = helperJudgement(request, answer, { engine: engine.label });
3198
+ record5("judge", input, {
3199
+ verdict: judgement.exit === 0 ? "pass" : "fail",
3200
+ reason,
3201
+ ...usage,
3202
+ data: { type: request.type, label: judgement.json.label, engine: engine.label, ...request.type === "bool" && answer.type === "noul" ? { noul: answer.noul, threshold: request.threshold } : { answer } }
3203
+ });
3204
+ return judgement;
3054
3205
  }
3055
3206
  };
3056
3207
  }
3057
3208
  };
3058
3209
 
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
3210
  // src/host/pixie-gate-settings.ts
3200
3211
  import { randomUUID as randomUUID10 } from "node:crypto";
3201
3212
  import { mkdir as mkdir7, readFile as readFile7, rename as rename7, writeFile as writeFile7 } from "node:fs/promises";
@@ -3256,17 +3267,203 @@ async function trialGate(payload, deps, signal) {
3256
3267
  return { output: execution.output, plan: gateOutputPlan(task, execution.output, raw.text), usage: execution.usage };
3257
3268
  }
3258
3269
 
3270
+ // src/host/pixie-judge.ts
3271
+ var TOLERANCE = 0.02;
3272
+ var isProbability = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
3273
+ var sumsToOne = (values) => Math.abs(values.reduce((sum, value) => sum + value, 0) - 1) <= TOLERANCE;
3274
+ function checkAnswer(id, question, raw) {
3275
+ const answer = raw;
3276
+ if (!answer || typeof answer !== "object") throw new Error(`Missing answer for "${id}"`);
3277
+ if (question.type === "choice") {
3278
+ const names3 = Object.keys(question.criteria);
3279
+ const probabilities = answer.probabilities;
3280
+ if (typeof answer.choice !== "string" || !names3.includes(answer.choice)) throw new Error(`Answer "${id}" chose an unknown option`);
3281
+ 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`);
3282
+ const values = names3.map((name) => probabilities[name]);
3283
+ if (!sumsToOne(values)) throw new Error(`Answer "${id}" probabilities must sum to 1`);
3284
+ return { type: "choice", choice: answer.choice, probabilities: Object.fromEntries(names3.map((name) => [name, probabilities[name]])), confidence: isProbability(answer.confidence) ? answer.confidence : Math.max(...values) };
3285
+ }
3286
+ if (question.type === "score") {
3287
+ const probabilities = answer.probabilities;
3288
+ const top = question.criteria.length - 1;
3289
+ 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}`);
3290
+ 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`);
3291
+ return { type: "score", score: answer.score, probabilities, confidence: isProbability(answer.confidence) ? answer.confidence : Math.max(...probabilities) };
3292
+ }
3293
+ if (!isProbability(answer.noul)) throw new Error(`Answer "${id}" noul must be a probability in 0..1`);
3294
+ return { type: "noul", noul: answer.noul };
3295
+ }
3296
+ function parseJudgeOutput(text2, questions) {
3297
+ const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/.exec(text2.trim());
3298
+ let parsed;
3299
+ try {
3300
+ parsed = JSON.parse(fenced ? fenced[1] : text2.trim());
3301
+ } catch {
3302
+ throw new Error("Judge model did not return JSON");
3303
+ }
3304
+ const answers = parsed?.answers;
3305
+ if (!answers || typeof answers !== "object") throw new Error("Judge model output has no answers object");
3306
+ return Object.fromEntries(Object.entries(questions).map(([id, question]) => [id, checkAnswer(id, question, answers[id])]));
3307
+ }
3308
+ var LlmJudgeEngine = class {
3309
+ constructor(llm, model, system = DEFAULT_JUDGE_SYSTEM_PROMPT) {
3310
+ this.llm = llm;
3311
+ this.model = model;
3312
+ this.system = system;
3313
+ }
3314
+ kind = "judge";
3315
+ label = "llm";
3316
+ async judge(request, signal) {
3317
+ let usage;
3318
+ try {
3319
+ const { state, questions } = validateJudgeRequest(request);
3320
+ const prompt = `State:
3321
+ ${typeof state === "string" ? state : JSON.stringify(state, null, 2)}
3322
+
3323
+ Questions:
3324
+ ${JSON.stringify(questions, null, 2)}`;
3325
+ let text2 = "";
3326
+ let finished = false;
3327
+ for await (const chunk of untilAborted(this.llm.stream({
3328
+ provider: this.model.provider,
3329
+ model: this.model.model,
3330
+ ...this.model.reasoningEffort ? { reasoningEffort: this.model.reasoningEffort } : {},
3331
+ ...this.model.maxTokens ? { maxTokens: this.model.maxTokens } : {},
3332
+ system: this.system,
3333
+ messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
3334
+ signal
3335
+ }), signal)) {
3336
+ if (chunk.type === "text-delta" && typeof chunk.text === "string") text2 += chunk.text;
3337
+ if (chunk.type === "usage" && chunk.usage && typeof chunk.usage === "object") {
3338
+ const raw = chunk.usage;
3339
+ usage = { inputTokens: typeof raw.inputTokens === "number" ? raw.inputTokens : 0, outputTokens: typeof raw.outputTokens === "number" ? raw.outputTokens : 0 };
3340
+ for (const key of ["cacheReadTokens", "cacheWriteTokens", "reasoningTokens"]) if (typeof raw[key] === "number") usage[key] = raw[key];
3341
+ }
3342
+ if (chunk.type === "finish") {
3343
+ const reason = chunk.reason;
3344
+ if (reason?.kind !== "stop") throw new Error(reason?.failure?.message ?? `Judge model ended with ${reason?.kind ?? "unknown"}`);
3345
+ finished = true;
3346
+ }
3347
+ }
3348
+ signal.throwIfAborted();
3349
+ if (!finished) throw new Error("Judge model stream ended without a finish");
3350
+ return { ok: true, answers: parseJudgeOutput(text2, questions), ...usage ? { usage } : {} };
3351
+ } catch (error) {
3352
+ return { ok: false, error: error instanceof Error ? error.message : String(error), ...usage ? { usage } : {} };
3353
+ }
3354
+ }
3355
+ };
3356
+
3357
+ // src/host/jev-judge.ts
3358
+ var JEV_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
3359
+ var JEV_KEY_REF = "TYPESAFE_API_KEY";
3360
+ var DEFAULT_JEV_MODEL = "jev-latest";
3361
+ var RETRIES = 2;
3362
+ var MAX_WAIT_MS = 4e3;
3363
+ function fromWire(question, raw) {
3364
+ if (question.type !== "score" || !raw || typeof raw !== "object") return raw;
3365
+ const answer = raw;
3366
+ const probabilities = answer.probabilities;
3367
+ if (!probabilities || typeof probabilities !== "object" || Array.isArray(probabilities)) return raw;
3368
+ const keyed = probabilities;
3369
+ if (Object.keys(keyed).length !== question.criteria.length) return { ...answer, probabilities: void 0 };
3370
+ return { ...answer, probabilities: question.criteria.map((_, index) => keyed[String(index)]) };
3371
+ }
3372
+ var wait = (ms, signal) => new Promise((resolve6, reject) => {
3373
+ if (signal.aborted) {
3374
+ reject(signal.reason);
3375
+ return;
3376
+ }
3377
+ const timer = setTimeout(() => {
3378
+ signal.removeEventListener("abort", abort);
3379
+ resolve6();
3380
+ }, ms);
3381
+ const abort = () => {
3382
+ clearTimeout(timer);
3383
+ reject(signal.reason);
3384
+ };
3385
+ signal.addEventListener("abort", abort, { once: true });
3386
+ });
3387
+ function retryDelay(retryAfter, attempt) {
3388
+ const seconds = retryAfter === null ? NaN : Number(retryAfter);
3389
+ return Math.min(Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : 500 * 2 ** attempt, MAX_WAIT_MS);
3390
+ }
3391
+ var JevJudgeEngine = class {
3392
+ constructor(deps) {
3393
+ this.deps = deps;
3394
+ }
3395
+ kind = "judge";
3396
+ label = "jev";
3397
+ async judge(request, signal) {
3398
+ let usage;
3399
+ try {
3400
+ const { state, questions } = validateJudgeRequest(request);
3401
+ if (!this.deps.credentials) throw new Error("DSH credentials service is unavailable");
3402
+ const key = (await this.deps.credentials.resolve(JEV_KEY_REF))?.value;
3403
+ if (!key) throw new Error(`Jev API key is not configured (${JEV_KEY_REF})`);
3404
+ signal.throwIfAborted();
3405
+ const send2 = this.deps.fetch ?? globalThis.fetch;
3406
+ const body = JSON.stringify({ model: this.deps.model, state, questions });
3407
+ for (let attempt = 0; ; attempt++) {
3408
+ const response = await send2(JEV_ENDPOINT, {
3409
+ method: "POST",
3410
+ signal,
3411
+ body,
3412
+ headers: { authorization: `Bearer ${key}`, "content-type": "application/json" }
3413
+ });
3414
+ const text2 = await response.text();
3415
+ if ((response.status === 429 || response.status === 529) && attempt < RETRIES) {
3416
+ await wait(retryDelay(response.headers.get("retry-after"), attempt), signal);
3417
+ continue;
3418
+ }
3419
+ if (response.status !== 200) throw new Error(`Jev request failed: HTTP ${response.status}${text2 ? `: ${text2.slice(0, 500)}` : ""}`);
3420
+ let parsed;
3421
+ try {
3422
+ parsed = JSON.parse(text2);
3423
+ } catch {
3424
+ throw new Error("Jev response is not JSON");
3425
+ }
3426
+ const raw = parsed.usage;
3427
+ if (raw && typeof raw.input_tokens === "number" && typeof raw.output_tokens === "number") usage = { inputTokens: raw.input_tokens, outputTokens: raw.output_tokens };
3428
+ const answers = parsed.answers;
3429
+ if (!answers || typeof answers !== "object") throw new Error("Jev response has no answers object");
3430
+ const typed = Object.fromEntries(Object.entries(questions).map(([id, question]) => [id, checkAnswer(id, question, fromWire(question, answers[id]))]));
3431
+ return { ok: true, answers: typed, ...usage ? { usage } : {} };
3432
+ }
3433
+ } catch (error) {
3434
+ const message = signal.aborted ? String(signal.reason instanceof Error ? signal.reason.message : signal.reason ?? "aborted") : error instanceof Error ? error.message : String(error);
3435
+ return { ok: false, error: message, ...usage ? { usage } : {} };
3436
+ }
3437
+ }
3438
+ };
3439
+ function judgeEngineFor(settings, deps) {
3440
+ if (settings.judgeEngine === "jev") return new JevJudgeEngine({ credentials: deps.credentials, model: settings.jevModel ?? DEFAULT_JEV_MODEL, ...deps.fetch ? { fetch: deps.fetch } : {} });
3441
+ const model = settings.judgeModel ?? settings.model;
3442
+ return model ? new LlmJudgeEngine(deps.llm, model, settings.judgeSystemPrompt || void 0) : void 0;
3443
+ }
3444
+
3259
3445
  // src/host/pixie-service.ts
3260
3446
  var PRUNE_INTERVAL_MS = 10 * 60 * 1e3;
3261
3447
  var json2 = (payload) => JSON.parse(payload);
3448
+ async function credentialWrite(write) {
3449
+ try {
3450
+ await write();
3451
+ } catch (error) {
3452
+ const message = error instanceof Error ? error.message : String(error);
3453
+ if (/Expected YAML collection at refs\b/.test(message)) throw new Error(`[pixie/credentials-refs] ${message}`);
3454
+ throw error;
3455
+ }
3456
+ }
3262
3457
  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) {
3458
+ 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;
3459
+ 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
3460
  constructor(ctx, dataDir) {
3266
3461
  super(ctx, "studioPixie");
3267
3462
  __runInitializers(_init, 5, this);
3268
3463
  __publicField(this, "defaultScriptDirectory");
3269
3464
  __publicField(this, "gateSettings");
3465
+ /** Optional: the DSH credentials service, read per call so a later mount applies. */
3466
+ __publicField(this, "credentials", () => this.ctx.get("credentials"));
3270
3467
  __publicField(this, "scriptSupported", () => process.platform !== "win32" && !!this.ctx.get("shell"));
3271
3468
  __publicField(this, "store");
3272
3469
  __publicField(this, "settings");
@@ -3297,6 +3494,7 @@ var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote]
3297
3494
  });
3298
3495
  const gateDeps = {
3299
3496
  definitions: () => this.definitions.read(),
3497
+ disabledGates: (sessionId) => this.gates.disabledGates(sessionId),
3300
3498
  warn: (message) => ctx.logger.warn(message),
3301
3499
  preset: (agent) => ctx.get("agentPresets")?.composedPreset(agent.ctx),
3302
3500
  isRoot: (agent) => ctx.get("agents")?.roots().some((root) => root.id === agent.id) ?? false,
@@ -3312,14 +3510,17 @@ var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote]
3312
3510
  };
3313
3511
  },
3314
3512
  runner: async () => {
3315
- const [{ model }, scriptDirectory] = await Promise.all([this.settings.read(), this.scriptDirectory()]);
3513
+ const [settings, scriptDirectory] = await Promise.all([this.settings.read(), this.scriptDirectory()]);
3514
+ const llm = { stream: (options) => llmOf(ctx).stream(options) };
3515
+ const judge = judgeEngineFor(settings, { llm, credentials: this.credentials() });
3316
3516
  return new GateTaskRunner({
3317
3517
  scriptDirectory,
3318
3518
  scriptSupported: this.scriptSupported(),
3319
3519
  shell: ctx.get("shell"),
3320
3520
  helper: this.helper,
3321
- llm: { stream: (options) => llmOf(ctx).stream(options) },
3322
- ...model ? { model } : {}
3521
+ llm,
3522
+ ...settings.model ? { model: settings.model } : {},
3523
+ ...judge ? { judge } : {}
3323
3524
  });
3324
3525
  },
3325
3526
  recorder: this.gates,
@@ -3469,6 +3670,24 @@ var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote]
3469
3670
  async resolveGateRejection(sessionId, runId, status) {
3470
3671
  return this.gates.resolveRejection(sessionId, runId, status);
3471
3672
  }
3673
+ async sessionGates(sessionId) {
3674
+ const agent = this.ctx.get("agents")?.get(sessionId);
3675
+ const preset = agent ? this.ctx.get("agentPresets")?.composedPreset(agent.ctx) : void 0;
3676
+ if (!preset) return { gates: [] };
3677
+ const [read, disabled] = await Promise.all([this.definitions.read(), this.gates.disabledGates(sessionId)]);
3678
+ const off = new Set(disabled);
3679
+ return { preset, gates: read.entries.filter((task) => enabledFor(task, preset)).map((task) => ({
3680
+ id: task.id,
3681
+ name: task.name,
3682
+ when: task.when,
3683
+ ...task.advanced.script ? { script: task.advanced.script } : {},
3684
+ sessionEnabled: !off.has(task.id)
3685
+ })) };
3686
+ }
3687
+ async setSessionGate(sessionId, gateId, enabled) {
3688
+ if (typeof enabled !== "boolean") throw new Error("enabled must be a boolean");
3689
+ return this.gates.setGateEnabled(sessionId, gateId, enabled);
3690
+ }
3472
3691
  async *followGates(sessionId, signal) {
3473
3692
  yield* this.gates.follow(sessionId, signal);
3474
3693
  }
@@ -3478,6 +3697,26 @@ var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote]
3478
3697
  async saveSettings(payload) {
3479
3698
  return this.settings.save(json2(payload));
3480
3699
  }
3700
+ async jevKeyStatus() {
3701
+ const credentials = this.credentials();
3702
+ if (!credentials) return { available: false, configured: false, writable: false };
3703
+ const info = await credentials.describe(JEV_KEY_REF);
3704
+ return { available: true, configured: info.configured, writable: info.writable, ...info.source ? { source: info.source } : {} };
3705
+ }
3706
+ async saveJevKey(payload) {
3707
+ const value = json2(payload).value;
3708
+ if (typeof value !== "string" || !value.trim()) throw new Error("Jev API key is required");
3709
+ const credentials = this.credentials();
3710
+ if (!credentials) throw new Error("DSH credentials service is unavailable");
3711
+ await credentialWrite(() => credentials.set(JEV_KEY_REF, value.trim()));
3712
+ return this.jevKeyStatus();
3713
+ }
3714
+ async deleteJevKey() {
3715
+ const credentials = this.credentials();
3716
+ if (!credentials) throw new Error("DSH credentials service is unavailable");
3717
+ await credentialWrite(() => credentials.unset(JEV_KEY_REF));
3718
+ return this.jevKeyStatus();
3719
+ }
3481
3720
  async models() {
3482
3721
  return this.ctx.get("sessionController").modelCatalog();
3483
3722
  }
@@ -3498,16 +3737,91 @@ __decorateElement(_init, 1, "gateDefinitions", _gateDefinitions_dec, PixieServic
3498
3737
  __decorateElement(_init, 1, "saveGateDefinitions", _saveGateDefinitions_dec, PixieService);
3499
3738
  __decorateElement(_init, 1, "gateRuns", _gateRuns_dec, PixieService);
3500
3739
  __decorateElement(_init, 1, "resolveGateRejection", _resolveGateRejection_dec, PixieService);
3740
+ __decorateElement(_init, 1, "sessionGates", _sessionGates_dec, PixieService);
3741
+ __decorateElement(_init, 1, "setSessionGate", _setSessionGate_dec, PixieService);
3501
3742
  __decorateElement(_init, 1, "followGates", _followGates_dec, PixieService);
3502
3743
  __decorateElement(_init, 1, "readSettings", _readSettings_dec, PixieService);
3503
3744
  __decorateElement(_init, 1, "saveSettings", _saveSettings_dec, PixieService);
3745
+ __decorateElement(_init, 1, "jevKeyStatus", _jevKeyStatus_dec, PixieService);
3746
+ __decorateElement(_init, 1, "saveJevKey", _saveJevKey_dec, PixieService);
3747
+ __decorateElement(_init, 1, "deleteJevKey", _deleteJevKey_dec, PixieService);
3504
3748
  __decorateElement(_init, 1, "models", _models_dec, PixieService);
3505
3749
  __decoratorMetadata(_init, PixieService);
3506
3750
  __publicField(PixieService, "inject", ["llm", "sessionController", "commands"]);
3507
3751
 
3752
+ // src/host/skill-install.ts
3753
+ import { createHash as createHash2 } from "node:crypto";
3754
+ import { mkdir as mkdir8, readFile as readFile8, rename as rename8, writeFile as writeFile8 } from "node:fs/promises";
3755
+ import { join as join7 } from "node:path";
3756
+ var MARKER = "studio-managed";
3757
+ var MANIFEST = ".studio-managed.json";
3758
+ var digest = (text2) => createHash2("sha256").update(text2).digest("hex");
3759
+ async function installSkill(skillsRoot, skill) {
3760
+ const directory = join7(skillsRoot, skill.name);
3761
+ await mkdir8(directory, { recursive: true });
3762
+ const manifestPath = join7(directory, MANIFEST);
3763
+ let recorded = {};
3764
+ try {
3765
+ const parsed = JSON.parse(await readFile8(manifestPath, "utf8"));
3766
+ if (parsed.kind === MARKER && parsed.files && typeof parsed.files === "object") recorded = parsed.files;
3767
+ } catch {
3768
+ }
3769
+ const results = {};
3770
+ const next = {};
3771
+ for (const [relative, content] of Object.entries(skill.files)) {
3772
+ const path = join7(directory, relative);
3773
+ const wanted = digest(content);
3774
+ let current;
3775
+ try {
3776
+ current = digest(await readFile8(path, "utf8"));
3777
+ } catch {
3778
+ current = void 0;
3779
+ }
3780
+ if (current === wanted) {
3781
+ results[relative] = "current";
3782
+ next[relative] = wanted;
3783
+ continue;
3784
+ }
3785
+ if (current !== void 0 && current !== recorded[relative]) {
3786
+ results[relative] = "kept-user-edit";
3787
+ continue;
3788
+ }
3789
+ const temporary2 = `${path}.${process.pid}.tmp`;
3790
+ await writeFile8(temporary2, content);
3791
+ await rename8(temporary2, path);
3792
+ results[relative] = "installed";
3793
+ next[relative] = wanted;
3794
+ }
3795
+ const temporary = `${manifestPath}.${process.pid}.tmp`;
3796
+ await writeFile8(temporary, `${JSON.stringify({ kind: MARKER, files: next }, null, 2)}
3797
+ `);
3798
+ await rename8(temporary, manifestPath);
3799
+ return results;
3800
+ }
3801
+
3802
+ // src/host/pixie-hook-skill.ts
3803
+ import { readFileSync } from "node:fs";
3804
+ import { fileURLToPath } from "node:url";
3805
+ function pixieHookSkill() {
3806
+ const name = "studio-pixie-hook-guide";
3807
+ const read = (packaged, source) => {
3808
+ 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)]) {
3809
+ try {
3810
+ return readFileSync(fileURLToPath(candidate), "utf8");
3811
+ } catch {
3812
+ }
3813
+ }
3814
+ throw new Error(`Studio skill file missing: ${packaged}`);
3815
+ };
3816
+ return { name, files: {
3817
+ "SKILL.md": read(`./skills/${name}/SKILL.md`, "../../release/docs/appendix/studio-pixie-hook-guide/SKILL.md"),
3818
+ "guide.md": read(`./skills/${name}/guide.md`, "../../release/docs/appendix/pixie-gate.md")
3819
+ } };
3820
+ }
3821
+
3508
3822
  // src/host/catalog-baseline.ts
3509
3823
  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";
3824
+ import { mkdir as mkdir9, readFile as readFile9, rename as rename9, unlink as unlink2, writeFile as writeFile9 } from "node:fs/promises";
3511
3825
  import { dirname as dirname7, resolve as resolve5 } from "node:path";
3512
3826
  var valid = (value) => {
3513
3827
  if (!value || typeof value !== "object") return false;
@@ -3524,7 +3838,7 @@ var CatalogBaseline = class {
3524
3838
  await this.tail;
3525
3839
  let content;
3526
3840
  try {
3527
- content = await readFile8(this.file, "utf8");
3841
+ content = await readFile9(this.file, "utf8");
3528
3842
  } catch (error) {
3529
3843
  if (error.code === "ENOENT") return void 0;
3530
3844
  throw error;
@@ -3536,11 +3850,11 @@ var CatalogBaseline = class {
3536
3850
  save(snapshot) {
3537
3851
  const content = JSON.stringify(snapshot, null, 2);
3538
3852
  const operation = this.tail.then(async () => {
3539
- await mkdir8(dirname7(this.file), { recursive: true, mode: 448 });
3853
+ await mkdir9(dirname7(this.file), { recursive: true, mode: 448 });
3540
3854
  const temp = `${this.file}.${randomUUID11()}.tmp`;
3541
3855
  try {
3542
- await writeFile8(temp, content, { mode: 384, flag: "wx" });
3543
- await rename8(temp, this.file);
3856
+ await writeFile9(temp, content, { mode: 384, flag: "wx" });
3857
+ await rename9(temp, this.file);
3544
3858
  } finally {
3545
3859
  await unlink2(temp).catch(() => {
3546
3860
  });
@@ -3582,10 +3896,18 @@ var StudioCatalog = class extends (_a2 = TypertRemoteService2, _listPresets_dec
3582
3896
  /** The newest complete collection and what it changes against the confirmed baseline. */
3583
3897
  __publicField(this, "pending");
3584
3898
  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"));
3899
+ this.extensions = new MasterExtensions(join8(dirname8(storePath), "master-extensions.json"));
3900
+ this.baseline = new CatalogBaseline(join8(dirname8(storePath), "catalog-snapshot.json"));
3587
3901
  this.store = new PresetStore(ctx, storePath);
3588
3902
  ctx.plugin(PixieService, dirname8(storePath));
3903
+ const home = ctx.get("profileContext")?.home;
3904
+ if (home) void Promise.resolve().then(() => {
3905
+ const skill = pixieHookSkill();
3906
+ return installSkill(join8(home, "skills"), skill).then((results) => {
3907
+ const kept = Object.entries(results).filter(([, result]) => result === "kept-user-edit").map(([file]) => file);
3908
+ if (kept.length) ctx.logger?.info?.(`studio: kept user-edited ${skill.name}/${kept.join(", ")}`);
3909
+ });
3910
+ }).catch((error) => ctx.logger?.warn?.(`studio: skill install skipped: ${String(error)}`));
3589
3911
  this.snapshot = new Promise((resolve6) => {
3590
3912
  setImmediate(resolve6);
3591
3913
  }).then(() => this.capture());