deepclause-pi 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/dist/config.d.ts +22 -0
- package/dist/config.js +60 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +101 -5
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +23 -1
- package/docs/SPECKIT.md +10 -0
- package/package.json +2 -2
- package/src/assets/apply.dml +2 -2
- package/src/assets/specs.dml +30 -26
- package/src/config.ts +91 -0
- package/src/index.ts +104 -5
- package/src/runtime.ts +30 -2
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
export type ContextMode = "turn" | "branch" | "isolated";
|
|
2
|
+
export interface JevJudgeConfig {
|
|
3
|
+
/** Whether the Jev (TypeSafe System One) backend may be used. */
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
/** Model alias or pinned version passed to TypeSafe. */
|
|
6
|
+
model: string;
|
|
7
|
+
/** Environment variable holding the TypeSafe API key. */
|
|
8
|
+
apiKeyEnv: string;
|
|
9
|
+
}
|
|
10
|
+
export interface JudgmentConfig {
|
|
11
|
+
/** Default backend name: "llm", "jev", or a name registered by an extension. */
|
|
12
|
+
default: string;
|
|
13
|
+
jev: JevJudgeConfig;
|
|
14
|
+
}
|
|
2
15
|
export interface DeepClauseConfig {
|
|
3
16
|
version: 1;
|
|
4
17
|
contextMode: ContextMode;
|
|
@@ -7,7 +20,16 @@ export interface DeepClauseConfig {
|
|
|
7
20
|
maxTokens: number;
|
|
8
21
|
verbose: boolean;
|
|
9
22
|
modelToolEnabled: boolean;
|
|
23
|
+
judgment: JudgmentConfig;
|
|
10
24
|
}
|
|
11
25
|
export declare const DEFAULT_CONFIG: DeepClauseConfig;
|
|
12
26
|
export declare function loadConfig(path: string): Promise<DeepClauseConfig>;
|
|
13
27
|
export declare function setModelToolEnabled(configPath: string, enabled: boolean): Promise<DeepClauseConfig>;
|
|
28
|
+
export interface JudgeConfigPatch {
|
|
29
|
+
/** Default backend name ("llm" or "jev"). */
|
|
30
|
+
default?: string;
|
|
31
|
+
/** Partial update of the Jev backend settings. */
|
|
32
|
+
jev?: Partial<JevJudgeConfig>;
|
|
33
|
+
}
|
|
34
|
+
/** Merge a judgment patch into the workspace config, preserving every other field. */
|
|
35
|
+
export declare function setJudgeConfig(configPath: string, patch: JudgeConfigPatch): Promise<DeepClauseConfig>;
|
package/dist/config.js
CHANGED
|
@@ -7,6 +7,10 @@ export const DEFAULT_CONFIG = {
|
|
|
7
7
|
maxTokens: 16_384,
|
|
8
8
|
verbose: false,
|
|
9
9
|
modelToolEnabled: false,
|
|
10
|
+
judgment: {
|
|
11
|
+
default: "llm",
|
|
12
|
+
jev: { enabled: false, model: "jev-latest", apiKeyEnv: "TYPESAFE_API_KEY" },
|
|
13
|
+
},
|
|
10
14
|
};
|
|
11
15
|
const isContextMode = (value) => value === "turn" || value === "branch" || value === "isolated";
|
|
12
16
|
export async function loadConfig(path) {
|
|
@@ -40,6 +44,40 @@ export async function loadConfig(path) {
|
|
|
40
44
|
maxTokens: positiveInteger("maxTokens", DEFAULT_CONFIG.maxTokens),
|
|
41
45
|
verbose: config.verbose === true,
|
|
42
46
|
modelToolEnabled: config.modelToolEnabled === true,
|
|
47
|
+
judgment: parseJudgmentConfig(config.judgment),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function parseJudgmentConfig(value) {
|
|
51
|
+
if (value === undefined)
|
|
52
|
+
return DEFAULT_CONFIG.judgment;
|
|
53
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
54
|
+
throw new Error("judgment must be a JSON object");
|
|
55
|
+
}
|
|
56
|
+
const judgment = value;
|
|
57
|
+
const defaultBackend = judgment.default ?? DEFAULT_CONFIG.judgment.default;
|
|
58
|
+
if (typeof defaultBackend !== "string" || !defaultBackend.trim()) {
|
|
59
|
+
throw new Error("judgment.default must be a non-empty backend name");
|
|
60
|
+
}
|
|
61
|
+
const jevValue = judgment.jev ?? {};
|
|
62
|
+
if (!jevValue || typeof jevValue !== "object" || Array.isArray(jevValue)) {
|
|
63
|
+
throw new Error("judgment.jev must be a JSON object");
|
|
64
|
+
}
|
|
65
|
+
const jev = jevValue;
|
|
66
|
+
const model = jev.model ?? DEFAULT_CONFIG.judgment.jev.model;
|
|
67
|
+
const apiKeyEnv = jev.apiKeyEnv ?? DEFAULT_CONFIG.judgment.jev.apiKeyEnv;
|
|
68
|
+
if (typeof model !== "string" || !model.trim()) {
|
|
69
|
+
throw new Error("judgment.jev.model must be a non-empty string");
|
|
70
|
+
}
|
|
71
|
+
if (typeof apiKeyEnv !== "string" || !apiKeyEnv.trim()) {
|
|
72
|
+
throw new Error("judgment.jev.apiKeyEnv must be a non-empty environment variable name");
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
default: defaultBackend.trim(),
|
|
76
|
+
jev: {
|
|
77
|
+
enabled: jev.enabled === true,
|
|
78
|
+
model: model.trim(),
|
|
79
|
+
apiKeyEnv: apiKeyEnv.trim(),
|
|
80
|
+
},
|
|
43
81
|
};
|
|
44
82
|
}
|
|
45
83
|
export async function setModelToolEnabled(configPath, enabled) {
|
|
@@ -57,3 +95,25 @@ export async function setModelToolEnabled(configPath, enabled) {
|
|
|
57
95
|
await writeFile(configPath, `${JSON.stringify({ ...existing, modelToolEnabled: enabled }, null, 2)}\n`, "utf8");
|
|
58
96
|
return loadConfig(configPath);
|
|
59
97
|
}
|
|
98
|
+
/** Merge a judgment patch into the workspace config, preserving every other field. */
|
|
99
|
+
export async function setJudgeConfig(configPath, patch) {
|
|
100
|
+
let existing = {};
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
103
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
104
|
+
existing = parsed;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error.code !== "ENOENT") {
|
|
108
|
+
throw new Error(`Invalid DeepClause config: ${error instanceof Error ? error.message : String(error)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
112
|
+
const nextJudgment = {
|
|
113
|
+
...record(existing.judgment),
|
|
114
|
+
...(patch.default !== undefined ? { default: patch.default } : {}),
|
|
115
|
+
jev: { ...record(record(existing.judgment).jev), ...(patch.jev ?? {}) },
|
|
116
|
+
};
|
|
117
|
+
await writeFile(configPath, `${JSON.stringify({ ...existing, judgment: nextJudgment }, null, 2)}\n`, "utf8");
|
|
118
|
+
return loadConfig(configPath);
|
|
119
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
import { buildInitialMessages } from "./context.js";
|
|
7
|
-
import { loadConfig, setModelToolEnabled } from "./config.js";
|
|
7
|
+
import { loadConfig, setJudgeConfig, setModelToolEnabled } from "./config.js";
|
|
8
8
|
import { renderDml, renderSequence } from "./diagram/extract.js";
|
|
9
9
|
import { polishDiagram, resolveGrade } from "./diagram/grade.js";
|
|
10
10
|
import { findChrome, validateMermaid } from "./diagram/validate.js";
|
|
@@ -68,10 +68,12 @@ export function parseRun(input) {
|
|
|
68
68
|
let contextMode;
|
|
69
69
|
let verbose = false;
|
|
70
70
|
let debug = false;
|
|
71
|
+
let judge;
|
|
71
72
|
const args = [];
|
|
72
73
|
for (let index = 0; index < tokens.length; index++) {
|
|
73
74
|
const token = tokens[index];
|
|
74
75
|
const contextValue = token.startsWith("--context=") ? token.slice("--context=".length) : undefined;
|
|
76
|
+
const judgeValue = token.startsWith("--judge=") ? token.slice("--judge=".length) : undefined;
|
|
75
77
|
if (token === "--verbose" || token === "-v") {
|
|
76
78
|
verbose = true;
|
|
77
79
|
}
|
|
@@ -92,11 +94,22 @@ export function parseRun(input) {
|
|
|
92
94
|
}
|
|
93
95
|
contextMode = value;
|
|
94
96
|
}
|
|
97
|
+
else if (judgeValue !== undefined) {
|
|
98
|
+
if (!judgeValue.trim())
|
|
99
|
+
throw new Error("--judge requires a non-empty backend name");
|
|
100
|
+
judge = judgeValue.trim();
|
|
101
|
+
}
|
|
102
|
+
else if (token === "--judge") {
|
|
103
|
+
const value = tokens[++index];
|
|
104
|
+
if (!value || !value.trim())
|
|
105
|
+
throw new Error("--judge requires a non-empty backend name");
|
|
106
|
+
judge = value.trim();
|
|
107
|
+
}
|
|
95
108
|
else {
|
|
96
109
|
args.push(token);
|
|
97
110
|
}
|
|
98
111
|
}
|
|
99
|
-
return { target, args, contextMode, verbose, debug };
|
|
112
|
+
return { target, args, contextMode, verbose, debug, judge };
|
|
100
113
|
}
|
|
101
114
|
export function parsePlan(input) {
|
|
102
115
|
const tokens = splitArguments(input);
|
|
@@ -242,6 +255,17 @@ async function listDmlFiles(directory, prefix = "") {
|
|
|
242
255
|
function modelLabel(ctx) {
|
|
243
256
|
return ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "none selected";
|
|
244
257
|
}
|
|
258
|
+
function judgeBackendsEnabled(config) {
|
|
259
|
+
return config.judgment.jev.enabled ? ", jev" : "";
|
|
260
|
+
}
|
|
261
|
+
function jevStatus(config) {
|
|
262
|
+
const jev = config.judgment.jev;
|
|
263
|
+
if (!jev.enabled)
|
|
264
|
+
return "disabled";
|
|
265
|
+
return process.env[jev.apiKeyEnv]
|
|
266
|
+
? `enabled (${jev.model})`
|
|
267
|
+
: `enabled but ${jev.apiKeyEnv} is not set`;
|
|
268
|
+
}
|
|
245
269
|
async function bundledViewerTemplate() {
|
|
246
270
|
return readFile(fileURLToPath(new URL("./assets/viewer.template.html", import.meta.url)), "utf8");
|
|
247
271
|
}
|
|
@@ -727,7 +751,7 @@ export default function deepClauseExtension(pi) {
|
|
|
727
751
|
const viewer = displayPath(ctx.cwd, build.viewerPath);
|
|
728
752
|
return {
|
|
729
753
|
content: [{ type: "text", text: `Created spec graph (${view}). Viewer: ${viewer}${opened ? " (opened in your browser)" : ""}` }],
|
|
730
|
-
details: { success: true, view, viewer, opened },
|
|
754
|
+
details: { success: true, view, viewer, viewerPath: build.viewerPath, opened },
|
|
731
755
|
};
|
|
732
756
|
}
|
|
733
757
|
catch (error) {
|
|
@@ -844,6 +868,8 @@ export default function deepClauseExtension(pi) {
|
|
|
844
868
|
`Skills: ${path.relative(ctx.cwd, paths.skills)}`,
|
|
845
869
|
`Plans: ${path.relative(ctx.cwd, paths.plans)}`,
|
|
846
870
|
`Context: ${config.contextMode} (verbose default: ${config.verbose})`,
|
|
871
|
+
`Judgment: ${config.judgment.default} (backends: llm${judgeBackendsEnabled(config)})`,
|
|
872
|
+
`Jev: ${jevStatus(config)}`,
|
|
847
873
|
`Model tool (${DC_RUN_TOOL}): ${config.modelToolEnabled && pi.getActiveTools().includes(DC_RUN_TOOL) ? "enabled" : "disabled"}`,
|
|
848
874
|
`Model tool (${DC_DIAGRAM_TOOL}): ${pi.getActiveTools().includes(DC_DIAGRAM_TOOL) ? "enabled" : "disabled"}`,
|
|
849
875
|
"Ask pi for a presentation-grade or specification-grade diagram of any .dml file;",
|
|
@@ -854,10 +880,13 @@ export default function deepClauseExtension(pi) {
|
|
|
854
880
|
" /dc-check <change|spec> validate specs and deltas deterministically",
|
|
855
881
|
" /dc-archive <change> merge a change delta into specs/ and archive it",
|
|
856
882
|
" /dc-apply <change> [--abort] execute tasks.dml; --abort discards an interrupted apply",
|
|
857
|
-
" /dc-run <skill|path> [args] [--context=turn|branch|isolated]",
|
|
883
|
+
" /dc-run <skill|path> [args] [--context=turn|branch|isolated] [--judge=llm|jev]",
|
|
858
884
|
" /dc-run <skill|path> --verbose show lifecycle events",
|
|
859
885
|
" /dc-run <skill|path> --debug show full event payloads and SDK diagnostics",
|
|
860
886
|
" /dc-tool enable|disable|status control the model-callable dc_run tool",
|
|
887
|
+
" /dc-judge [enable|disable|status] select the judgment backend (llm|jev)",
|
|
888
|
+
" /dc-judge default llm|jev set the default judgment backend",
|
|
889
|
+
" /dc-judge model <name> | key-env <ENV_VAR>",
|
|
861
890
|
" /dc-cancel",
|
|
862
891
|
].join("\n");
|
|
863
892
|
ctx.ui.notify(message, "info");
|
|
@@ -949,6 +978,73 @@ export default function deepClauseExtension(pi) {
|
|
|
949
978
|
}
|
|
950
979
|
},
|
|
951
980
|
});
|
|
981
|
+
pi.registerCommand("dc-judge", {
|
|
982
|
+
description: "Enable, disable, or select the semantic judgment backend (llm|jev)",
|
|
983
|
+
handler: async (rawArgs, ctx) => {
|
|
984
|
+
const tokens = rawArgs.trim().split(/\s+/).filter(Boolean);
|
|
985
|
+
const action = (tokens[0] ?? "status").toLowerCase();
|
|
986
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
987
|
+
const describe = (config) => {
|
|
988
|
+
const jev = config.judgment.jev;
|
|
989
|
+
const keyPresent = Boolean(process.env[jev.apiKeyEnv]);
|
|
990
|
+
return [
|
|
991
|
+
`judgment backend: ${config.judgment.default}`,
|
|
992
|
+
`registered backends: llm${jev.enabled ? ", jev" : ""}`,
|
|
993
|
+
`jev: ${jev.enabled ? "enabled" : "disabled"} | model=${jev.model} | ${jev.apiKeyEnv}=${keyPresent ? "set" : "not set"}`,
|
|
994
|
+
].join("\n");
|
|
995
|
+
};
|
|
996
|
+
try {
|
|
997
|
+
if (action === "status") {
|
|
998
|
+
ctx.ui.notify(describe(await loadConfig(paths.config)), "info");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (action === "enable" || action === "on" || action === "disable" || action === "off") {
|
|
1002
|
+
const enabled = action === "enable" || action === "on";
|
|
1003
|
+
const config = await setJudgeConfig(paths.config, { jev: { enabled } });
|
|
1004
|
+
const jev = config.judgment.jev;
|
|
1005
|
+
const note = enabled && !process.env[jev.apiKeyEnv]
|
|
1006
|
+
? `\n${jev.apiKeyEnv} is not set; export it before using --judge=jev.`
|
|
1007
|
+
: "";
|
|
1008
|
+
ctx.ui.notify(`Jev backend ${enabled ? "enabled" : "disabled"} for this workspace.${note}`, enabled ? "warning" : "info");
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (action === "default") {
|
|
1012
|
+
const name = tokens[1];
|
|
1013
|
+
if (name !== "llm" && name !== "jev") {
|
|
1014
|
+
ctx.ui.notify("Usage: /dc-judge default llm|jev", "warning");
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
await setJudgeConfig(paths.config, { default: name });
|
|
1018
|
+
ctx.ui.notify(`Default judgment backend set to '${name}'`, "info");
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (action === "model") {
|
|
1022
|
+
const model = tokens[1];
|
|
1023
|
+
if (!model) {
|
|
1024
|
+
ctx.ui.notify("Usage: /dc-judge model <name>", "warning");
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
await setJudgeConfig(paths.config, { jev: { model } });
|
|
1028
|
+
ctx.ui.notify(`Jev model set to '${model}'`, "info");
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (action === "key-env") {
|
|
1032
|
+
const envName = tokens[1];
|
|
1033
|
+
if (!envName) {
|
|
1034
|
+
ctx.ui.notify("Usage: /dc-judge key-env <ENV_VAR>", "warning");
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
await setJudgeConfig(paths.config, { jev: { apiKeyEnv: envName } });
|
|
1038
|
+
ctx.ui.notify(`Jev API key environment variable set to '${envName}'`, "info");
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
ctx.ui.notify("Usage: /dc-judge [enable|disable|status|default llm|jev|model <name>|key-env <ENV_VAR>]", "warning");
|
|
1042
|
+
}
|
|
1043
|
+
catch (error) {
|
|
1044
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
1045
|
+
}
|
|
1046
|
+
},
|
|
1047
|
+
});
|
|
952
1048
|
pi.registerCommand("dc-list", {
|
|
953
1049
|
description: "List DeepClause DML skills and generated plans",
|
|
954
1050
|
handler: async (_args, ctx) => {
|
|
@@ -1209,7 +1305,7 @@ export default function deepClauseExtension(pi) {
|
|
|
1209
1305
|
renderExecution();
|
|
1210
1306
|
};
|
|
1211
1307
|
try {
|
|
1212
|
-
const result = await executeDml(filePath, parsed.args, initialMessages, { ...config, verbose: config.verbose || parsed.debug }, pi, ctx, controller, {
|
|
1308
|
+
const result = await executeDml(filePath, parsed.args, initialMessages, { ...config, verbose: config.verbose || parsed.debug, judgeBackend: parsed.judge }, pi, ctx, controller, {
|
|
1213
1309
|
onEvent,
|
|
1214
1310
|
onDiagnostic,
|
|
1215
1311
|
onInput: async (prompt, signal) => {
|
package/dist/runtime.d.ts
CHANGED
|
@@ -41,4 +41,6 @@ export interface ExecutionResult {
|
|
|
41
41
|
errors: string[];
|
|
42
42
|
usage: LLMUsage;
|
|
43
43
|
}
|
|
44
|
-
export declare function executeDml(filePath: string, args: string[], initialMessages: MemoryMessage[], config: DeepClauseConfig
|
|
44
|
+
export declare function executeDml(filePath: string, args: string[], initialMessages: MemoryMessage[], config: DeepClauseConfig & {
|
|
45
|
+
judgeBackend?: string;
|
|
46
|
+
}, pi: ExtensionAPI, ctx: ExtensionContext, controller: AbortController, callbacks: ExecutionCallbacks, runPiAgentStep?: (request: PiAgentStepRequest, signal: AbortSignal) => Promise<PiAgentStepResult>, verifyCommands?: string[], applyChangeJsonPath?: string): Promise<ExecutionResult>;
|
package/dist/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { realpath, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { createDeepClause } from "deepclause-sdk";
|
|
3
|
+
import { createDeepClause, createJevJudgeBackend, createLLMJudgeBackend } from "deepclause-sdk";
|
|
4
4
|
import { PI_AGENT_STEP_TOOL } from "./planner.js";
|
|
5
5
|
export const PI_WORKSPACE_LIST_TOOL = "pi_workspace_list";
|
|
6
6
|
export const PI_BASH_TOOL = "pi_bash";
|
|
@@ -265,12 +265,33 @@ export async function executeDml(filePath, args, initialMessages, config, pi, ct
|
|
|
265
265
|
if (!model)
|
|
266
266
|
throw new Error("Select a pi model before running DeepClause");
|
|
267
267
|
const backend = createPiBackend(ctx, config.maxTokens, callbacks.onDiagnostic ?? (() => { }));
|
|
268
|
+
// Judgment backends. `llm` reuses pi's active model and credentials; `jev`
|
|
269
|
+
// is opt-in and only registered when enabled and its key is present.
|
|
270
|
+
const judgeBackends = {
|
|
271
|
+
llm: createLLMJudgeBackend({ llmBackend: backend, model: model.id }),
|
|
272
|
+
};
|
|
273
|
+
const jev = config.judgment.jev;
|
|
274
|
+
if (jev.enabled) {
|
|
275
|
+
const apiKey = process.env[jev.apiKeyEnv];
|
|
276
|
+
if (apiKey) {
|
|
277
|
+
judgeBackends.jev = createJevJudgeBackend({ apiKey, model: jev.model });
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
callbacks.onDiagnostic?.(`judgment backend 'jev' is enabled but ${jev.apiKeyEnv} is not set; falling back to 'llm'`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const requestedJudge = config.judgeBackend ?? config.judgment.default;
|
|
284
|
+
if (!judgeBackends[requestedJudge]) {
|
|
285
|
+
throw new Error(`Judgment backend '${requestedJudge}' is not available. Enable it in .pi/deepclause/config.json or use --judge=llm.`);
|
|
286
|
+
}
|
|
268
287
|
const sdk = await createDeepClause({
|
|
269
288
|
model: model.id,
|
|
270
289
|
maxTokens: config.maxTokens,
|
|
271
290
|
streaming: true,
|
|
272
291
|
debug: config.verbose,
|
|
273
292
|
llmBackend: backend,
|
|
293
|
+
judgeBackends,
|
|
294
|
+
defaultJudge: requestedJudge,
|
|
274
295
|
});
|
|
275
296
|
registerPiRuntimeTools(sdk, pi, ctx.cwd, controller.signal, async (command, signal) => {
|
|
276
297
|
if (!ctx.hasUI)
|
|
@@ -371,6 +392,7 @@ export async function executeDml(filePath, args, initialMessages, config, pi, ct
|
|
|
371
392
|
gasLimit: config.gasLimit,
|
|
372
393
|
signal: controller.signal,
|
|
373
394
|
initialMessages,
|
|
395
|
+
judgeBackend: config.judgeBackend,
|
|
374
396
|
onUserInput: (prompt) => callbacks.onInput(prompt, controller.signal),
|
|
375
397
|
})) {
|
|
376
398
|
callbacks.onEvent(event);
|
package/docs/SPECKIT.md
CHANGED
|
@@ -16,6 +16,16 @@ Spec-driven changes for pi, without leaving the session.
|
|
|
16
16
|
/dc-archive <slug> merge into specs/
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## Why DML and DeepClause for spec-driven development
|
|
20
|
+
|
|
21
|
+
Most spec tools are a Markdown convention plus a program that parses it. The convention is the good idea; the program is where it gets fragile, because parsing, validating and merging structured text is usually written as regexes and imperative branches. OpenSpec, for example, needs a few thousand lines of TypeScript for what is essentially parsing requirements, checking coverage, and reconciling deltas — and its own docs warn about silent failures such as a scenario written with three hashes instead of four.
|
|
22
|
+
|
|
23
|
+
Spec work is logic. A requirement is a term; *every requirement has at least one scenario* is a rule; merging a delta is matching and rewriting; *which scenarios are uncovered?* and *do two in-flight changes touch the same requirement?* are queries. DML is a Prolog dialect, so these are expressed directly instead of emulated. Structure is parsed once into terms, so validation is a decision procedure with line numbers, merging preserves untouched blocks, and coverage and conflict checks are single queries — deterministic, zero tokens, reproducible in CI.
|
|
24
|
+
|
|
25
|
+
That determinism is the point. The value of agreeing on a spec is that the agreement is *checkable*; if the check is another model call, it is just another opinion. DML lets the model do what it is good at — drafting requirements, designing, implementing — and keeps correctness in logic. The same runtime supplies what raw Prolog lacks: `task/N` and `prompt/N` for bounded model calls with typed results, `exec/2` for tools, streaming events, cancellation, and usage accounting. Backtracking across model calls is what makes verify → repair → retry loops natural, and CLP(FD)/(Q)/(R) cover hard constraints instead of asking the model to do arithmetic.
|
|
26
|
+
|
|
27
|
+
DeepClause is what makes that practical inside pi: DML programs run with pi's active model, credentials, session context and approvals, so the spec engine, the task plan (`tasks.dml`), and the execution loop are one system in the session you already work in, rather than a separate CLI. You keep human-readable Markdown specs, but the thing enforcing them is a proof, not a prompt.
|
|
28
|
+
|
|
19
29
|
## Requirements
|
|
20
30
|
|
|
21
31
|
- pi with this extension installed (see the [README](../README.md#install)).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepclause-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Pi-hosted runtime for DeepClause DML programs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"prepublishOnly": "npm run check"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"deepclause-sdk": "npm:deepclause-sdk@0.0.
|
|
46
|
+
"deepclause-sdk": "npm:deepclause-sdk@0.0.89",
|
|
47
47
|
"typebox": "1.3.7"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
package/src/assets/apply.dml
CHANGED
|
@@ -85,7 +85,7 @@ attempt(TasksPath, Statuses0, Change, Id, Props, N, Max, Feedback, Statuses) :-
|
|
|
85
85
|
format(string(Progress), "task ~w attempt ~w/~w", [Id, N, Max]),
|
|
86
86
|
output(Progress),
|
|
87
87
|
execute(Change, Id, Props, Feedback, Summary),
|
|
88
|
-
|
|
88
|
+
verify_task(Props, Summary, Verdict),
|
|
89
89
|
( Verdict = ok
|
|
90
90
|
-> sp_set_status(Statuses0, Id, done(N), Statuses),
|
|
91
91
|
sp_write_statuses(TasksPath, Statuses),
|
|
@@ -123,7 +123,7 @@ build_instruction(Id, Do, Expected, Feedback, Instruction) :-
|
|
|
123
123
|
"Task ~w. ~w~nExpected result: ~w~n~nThe previous attempt failed verification: ~w~nFix only what is needed; do not redo the whole task.",
|
|
124
124
|
[Id, Do, Expected, Feedback]).
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
verify_task(Props, Summary, Verdict) :-
|
|
127
127
|
catch(get_dict(checks, Props, Checks), _, Checks = []),
|
|
128
128
|
findall(Message, (member(Check, Checks), run_check(Check, Message), Message \= ok), Failures),
|
|
129
129
|
( Summary == ""
|
package/src/assets/specs.dml
CHANGED
|
@@ -64,9 +64,12 @@ sp_sections(Lines, Sections) :-
|
|
|
64
64
|
sp_skip_plain(Lines, Rest),
|
|
65
65
|
sp_sections_from(Rest, Sections).
|
|
66
66
|
|
|
67
|
-
sp_skip_plain([line(Line, Number)|Rest], [line(Line, Number)|Rest]) :- sp_heading(Line, Level, _), Level >= 1, Level =< 3, !.
|
|
68
|
-
sp_skip_plain([_|Rest], Out) :- sp_skip_plain(Rest, Out).
|
|
69
67
|
sp_skip_plain([], []).
|
|
68
|
+
sp_skip_plain([Item|Rest], Out) :-
|
|
69
|
+
( Item = line(Line, _), sp_heading(Line, Level, _), Level >= 1, Level =< 3
|
|
70
|
+
-> Out = [Item|Rest]
|
|
71
|
+
; sp_skip_plain(Rest, Out)
|
|
72
|
+
).
|
|
70
73
|
|
|
71
74
|
sp_sections_from([], []).
|
|
72
75
|
sp_sections_from([line(Line, N)|Rest], [section(Level, Text, N, Body)|More]) :-
|
|
@@ -77,12 +80,11 @@ sp_sections_from([line(Line, N)|Rest], [section(Level, Text, N, Body)|More]) :-
|
|
|
77
80
|
sp_sections_from(Tail, More).
|
|
78
81
|
|
|
79
82
|
sp_take_body([], [], []).
|
|
80
|
-
sp_take_body([
|
|
81
|
-
sp_heading(Line, Level, _),
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
sp_take_body([Item|Rest], [Item|Body], Tail) :- sp_take_body(Rest, Body, Tail).
|
|
83
|
+
sp_take_body([Item|Rest], Body, Tail) :-
|
|
84
|
+
( Item = line(Line, _), sp_heading(Line, Level, _), Level >= 1, Level =< 3
|
|
85
|
+
-> Body = [], Tail = [Item|Rest]
|
|
86
|
+
; Body = [Item|Body1], sp_take_body(Rest, Body1, Tail)
|
|
87
|
+
).
|
|
86
88
|
|
|
87
89
|
%% Split a requirement body (level-4 headings inline) into description + scenarios.
|
|
88
90
|
sp_split_scenarios(Body, DescLines, ScenarioSections) :-
|
|
@@ -90,11 +92,11 @@ sp_split_scenarios(Body, DescLines, ScenarioSections) :-
|
|
|
90
92
|
sp_scenario_sections(Rest, ScenarioSections).
|
|
91
93
|
|
|
92
94
|
sp_take_plain([], [], []).
|
|
93
|
-
sp_take_plain([
|
|
94
|
-
sp_heading(Line, Level, _),
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
sp_take_plain([Item|Rest], Body, Tail) :-
|
|
96
|
+
( Item = line(Line, _), sp_heading(Line, Level, _), Level >= 4
|
|
97
|
+
-> Body = [], Tail = [Item|Rest]
|
|
98
|
+
; Body = [Item|Body1], sp_take_plain(Rest, Body1, Tail)
|
|
99
|
+
).
|
|
98
100
|
|
|
99
101
|
sp_scenario_sections([], []).
|
|
100
102
|
sp_scenario_sections([line(Line, N)|Rest], [section(4, Text, N, Body)|More]) :-
|
|
@@ -103,11 +105,11 @@ sp_scenario_sections([line(Line, N)|Rest], [section(4, Text, N, Body)|More]) :-
|
|
|
103
105
|
sp_scenario_sections(Tail, More).
|
|
104
106
|
|
|
105
107
|
sp_take_until_level4([], [], []).
|
|
106
|
-
sp_take_until_level4([
|
|
107
|
-
sp_heading(Line, Level, _),
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
sp_take_until_level4([Item|Rest], Body, Tail) :-
|
|
109
|
+
( Item = line(Line, _), sp_heading(Line, Level, _), Level >= 4
|
|
110
|
+
-> Body = [], Tail = [Item|Rest]
|
|
111
|
+
; Body = [Item|Body1], sp_take_until_level4(Rest, Body1, Tail)
|
|
112
|
+
).
|
|
111
113
|
|
|
112
114
|
%% ---------------------------------------------------------------------------
|
|
113
115
|
%% Term extraction
|
|
@@ -164,10 +166,11 @@ sp_parse_delta(Text, Deltas) :-
|
|
|
164
166
|
sp_collect_ops(Rest, Deltas).
|
|
165
167
|
|
|
166
168
|
sp_drop_to_op([], []).
|
|
167
|
-
sp_drop_to_op([
|
|
168
|
-
sp_delta_header(Header, _)
|
|
169
|
-
|
|
170
|
-
sp_drop_to_op(
|
|
169
|
+
sp_drop_to_op([Item|Rest], Out) :-
|
|
170
|
+
( Item = section(2, Header, _, _), sp_delta_header(Header, _)
|
|
171
|
+
-> Out = [Item|Rest]
|
|
172
|
+
; sp_drop_to_op(Rest, Out)
|
|
173
|
+
).
|
|
171
174
|
|
|
172
175
|
sp_collect_ops([], []).
|
|
173
176
|
sp_collect_ops([section(2, Header, _, _)|Rest], Deltas) :-
|
|
@@ -184,10 +187,11 @@ sp_collect_ops([section(2, Header, _, _)|Rest], Deltas) :-
|
|
|
184
187
|
).
|
|
185
188
|
|
|
186
189
|
sp_take_until_op([], [], []).
|
|
187
|
-
sp_take_until_op([
|
|
188
|
-
sp_delta_header(Header, _)
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
sp_take_until_op([Item|Rest], Group, Tail) :-
|
|
191
|
+
( Item = section(2, Header, _, _), sp_delta_header(Header, _)
|
|
192
|
+
-> Group = [], Tail = [Item|Rest]
|
|
193
|
+
; Group = [Item|Group1], sp_take_until_op(Rest, Group1, Tail)
|
|
194
|
+
).
|
|
191
195
|
|
|
192
196
|
%% ---------------------------------------------------------------------------
|
|
193
197
|
%% Validation
|
package/src/config.ts
CHANGED
|
@@ -2,6 +2,21 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
|
|
3
3
|
export type ContextMode = "turn" | "branch" | "isolated";
|
|
4
4
|
|
|
5
|
+
export interface JevJudgeConfig {
|
|
6
|
+
/** Whether the Jev (TypeSafe System One) backend may be used. */
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
/** Model alias or pinned version passed to TypeSafe. */
|
|
9
|
+
model: string;
|
|
10
|
+
/** Environment variable holding the TypeSafe API key. */
|
|
11
|
+
apiKeyEnv: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface JudgmentConfig {
|
|
15
|
+
/** Default backend name: "llm", "jev", or a name registered by an extension. */
|
|
16
|
+
default: string;
|
|
17
|
+
jev: JevJudgeConfig;
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
export interface DeepClauseConfig {
|
|
6
21
|
version: 1;
|
|
7
22
|
contextMode: ContextMode;
|
|
@@ -10,6 +25,7 @@ export interface DeepClauseConfig {
|
|
|
10
25
|
maxTokens: number;
|
|
11
26
|
verbose: boolean;
|
|
12
27
|
modelToolEnabled: boolean;
|
|
28
|
+
judgment: JudgmentConfig;
|
|
13
29
|
}
|
|
14
30
|
|
|
15
31
|
export const DEFAULT_CONFIG: DeepClauseConfig = {
|
|
@@ -20,6 +36,10 @@ export const DEFAULT_CONFIG: DeepClauseConfig = {
|
|
|
20
36
|
maxTokens: 16_384,
|
|
21
37
|
verbose: false,
|
|
22
38
|
modelToolEnabled: false,
|
|
39
|
+
judgment: {
|
|
40
|
+
default: "llm",
|
|
41
|
+
jev: { enabled: false, model: "jev-latest", apiKeyEnv: "TYPESAFE_API_KEY" },
|
|
42
|
+
},
|
|
23
43
|
};
|
|
24
44
|
|
|
25
45
|
const isContextMode = (value: unknown): value is ContextMode =>
|
|
@@ -55,6 +75,42 @@ export async function loadConfig(path: string): Promise<DeepClauseConfig> {
|
|
|
55
75
|
maxTokens: positiveInteger("maxTokens", DEFAULT_CONFIG.maxTokens),
|
|
56
76
|
verbose: config.verbose === true,
|
|
57
77
|
modelToolEnabled: config.modelToolEnabled === true,
|
|
78
|
+
judgment: parseJudgmentConfig(config.judgment),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseJudgmentConfig(value: unknown): JudgmentConfig {
|
|
83
|
+
if (value === undefined) return DEFAULT_CONFIG.judgment;
|
|
84
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
85
|
+
throw new Error("judgment must be a JSON object");
|
|
86
|
+
}
|
|
87
|
+
const judgment = value as Record<string, unknown>;
|
|
88
|
+
const defaultBackend = judgment.default ?? DEFAULT_CONFIG.judgment.default;
|
|
89
|
+
if (typeof defaultBackend !== "string" || !defaultBackend.trim()) {
|
|
90
|
+
throw new Error("judgment.default must be a non-empty backend name");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const jevValue = judgment.jev ?? {};
|
|
94
|
+
if (!jevValue || typeof jevValue !== "object" || Array.isArray(jevValue)) {
|
|
95
|
+
throw new Error("judgment.jev must be a JSON object");
|
|
96
|
+
}
|
|
97
|
+
const jev = jevValue as Record<string, unknown>;
|
|
98
|
+
const model = jev.model ?? DEFAULT_CONFIG.judgment.jev.model;
|
|
99
|
+
const apiKeyEnv = jev.apiKeyEnv ?? DEFAULT_CONFIG.judgment.jev.apiKeyEnv;
|
|
100
|
+
if (typeof model !== "string" || !model.trim()) {
|
|
101
|
+
throw new Error("judgment.jev.model must be a non-empty string");
|
|
102
|
+
}
|
|
103
|
+
if (typeof apiKeyEnv !== "string" || !apiKeyEnv.trim()) {
|
|
104
|
+
throw new Error("judgment.jev.apiKeyEnv must be a non-empty environment variable name");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
default: defaultBackend.trim(),
|
|
109
|
+
jev: {
|
|
110
|
+
enabled: jev.enabled === true,
|
|
111
|
+
model: model.trim(),
|
|
112
|
+
apiKeyEnv: apiKeyEnv.trim(),
|
|
113
|
+
},
|
|
58
114
|
};
|
|
59
115
|
}
|
|
60
116
|
|
|
@@ -72,3 +128,38 @@ export async function setModelToolEnabled(configPath: string, enabled: boolean):
|
|
|
72
128
|
await writeFile(configPath, `${JSON.stringify({ ...existing, modelToolEnabled: enabled }, null, 2)}\n`, "utf8");
|
|
73
129
|
return loadConfig(configPath);
|
|
74
130
|
}
|
|
131
|
+
|
|
132
|
+
export interface JudgeConfigPatch {
|
|
133
|
+
/** Default backend name ("llm" or "jev"). */
|
|
134
|
+
default?: string;
|
|
135
|
+
/** Partial update of the Jev backend settings. */
|
|
136
|
+
jev?: Partial<JevJudgeConfig>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Merge a judgment patch into the workspace config, preserving every other field. */
|
|
140
|
+
export async function setJudgeConfig(
|
|
141
|
+
configPath: string,
|
|
142
|
+
patch: JudgeConfigPatch,
|
|
143
|
+
): Promise<DeepClauseConfig> {
|
|
144
|
+
let existing: Record<string, unknown> = {};
|
|
145
|
+
try {
|
|
146
|
+
const parsed: unknown = JSON.parse(await readFile(configPath, "utf8"));
|
|
147
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed as Record<string, unknown>;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
150
|
+
throw new Error(`Invalid DeepClause config: ${error instanceof Error ? error.message : String(error)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const record = (value: unknown): Record<string, unknown> =>
|
|
155
|
+
value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
156
|
+
|
|
157
|
+
const nextJudgment = {
|
|
158
|
+
...record(existing.judgment),
|
|
159
|
+
...(patch.default !== undefined ? { default: patch.default } : {}),
|
|
160
|
+
jev: { ...record(record(existing.judgment).jev), ...(patch.jev ?? {}) },
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
await writeFile(configPath, `${JSON.stringify({ ...existing, judgment: nextJudgment }, null, 2)}\n`, "utf8");
|
|
164
|
+
return loadConfig(configPath);
|
|
165
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { StringEnum } from "@earendil-works/pi-ai";
|
|
|
6
6
|
import type { DMLEvent } from "deepclause-sdk";
|
|
7
7
|
import { Type } from "typebox";
|
|
8
8
|
import { buildInitialMessages } from "./context.js";
|
|
9
|
-
import { loadConfig, setModelToolEnabled, type ContextMode } from "./config.js";
|
|
9
|
+
import { loadConfig, setJudgeConfig, setModelToolEnabled, type ContextMode, type DeepClauseConfig } from "./config.js";
|
|
10
10
|
import { renderDml, renderSequence } from "./diagram/extract.js";
|
|
11
11
|
import { polishDiagram, resolveGrade, type DiagramGrade } from "./diagram/grade.js";
|
|
12
12
|
import { findChrome, validateMermaid, type MermaidView } from "./diagram/validate.js";
|
|
@@ -57,6 +57,7 @@ export interface ParsedRun {
|
|
|
57
57
|
contextMode?: ContextMode;
|
|
58
58
|
verbose: boolean;
|
|
59
59
|
debug: boolean;
|
|
60
|
+
judge?: string;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
interface ParsedPlan {
|
|
@@ -125,11 +126,13 @@ export function parseRun(input: string): ParsedRun {
|
|
|
125
126
|
let contextMode: ContextMode | undefined;
|
|
126
127
|
let verbose = false;
|
|
127
128
|
let debug = false;
|
|
129
|
+
let judge: string | undefined;
|
|
128
130
|
const args: string[] = [];
|
|
129
131
|
|
|
130
132
|
for (let index = 0; index < tokens.length; index++) {
|
|
131
133
|
const token = tokens[index]!;
|
|
132
134
|
const contextValue = token.startsWith("--context=") ? token.slice("--context=".length) : undefined;
|
|
135
|
+
const judgeValue = token.startsWith("--judge=") ? token.slice("--judge=".length) : undefined;
|
|
133
136
|
if (token === "--verbose" || token === "-v") {
|
|
134
137
|
verbose = true;
|
|
135
138
|
} else if (token === "--debug" || token === "-d") {
|
|
@@ -146,11 +149,18 @@ export function parseRun(input: string): ParsedRun {
|
|
|
146
149
|
throw new Error("--context must be turn, branch, or isolated");
|
|
147
150
|
}
|
|
148
151
|
contextMode = value;
|
|
152
|
+
} else if (judgeValue !== undefined) {
|
|
153
|
+
if (!judgeValue.trim()) throw new Error("--judge requires a non-empty backend name");
|
|
154
|
+
judge = judgeValue.trim();
|
|
155
|
+
} else if (token === "--judge") {
|
|
156
|
+
const value = tokens[++index];
|
|
157
|
+
if (!value || !value.trim()) throw new Error("--judge requires a non-empty backend name");
|
|
158
|
+
judge = value.trim();
|
|
149
159
|
} else {
|
|
150
160
|
args.push(token);
|
|
151
161
|
}
|
|
152
162
|
}
|
|
153
|
-
return { target, args, contextMode, verbose, debug };
|
|
163
|
+
return { target, args, contextMode, verbose, debug, judge };
|
|
154
164
|
}
|
|
155
165
|
|
|
156
166
|
export function parsePlan(input: string): ParsedPlan {
|
|
@@ -288,6 +298,18 @@ function modelLabel(ctx: ExtensionCommandContext): string {
|
|
|
288
298
|
return ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "none selected";
|
|
289
299
|
}
|
|
290
300
|
|
|
301
|
+
function judgeBackendsEnabled(config: DeepClauseConfig): string {
|
|
302
|
+
return config.judgment.jev.enabled ? ", jev" : "";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function jevStatus(config: DeepClauseConfig): string {
|
|
306
|
+
const jev = config.judgment.jev;
|
|
307
|
+
if (!jev.enabled) return "disabled";
|
|
308
|
+
return process.env[jev.apiKeyEnv]
|
|
309
|
+
? `enabled (${jev.model})`
|
|
310
|
+
: `enabled but ${jev.apiKeyEnv} is not set`;
|
|
311
|
+
}
|
|
312
|
+
|
|
291
313
|
async function bundledViewerTemplate(): Promise<string> {
|
|
292
314
|
return readFile(fileURLToPath(new URL("./assets/viewer.template.html", import.meta.url)), "utf8");
|
|
293
315
|
}
|
|
@@ -789,7 +811,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
789
811
|
const viewer = displayPath(ctx.cwd, build.viewerPath);
|
|
790
812
|
return {
|
|
791
813
|
content: [{ type: "text", text: `Created spec graph (${view}). Viewer: ${viewer}${opened ? " (opened in your browser)" : ""}` }],
|
|
792
|
-
details: { success: true, view, viewer, opened },
|
|
814
|
+
details: { success: true, view, viewer, viewerPath: build.viewerPath, opened },
|
|
793
815
|
};
|
|
794
816
|
} catch (error) {
|
|
795
817
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -926,6 +948,8 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
926
948
|
`Skills: ${path.relative(ctx.cwd, paths.skills)}`,
|
|
927
949
|
`Plans: ${path.relative(ctx.cwd, paths.plans)}`,
|
|
928
950
|
`Context: ${config.contextMode} (verbose default: ${config.verbose})`,
|
|
951
|
+
`Judgment: ${config.judgment.default} (backends: llm${judgeBackendsEnabled(config)})`,
|
|
952
|
+
`Jev: ${jevStatus(config)}`,
|
|
929
953
|
`Model tool (${DC_RUN_TOOL}): ${config.modelToolEnabled && pi.getActiveTools().includes(DC_RUN_TOOL) ? "enabled" : "disabled"}`,
|
|
930
954
|
`Model tool (${DC_DIAGRAM_TOOL}): ${pi.getActiveTools().includes(DC_DIAGRAM_TOOL) ? "enabled" : "disabled"}`,
|
|
931
955
|
"Ask pi for a presentation-grade or specification-grade diagram of any .dml file;",
|
|
@@ -936,10 +960,13 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
936
960
|
" /dc-check <change|spec> validate specs and deltas deterministically",
|
|
937
961
|
" /dc-archive <change> merge a change delta into specs/ and archive it",
|
|
938
962
|
" /dc-apply <change> [--abort] execute tasks.dml; --abort discards an interrupted apply",
|
|
939
|
-
" /dc-run <skill|path> [args] [--context=turn|branch|isolated]",
|
|
963
|
+
" /dc-run <skill|path> [args] [--context=turn|branch|isolated] [--judge=llm|jev]",
|
|
940
964
|
" /dc-run <skill|path> --verbose show lifecycle events",
|
|
941
965
|
" /dc-run <skill|path> --debug show full event payloads and SDK diagnostics",
|
|
942
966
|
" /dc-tool enable|disable|status control the model-callable dc_run tool",
|
|
967
|
+
" /dc-judge [enable|disable|status] select the judgment backend (llm|jev)",
|
|
968
|
+
" /dc-judge default llm|jev set the default judgment backend",
|
|
969
|
+
" /dc-judge model <name> | key-env <ENV_VAR>",
|
|
943
970
|
" /dc-cancel",
|
|
944
971
|
].join("\n");
|
|
945
972
|
ctx.ui.notify(message, "info");
|
|
@@ -1032,6 +1059,78 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
1032
1059
|
},
|
|
1033
1060
|
});
|
|
1034
1061
|
|
|
1062
|
+
pi.registerCommand("dc-judge", {
|
|
1063
|
+
description: "Enable, disable, or select the semantic judgment backend (llm|jev)",
|
|
1064
|
+
handler: async (rawArgs, ctx) => {
|
|
1065
|
+
const tokens = rawArgs.trim().split(/\s+/).filter(Boolean);
|
|
1066
|
+
const action = (tokens[0] ?? "status").toLowerCase();
|
|
1067
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
1068
|
+
|
|
1069
|
+
const describe = (config: DeepClauseConfig): string => {
|
|
1070
|
+
const jev = config.judgment.jev;
|
|
1071
|
+
const keyPresent = Boolean(process.env[jev.apiKeyEnv]);
|
|
1072
|
+
return [
|
|
1073
|
+
`judgment backend: ${config.judgment.default}`,
|
|
1074
|
+
`registered backends: llm${jev.enabled ? ", jev" : ""}`,
|
|
1075
|
+
`jev: ${jev.enabled ? "enabled" : "disabled"} | model=${jev.model} | ${jev.apiKeyEnv}=${keyPresent ? "set" : "not set"}`,
|
|
1076
|
+
].join("\n");
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
try {
|
|
1080
|
+
if (action === "status") {
|
|
1081
|
+
ctx.ui.notify(describe(await loadConfig(paths.config)), "info");
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (action === "enable" || action === "on" || action === "disable" || action === "off") {
|
|
1085
|
+
const enabled = action === "enable" || action === "on";
|
|
1086
|
+
const config = await setJudgeConfig(paths.config, { jev: { enabled } });
|
|
1087
|
+
const jev = config.judgment.jev;
|
|
1088
|
+
const note = enabled && !process.env[jev.apiKeyEnv]
|
|
1089
|
+
? `\n${jev.apiKeyEnv} is not set; export it before using --judge=jev.`
|
|
1090
|
+
: "";
|
|
1091
|
+
ctx.ui.notify(`Jev backend ${enabled ? "enabled" : "disabled"} for this workspace.${note}`, enabled ? "warning" : "info");
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (action === "default") {
|
|
1095
|
+
const name = tokens[1];
|
|
1096
|
+
if (name !== "llm" && name !== "jev") {
|
|
1097
|
+
ctx.ui.notify("Usage: /dc-judge default llm|jev", "warning");
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
await setJudgeConfig(paths.config, { default: name });
|
|
1101
|
+
ctx.ui.notify(`Default judgment backend set to '${name}'`, "info");
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (action === "model") {
|
|
1105
|
+
const model = tokens[1];
|
|
1106
|
+
if (!model) {
|
|
1107
|
+
ctx.ui.notify("Usage: /dc-judge model <name>", "warning");
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
await setJudgeConfig(paths.config, { jev: { model } });
|
|
1111
|
+
ctx.ui.notify(`Jev model set to '${model}'`, "info");
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (action === "key-env") {
|
|
1115
|
+
const envName = tokens[1];
|
|
1116
|
+
if (!envName) {
|
|
1117
|
+
ctx.ui.notify("Usage: /dc-judge key-env <ENV_VAR>", "warning");
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
await setJudgeConfig(paths.config, { jev: { apiKeyEnv: envName } });
|
|
1121
|
+
ctx.ui.notify(`Jev API key environment variable set to '${envName}'`, "info");
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
ctx.ui.notify(
|
|
1125
|
+
"Usage: /dc-judge [enable|disable|status|default llm|jev|model <name>|key-env <ENV_VAR>]",
|
|
1126
|
+
"warning",
|
|
1127
|
+
);
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1035
1134
|
pi.registerCommand("dc-list", {
|
|
1036
1135
|
description: "List DeepClause DML skills and generated plans",
|
|
1037
1136
|
handler: async (_args, ctx) => {
|
|
@@ -1302,7 +1401,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
1302
1401
|
filePath,
|
|
1303
1402
|
parsed.args,
|
|
1304
1403
|
initialMessages,
|
|
1305
|
-
{ ...config, verbose: config.verbose || parsed.debug },
|
|
1404
|
+
{ ...config, verbose: config.verbose || parsed.debug, judgeBackend: parsed.judge },
|
|
1306
1405
|
pi,
|
|
1307
1406
|
ctx,
|
|
1308
1407
|
controller,
|
package/src/runtime.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { realpath, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { createDeepClause } from "deepclause-sdk";
|
|
3
|
+
import { createDeepClause, createJevJudgeBackend, createLLMJudgeBackend } from "deepclause-sdk";
|
|
4
4
|
import type {
|
|
5
5
|
DMLEvent,
|
|
6
|
+
JudgeBackend,
|
|
6
7
|
LLMBackend,
|
|
7
8
|
LLMBackendMessage,
|
|
8
9
|
LLMUsage,
|
|
@@ -327,7 +328,7 @@ export async function executeDml(
|
|
|
327
328
|
filePath: string,
|
|
328
329
|
args: string[],
|
|
329
330
|
initialMessages: MemoryMessage[],
|
|
330
|
-
config: DeepClauseConfig,
|
|
331
|
+
config: DeepClauseConfig & { judgeBackend?: string },
|
|
331
332
|
pi: ExtensionAPI,
|
|
332
333
|
ctx: ExtensionContext,
|
|
333
334
|
controller: AbortController,
|
|
@@ -339,12 +340,38 @@ export async function executeDml(
|
|
|
339
340
|
const model = ctx.model;
|
|
340
341
|
if (!model) throw new Error("Select a pi model before running DeepClause");
|
|
341
342
|
const backend = createPiBackend(ctx, config.maxTokens, callbacks.onDiagnostic ?? (() => {}));
|
|
343
|
+
|
|
344
|
+
// Judgment backends. `llm` reuses pi's active model and credentials; `jev`
|
|
345
|
+
// is opt-in and only registered when enabled and its key is present.
|
|
346
|
+
const judgeBackends: Record<string, JudgeBackend> = {
|
|
347
|
+
llm: createLLMJudgeBackend({ llmBackend: backend, model: model.id }),
|
|
348
|
+
};
|
|
349
|
+
const jev = config.judgment.jev;
|
|
350
|
+
if (jev.enabled) {
|
|
351
|
+
const apiKey = process.env[jev.apiKeyEnv];
|
|
352
|
+
if (apiKey) {
|
|
353
|
+
judgeBackends.jev = createJevJudgeBackend({ apiKey, model: jev.model });
|
|
354
|
+
} else {
|
|
355
|
+
callbacks.onDiagnostic?.(
|
|
356
|
+
`judgment backend 'jev' is enabled but ${jev.apiKeyEnv} is not set; falling back to 'llm'`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const requestedJudge = config.judgeBackend ?? config.judgment.default;
|
|
361
|
+
if (!judgeBackends[requestedJudge]) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Judgment backend '${requestedJudge}' is not available. Enable it in .pi/deepclause/config.json or use --judge=llm.`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
342
367
|
const sdk = await createDeepClause({
|
|
343
368
|
model: model.id,
|
|
344
369
|
maxTokens: config.maxTokens,
|
|
345
370
|
streaming: true,
|
|
346
371
|
debug: config.verbose,
|
|
347
372
|
llmBackend: backend,
|
|
373
|
+
judgeBackends,
|
|
374
|
+
defaultJudge: requestedJudge,
|
|
348
375
|
});
|
|
349
376
|
registerPiRuntimeTools(
|
|
350
377
|
sdk,
|
|
@@ -458,6 +485,7 @@ export async function executeDml(
|
|
|
458
485
|
gasLimit: config.gasLimit,
|
|
459
486
|
signal: controller.signal,
|
|
460
487
|
initialMessages,
|
|
488
|
+
judgeBackend: config.judgeBackend,
|
|
461
489
|
onUserInput: (prompt) => callbacks.onInput(prompt, controller.signal),
|
|
462
490
|
})) {
|
|
463
491
|
callbacks.onEvent(event);
|