@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/README.md +3 -2
- package/client.js +1078 -426
- package/index.js +658 -334
- package/judge.js +530 -0
- package/package.json +5 -2
- package/roles.js +1 -0
- package/skills/studio-pixie-hook-guide/SKILL.md +53 -0
- package/skills/studio-pixie-hook-guide/guide.md +287 -0
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.
|
|
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
|
|
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
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
if (
|
|
1758
|
-
} else
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
const
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
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
|
|
2852
|
-
const
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
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
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
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
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
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
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
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
|
-
|
|
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 (
|
|
3038
|
-
const
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
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
|
|
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
|
|
3047
|
-
const error = result.ok ? "Judge
|
|
3048
|
-
record5("judge", input, { verdict: "error", error, reason:
|
|
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
|
|
3052
|
-
record5("judge", input, {
|
|
3053
|
-
|
|
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 [
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3855
|
+
await mkdir9(dirname7(this.file), { recursive: true, mode: 448 });
|
|
3540
3856
|
const temp = `${this.file}.${randomUUID11()}.tmp`;
|
|
3541
3857
|
try {
|
|
3542
|
-
await
|
|
3543
|
-
await
|
|
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(
|
|
3586
|
-
this.baseline = new CatalogBaseline(
|
|
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());
|