deepclause-pi 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/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
  }
@@ -300,6 +322,24 @@ function publishResult(pi: ExtensionAPI, content: string, details: Record<string
300
322
  pi.sendMessage({ customType: "deepclause-result", content, display: true, details });
301
323
  }
302
324
 
325
+ /**
326
+ * Ask the user a DeepClause question.
327
+ *
328
+ * Pi's interactive text-input dialog renders only its title: the placeholder
329
+ * argument is ignored by `ExtensionInputComponent`. Passing the question as the
330
+ * placeholder (as the extension used to) made it invisible, so put the whole
331
+ * question in the title and label which run is asking.
332
+ */
333
+ export function requestDeepClauseInput(
334
+ ctx: Pick<ExtensionContext, "ui">,
335
+ label: string,
336
+ prompt: string,
337
+ signal: AbortSignal,
338
+ ): Promise<string | undefined> {
339
+ const heading = label ? `DeepClause input — ${label}` : "DeepClause input";
340
+ return ctx.ui.input(`${heading}\n\n${prompt}`, undefined, { signal });
341
+ }
342
+
303
343
  export default function deepClauseExtension(pi: ExtensionAPI) {
304
344
  let activeController: AbortController | undefined;
305
345
  let activeDescription: string | undefined;
@@ -310,6 +350,27 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
310
350
  let planningTransaction: PlanningTransaction | undefined;
311
351
  let pendingAgentStep: PendingAgentStep | undefined;
312
352
 
353
+ /**
354
+ * Claim the single DeepClause execution slot synchronously, before any await.
355
+ * Without this, parallel `dc_run` calls all pass the `activeController` check
356
+ * while the first one is still awaiting its setup, then run concurrently and
357
+ * fight over the one-slot pi input dialog (leaving earlier runs hung).
358
+ */
359
+ const claimExecution = (description: string): AbortController | undefined => {
360
+ if (activeController) return undefined;
361
+ const controller = new AbortController();
362
+ activeController = controller;
363
+ activeDescription = description;
364
+ return controller;
365
+ };
366
+
367
+ const releaseExecution = (controller: AbortController): void => {
368
+ if (activeController === controller) {
369
+ activeController = undefined;
370
+ activeDescription = undefined;
371
+ }
372
+ };
373
+
313
374
  const setPlanCommitActive = (enabled: boolean) => {
314
375
  if (enabled && !planCommitRegistered) {
315
376
  pi.registerTool({
@@ -506,6 +567,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
506
567
  promptGuidelines: [
507
568
  "Use dc_run only for existing DML programs when their deterministic logic, constraints, or specialized orchestration is useful; do not use dc_run to compile natural language or create a skill.",
508
569
  "Do not call dc_run while another DeepClause execution is active, and do not claim success unless dc_run returns an answer without errors.",
570
+ "If dc_run reports execution_already_active, wait for the active run to finish and then retry this skill instead of reporting failure.",
509
571
  ],
510
572
  parameters: Type.Object({
511
573
  skill: Type.String({ description: "Skill name such as example, or a DML path relative to .pi/deepclause/." }),
@@ -515,46 +577,45 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
515
577
  })),
516
578
  }),
517
579
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
518
- if (activeController) {
580
+ const controller = claimExecution("dc_run");
581
+ if (!controller) {
519
582
  return {
520
- content: [{ type: "text", text: "DeepClause execution rejected: another execution is already active." }],
583
+ content: [{ type: "text", text: "DeepClause execution rejected: another execution is active. Wait for it to finish, then call dc_run again for this skill." }],
521
584
  details: { success: false, error: "execution_already_active" },
522
585
  };
523
586
  }
524
-
525
- const paths = await initializeWorkspace(ctx.cwd);
526
- const config = await loadConfig(paths.config);
527
- if (!config.modelToolEnabled || !pi.getActiveTools().includes(DC_RUN_TOOL)) {
528
- return {
529
- content: [{ type: "text", text: "The dc_run tool is disabled. The user can enable it with /dc-tool enable." }],
530
- details: { success: false, error: "tool_disabled" },
531
- };
532
- }
533
-
534
- const mode = params.context ?? config.contextMode;
535
- const filePath = await resolveDmlPath(paths, params.skill);
536
- if (await isContextualPlan(filePath)) {
537
- return {
538
- content: [{ type: "text", text: "Contextual DML plans must be started by the user with /dc-run; they cannot start a nested pi agent turn from dc_run." }],
539
- details: { success: false, error: "interactive_plan_requires_user_run" },
540
- };
541
- }
542
- const skillName = path.relative(paths.root, filePath);
543
- const initialMessages = buildInitialMessages(
544
- ctx.sessionManager.getBranch(),
545
- mode,
546
- config.branchMessageLimit,
547
- );
548
- const controller = new AbortController();
549
587
  const cancel = () => controller.abort(signal?.reason ?? new Error("dc_run cancelled"));
550
588
  if (signal?.aborted) cancel();
551
589
  else signal?.addEventListener("abort", cancel, { once: true });
552
- activeController = controller;
553
- activeDescription = `model tool running ${skillName}`;
554
- const startedAt = Date.now();
555
- const progress: string[] = [];
556
590
 
557
591
  try {
592
+ const paths = await initializeWorkspace(ctx.cwd);
593
+ const config = await loadConfig(paths.config);
594
+ if (!config.modelToolEnabled || !pi.getActiveTools().includes(DC_RUN_TOOL)) {
595
+ return {
596
+ content: [{ type: "text", text: "The dc_run tool is disabled. The user can enable it with /dc-tool enable." }],
597
+ details: { success: false, error: "tool_disabled" },
598
+ };
599
+ }
600
+
601
+ const mode = params.context ?? config.contextMode;
602
+ const filePath = await resolveDmlPath(paths, params.skill);
603
+ if (await isContextualPlan(filePath)) {
604
+ return {
605
+ content: [{ type: "text", text: "Contextual DML plans must be started by the user with /dc-run; they cannot start a nested pi agent turn from dc_run." }],
606
+ details: { success: false, error: "interactive_plan_requires_user_run" },
607
+ };
608
+ }
609
+ const skillName = path.relative(paths.root, filePath);
610
+ activeDescription = `model tool running ${skillName}`;
611
+ const initialMessages = buildInitialMessages(
612
+ ctx.sessionManager.getBranch(),
613
+ mode,
614
+ config.branchMessageLimit,
615
+ );
616
+ const startedAt = Date.now();
617
+ const progress: string[] = [];
618
+
558
619
  const result = await executeDml(
559
620
  filePath,
560
621
  params.args ?? [],
@@ -578,7 +639,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
578
639
  onDiagnostic: () => {},
579
640
  onInput: async (prompt, inputSignal) => {
580
641
  if (!ctx.hasUI) throw new Error("dc_run cannot request user input without interactive UI");
581
- const answer = await ctx.ui.input("DeepClause input", prompt, { signal: inputSignal });
642
+ const answer = await requestDeepClauseInput(ctx, skillName, prompt, inputSignal);
582
643
  if (answer === undefined) throw new Error("Input cancelled");
583
644
  return answer;
584
645
  },
@@ -604,12 +665,11 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
604
665
  const message = error instanceof Error ? error.message : String(error);
605
666
  return {
606
667
  content: [{ type: "text", text: `DeepClause execution failed: ${message}` }],
607
- details: { success: false, skill: skillName, contextMode: mode, error: message },
668
+ details: { success: false, error: message },
608
669
  };
609
670
  } finally {
610
671
  signal?.removeEventListener("abort", cancel);
611
- activeController = undefined;
612
- activeDescription = undefined;
672
+ releaseExecution(controller);
613
673
  }
614
674
  },
615
675
  });
@@ -630,11 +690,11 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
630
690
  pi.registerTool({
631
691
  name: DC_DIAGRAM_TOOL,
632
692
  label: "Create DeepClause Diagram",
633
- description: "Create a presentation-grade or specification-grade Mermaid diagram from any .dml file, write a self-contained offline viewer under .pi/deepclause/diagrams/, and open it.",
693
+ description: "Create a presentation-grade or specification-grade Mermaid diagram from any .dml file, write a self-contained offline viewer under .pi/deepclause/diagrams/, and open it. The specification grade preserves the core decision logic and rule facts.",
634
694
  promptSnippet: "Create a presentation- or specification-grade diagram from a DML file",
635
695
  promptGuidelines: [
636
696
  "Use dc_diagram whenever the user asks for a diagram, flowchart, or visual of a .dml file; pass the exact path the user named.",
637
- "Choose grade=presentation for slides and overviews and grade=specification for engineering detail; use grade=both only when the user asks for both.",
697
+ "Choose grade=presentation for slides and overviews and grade=specification for engineering detail (decision predicates, thresholds, rule facts); use grade=both only when the user asks for both.",
638
698
  "Do not hand-write Mermaid or run diagram tools yourself; call dc_diagram and report the viewer result.",
639
699
  ],
640
700
  parameters: Type.Object({
@@ -643,55 +703,51 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
643
703
  view: Type.Optional(StringEnum(["flow", "sequence"] as const, { description: "Base layout used to seed the grade; default flow." })),
644
704
  }),
645
705
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
646
- if (activeController) {
706
+ const controller = claimExecution("dc_diagram");
707
+ if (!controller) {
647
708
  return {
648
709
  content: [{ type: "text", text: "Another DeepClause operation is already active; wait for it to finish." }],
649
710
  details: { success: false, error: "execution_already_active" },
650
711
  };
651
712
  }
652
-
653
- const requested = resolveGrade(String(params.grade ?? "")) ?? "presentation";
654
- const grades: DiagramGrade[] = requested === "both" ? ["presentation", "specification"] : [requested];
655
- const view: MermaidView = params.view === "sequence" ? "sequence" : "flow";
656
-
657
- let sourcePath: string;
658
- try {
659
- sourcePath = await resolveDiagramSource(ctx.cwd, params.dml);
660
- } catch (error) {
661
- const message = error instanceof Error ? error.message : String(error);
662
- return { content: [{ type: "text", text: `dc_diagram failed: ${message}` }], details: { success: false, error: message } };
663
- }
664
- if (!ctx.model) {
665
- return {
666
- content: [{ type: "text", text: "dc_diagram requires an active pi model. Select one and try again." }],
667
- details: { success: false, error: "no_model" },
668
- };
669
- }
670
-
671
- const config = await loadConfig(getPaths(ctx.cwd).config);
672
- const source = await readFile(sourcePath, "utf8");
673
- const display = displayPath(ctx.cwd, sourcePath);
674
- const seed = view === "sequence"
675
- ? renderSequence(display, source)
676
- : renderDml(display, source, { hideOutput: true });
677
- const targets = await collectDiagramTargets(ctx.cwd, [sourcePath]);
678
- const name = diagramNameFor(sourcePath, targets, ctx.cwd);
679
- const { diagrams, vendor } = await ensureDiagramDir(ctx.cwd, viewerVendorAssetPath());
680
- const templateText = await bundledViewerTemplate();
681
-
682
- const controller = new AbortController();
683
713
  const cancel = () => controller.abort(signal?.reason ?? new Error("dc_diagram cancelled"));
684
714
  if (signal?.aborted) cancel();
685
715
  else signal?.addEventListener("abort", cancel, { once: true });
686
- activeController = controller;
687
- activeDescription = `diagram ${name} (${grades.join("+")})`;
688
-
689
- const run = (command: string, args: string[], options?: { timeout?: number }) => pi.exec(command, args, options);
690
- let chrome: string | undefined;
691
- let chromeResolved = false;
692
716
 
693
717
  try {
718
+ const requested = resolveGrade(String(params.grade ?? "")) ?? "presentation";
719
+ const grades: DiagramGrade[] = requested === "both" ? ["presentation", "specification"] : [requested];
720
+ const view: MermaidView = params.view === "sequence" ? "sequence" : "flow";
721
+
722
+ const sourcePath = await resolveDiagramSource(ctx.cwd, params.dml);
723
+ if (!ctx.model) {
724
+ return {
725
+ content: [{ type: "text", text: "dc_diagram requires an active pi model. Select one and try again." }],
726
+ details: { success: false, error: "no_model" },
727
+ };
728
+ }
729
+
730
+ const config = await loadConfig(getPaths(ctx.cwd).config);
731
+ const source = await readFile(sourcePath, "utf8");
732
+ const display = displayPath(ctx.cwd, sourcePath);
733
+ const targets = await collectDiagramTargets(ctx.cwd, [sourcePath]);
734
+ const name = diagramNameFor(sourcePath, targets, ctx.cwd);
735
+ const { diagrams, vendor } = await ensureDiagramDir(ctx.cwd, viewerVendorAssetPath());
736
+ const templateText = await bundledViewerTemplate();
737
+
738
+ activeDescription = `diagram ${name} (${grades.join("+")})`;
739
+
740
+ const run = (command: string, args: string[], options?: { timeout?: number }) => pi.exec(command, args, options);
741
+ let chrome: string | undefined;
742
+ let chromeResolved = false;
743
+
694
744
  for (const grade of grades) {
745
+ // The specification seed carries the core decision logic and rule
746
+ // facts; the presentation seed stays small so the model can keep
747
+ // it to a general-audience overview.
748
+ const seed = view === "sequence"
749
+ ? renderSequence(display, source)
750
+ : renderDml(display, source, { hideOutput: true, includeLogic: grade === "specification" });
695
751
  const result = await polishDiagram({
696
752
  grade,
697
753
  view,
@@ -736,8 +792,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
736
792
  return { content: [{ type: "text", text: `dc_diagram failed: ${message}` }], details: { success: false, error: message } };
737
793
  } finally {
738
794
  signal?.removeEventListener("abort", cancel);
739
- activeController = undefined;
740
- activeDescription = undefined;
795
+ releaseExecution(controller);
741
796
  }
742
797
  },
743
798
  });
@@ -789,7 +844,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
789
844
  const viewer = displayPath(ctx.cwd, build.viewerPath);
790
845
  return {
791
846
  content: [{ type: "text", text: `Created spec graph (${view}). Viewer: ${viewer}${opened ? " (opened in your browser)" : ""}` }],
792
- details: { success: true, view, viewer, opened },
847
+ details: { success: true, view, viewer, viewerPath: build.viewerPath, opened },
793
848
  };
794
849
  } catch (error) {
795
850
  const message = error instanceof Error ? error.message : String(error);
@@ -811,13 +866,12 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
811
866
  args: string[] = [],
812
867
  options: { verifyCommands?: string[]; piAgentStep?: boolean; changeJsonPath?: string } = {},
813
868
  ): Promise<string> => {
814
- const paths = await initializeWorkspace(ctx.cwd);
815
- const config = await loadConfig(paths.config);
816
- const filePath = await resolveDmlPath(paths, skill);
817
- const controller = new AbortController();
818
- activeController = controller;
819
- activeDescription = `running ${skill}`;
869
+ const controller = claimExecution(`running ${skill}`);
870
+ if (!controller) throw new Error("Another DeepClause execution is already active");
820
871
  try {
872
+ const paths = await initializeWorkspace(ctx.cwd);
873
+ const config = await loadConfig(paths.config);
874
+ const filePath = await resolveDmlPath(paths, skill);
821
875
  const result = await executeDml(
822
876
  filePath,
823
877
  args,
@@ -837,8 +891,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
837
891
  if (result.errors.length) throw new Error(result.errors.join("\n"));
838
892
  return result.answer ?? "(no answer)";
839
893
  } finally {
840
- activeController = undefined;
841
- activeDescription = undefined;
894
+ releaseExecution(controller);
842
895
  }
843
896
  };
844
897
 
@@ -926,6 +979,8 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
926
979
  `Skills: ${path.relative(ctx.cwd, paths.skills)}`,
927
980
  `Plans: ${path.relative(ctx.cwd, paths.plans)}`,
928
981
  `Context: ${config.contextMode} (verbose default: ${config.verbose})`,
982
+ `Judgment: ${config.judgment.default} (backends: llm${judgeBackendsEnabled(config)})`,
983
+ `Jev: ${jevStatus(config)}`,
929
984
  `Model tool (${DC_RUN_TOOL}): ${config.modelToolEnabled && pi.getActiveTools().includes(DC_RUN_TOOL) ? "enabled" : "disabled"}`,
930
985
  `Model tool (${DC_DIAGRAM_TOOL}): ${pi.getActiveTools().includes(DC_DIAGRAM_TOOL) ? "enabled" : "disabled"}`,
931
986
  "Ask pi for a presentation-grade or specification-grade diagram of any .dml file;",
@@ -936,10 +991,13 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
936
991
  " /dc-check <change|spec> validate specs and deltas deterministically",
937
992
  " /dc-archive <change> merge a change delta into specs/ and archive it",
938
993
  " /dc-apply <change> [--abort] execute tasks.dml; --abort discards an interrupted apply",
939
- " /dc-run <skill|path> [args] [--context=turn|branch|isolated]",
994
+ " /dc-run <skill|path> [args] [--context=turn|branch|isolated] [--judge=llm|jev]",
940
995
  " /dc-run <skill|path> --verbose show lifecycle events",
941
996
  " /dc-run <skill|path> --debug show full event payloads and SDK diagnostics",
942
997
  " /dc-tool enable|disable|status control the model-callable dc_run tool",
998
+ " /dc-judge [enable|disable|status] select the judgment backend (llm|jev)",
999
+ " /dc-judge default llm|jev set the default judgment backend",
1000
+ " /dc-judge model <name> | key-env <ENV_VAR>",
943
1001
  " /dc-cancel",
944
1002
  ].join("\n");
945
1003
  ctx.ui.notify(message, "info");
@@ -1032,6 +1090,78 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
1032
1090
  },
1033
1091
  });
1034
1092
 
1093
+ pi.registerCommand("dc-judge", {
1094
+ description: "Enable, disable, or select the semantic judgment backend (llm|jev)",
1095
+ handler: async (rawArgs, ctx) => {
1096
+ const tokens = rawArgs.trim().split(/\s+/).filter(Boolean);
1097
+ const action = (tokens[0] ?? "status").toLowerCase();
1098
+ const paths = await initializeWorkspace(ctx.cwd);
1099
+
1100
+ const describe = (config: DeepClauseConfig): string => {
1101
+ const jev = config.judgment.jev;
1102
+ const keyPresent = Boolean(process.env[jev.apiKeyEnv]);
1103
+ return [
1104
+ `judgment backend: ${config.judgment.default}`,
1105
+ `registered backends: llm${jev.enabled ? ", jev" : ""}`,
1106
+ `jev: ${jev.enabled ? "enabled" : "disabled"} | model=${jev.model} | ${jev.apiKeyEnv}=${keyPresent ? "set" : "not set"}`,
1107
+ ].join("\n");
1108
+ };
1109
+
1110
+ try {
1111
+ if (action === "status") {
1112
+ ctx.ui.notify(describe(await loadConfig(paths.config)), "info");
1113
+ return;
1114
+ }
1115
+ if (action === "enable" || action === "on" || action === "disable" || action === "off") {
1116
+ const enabled = action === "enable" || action === "on";
1117
+ const config = await setJudgeConfig(paths.config, { jev: { enabled } });
1118
+ const jev = config.judgment.jev;
1119
+ const note = enabled && !process.env[jev.apiKeyEnv]
1120
+ ? `\n${jev.apiKeyEnv} is not set; export it before using --judge=jev.`
1121
+ : "";
1122
+ ctx.ui.notify(`Jev backend ${enabled ? "enabled" : "disabled"} for this workspace.${note}`, enabled ? "warning" : "info");
1123
+ return;
1124
+ }
1125
+ if (action === "default") {
1126
+ const name = tokens[1];
1127
+ if (name !== "llm" && name !== "jev") {
1128
+ ctx.ui.notify("Usage: /dc-judge default llm|jev", "warning");
1129
+ return;
1130
+ }
1131
+ await setJudgeConfig(paths.config, { default: name });
1132
+ ctx.ui.notify(`Default judgment backend set to '${name}'`, "info");
1133
+ return;
1134
+ }
1135
+ if (action === "model") {
1136
+ const model = tokens[1];
1137
+ if (!model) {
1138
+ ctx.ui.notify("Usage: /dc-judge model <name>", "warning");
1139
+ return;
1140
+ }
1141
+ await setJudgeConfig(paths.config, { jev: { model } });
1142
+ ctx.ui.notify(`Jev model set to '${model}'`, "info");
1143
+ return;
1144
+ }
1145
+ if (action === "key-env") {
1146
+ const envName = tokens[1];
1147
+ if (!envName) {
1148
+ ctx.ui.notify("Usage: /dc-judge key-env <ENV_VAR>", "warning");
1149
+ return;
1150
+ }
1151
+ await setJudgeConfig(paths.config, { jev: { apiKeyEnv: envName } });
1152
+ ctx.ui.notify(`Jev API key environment variable set to '${envName}'`, "info");
1153
+ return;
1154
+ }
1155
+ ctx.ui.notify(
1156
+ "Usage: /dc-judge [enable|disable|status|default llm|jev|model <name>|key-env <ENV_VAR>]",
1157
+ "warning",
1158
+ );
1159
+ } catch (error) {
1160
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1161
+ }
1162
+ },
1163
+ });
1164
+
1035
1165
  pi.registerCommand("dc-list", {
1036
1166
  description: "List DeepClause DML skills and generated plans",
1037
1167
  handler: async (_args, ctx) => {
@@ -1188,8 +1318,8 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
1188
1318
  pi.registerCommand("dc-run", {
1189
1319
  description: "Run a DML skill with pi's active model",
1190
1320
  handler: async (rawArgs, ctx) => {
1191
-
1192
- if (activeController) {
1321
+ const controller = claimExecution("dc-run");
1322
+ if (!controller) {
1193
1323
  ctx.ui.notify("A DeepClause execution is already active", "warning");
1194
1324
  return;
1195
1325
  }
@@ -1230,8 +1360,6 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
1230
1360
  mode,
1231
1361
  config.branchMessageLimit,
1232
1362
  );
1233
- const controller = new AbortController();
1234
- activeController = controller;
1235
1363
  const outputLines: string[] = [];
1236
1364
  const recentEvents: string[] = [];
1237
1365
  const events: Array<Record<string, unknown>> = [];
@@ -1302,7 +1430,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
1302
1430
  filePath,
1303
1431
  parsed.args,
1304
1432
  initialMessages,
1305
- { ...config, verbose: config.verbose || parsed.debug },
1433
+ { ...config, verbose: config.verbose || parsed.debug, judgeBackend: parsed.judge },
1306
1434
  pi,
1307
1435
  ctx,
1308
1436
  controller,
@@ -1310,7 +1438,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
1310
1438
  onEvent,
1311
1439
  onDiagnostic,
1312
1440
  onInput: async (prompt, signal) => {
1313
- const answer = await ctx.ui.input("DeepClause input", prompt, { signal });
1441
+ const answer = await requestDeepClauseInput(ctx, skillName, prompt, signal);
1314
1442
  if (answer === undefined) throw new Error("Input cancelled");
1315
1443
  return answer;
1316
1444
  },
@@ -1338,8 +1466,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
1338
1466
  publishResult(pi, `DeepClause execution failed: ${message}`, { error: message });
1339
1467
  ctx.ui.notify(message, activeController?.signal.aborted ? "warning" : "error");
1340
1468
  } finally {
1341
- activeController = undefined;
1342
- activeDescription = undefined;
1469
+ releaseExecution(controller);
1343
1470
  ctx.ui.setStatus(STATUS_KEY, undefined);
1344
1471
  ctx.ui.setWidget(WIDGET_KEY, undefined);
1345
1472
  }
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);