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/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
  }
@@ -251,6 +275,18 @@ function viewerVendorAssetPath() {
251
275
  function publishResult(pi, content, details) {
252
276
  pi.sendMessage({ customType: "deepclause-result", content, display: true, details });
253
277
  }
278
+ /**
279
+ * Ask the user a DeepClause question.
280
+ *
281
+ * Pi's interactive text-input dialog renders only its title: the placeholder
282
+ * argument is ignored by `ExtensionInputComponent`. Passing the question as the
283
+ * placeholder (as the extension used to) made it invisible, so put the whole
284
+ * question in the title and label which run is asking.
285
+ */
286
+ export function requestDeepClauseInput(ctx, label, prompt, signal) {
287
+ const heading = label ? `DeepClause input — ${label}` : "DeepClause input";
288
+ return ctx.ui.input(`${heading}\n\n${prompt}`, undefined, { signal });
289
+ }
254
290
  export default function deepClauseExtension(pi) {
255
291
  let activeController;
256
292
  let activeDescription;
@@ -260,6 +296,26 @@ export default function deepClauseExtension(pi) {
260
296
  let planCommitRegistered = false;
261
297
  let planningTransaction;
262
298
  let pendingAgentStep;
299
+ /**
300
+ * Claim the single DeepClause execution slot synchronously, before any await.
301
+ * Without this, parallel `dc_run` calls all pass the `activeController` check
302
+ * while the first one is still awaiting its setup, then run concurrently and
303
+ * fight over the one-slot pi input dialog (leaving earlier runs hung).
304
+ */
305
+ const claimExecution = (description) => {
306
+ if (activeController)
307
+ return undefined;
308
+ const controller = new AbortController();
309
+ activeController = controller;
310
+ activeDescription = description;
311
+ return controller;
312
+ };
313
+ const releaseExecution = (controller) => {
314
+ if (activeController === controller) {
315
+ activeController = undefined;
316
+ activeDescription = undefined;
317
+ }
318
+ };
263
319
  const setPlanCommitActive = (enabled) => {
264
320
  if (enabled && !planCommitRegistered) {
265
321
  pi.registerTool({
@@ -455,6 +511,7 @@ export default function deepClauseExtension(pi) {
455
511
  promptGuidelines: [
456
512
  "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.",
457
513
  "Do not call dc_run while another DeepClause execution is active, and do not claim success unless dc_run returns an answer without errors.",
514
+ "If dc_run reports execution_already_active, wait for the active run to finish and then retry this skill instead of reporting failure.",
458
515
  ],
459
516
  parameters: Type.Object({
460
517
  skill: Type.String({ description: "Skill name such as example, or a DML path relative to .pi/deepclause/." }),
@@ -464,41 +521,40 @@ export default function deepClauseExtension(pi) {
464
521
  })),
465
522
  }),
466
523
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
467
- if (activeController) {
524
+ const controller = claimExecution("dc_run");
525
+ if (!controller) {
468
526
  return {
469
- content: [{ type: "text", text: "DeepClause execution rejected: another execution is already active." }],
527
+ content: [{ type: "text", text: "DeepClause execution rejected: another execution is active. Wait for it to finish, then call dc_run again for this skill." }],
470
528
  details: { success: false, error: "execution_already_active" },
471
529
  };
472
530
  }
473
- const paths = await initializeWorkspace(ctx.cwd);
474
- const config = await loadConfig(paths.config);
475
- if (!config.modelToolEnabled || !pi.getActiveTools().includes(DC_RUN_TOOL)) {
476
- return {
477
- content: [{ type: "text", text: "The dc_run tool is disabled. The user can enable it with /dc-tool enable." }],
478
- details: { success: false, error: "tool_disabled" },
479
- };
480
- }
481
- const mode = params.context ?? config.contextMode;
482
- const filePath = await resolveDmlPath(paths, params.skill);
483
- if (await isContextualPlan(filePath)) {
484
- return {
485
- 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." }],
486
- details: { success: false, error: "interactive_plan_requires_user_run" },
487
- };
488
- }
489
- const skillName = path.relative(paths.root, filePath);
490
- const initialMessages = buildInitialMessages(ctx.sessionManager.getBranch(), mode, config.branchMessageLimit);
491
- const controller = new AbortController();
492
531
  const cancel = () => controller.abort(signal?.reason ?? new Error("dc_run cancelled"));
493
532
  if (signal?.aborted)
494
533
  cancel();
495
534
  else
496
535
  signal?.addEventListener("abort", cancel, { once: true });
497
- activeController = controller;
498
- activeDescription = `model tool running ${skillName}`;
499
- const startedAt = Date.now();
500
- const progress = [];
501
536
  try {
537
+ const paths = await initializeWorkspace(ctx.cwd);
538
+ const config = await loadConfig(paths.config);
539
+ if (!config.modelToolEnabled || !pi.getActiveTools().includes(DC_RUN_TOOL)) {
540
+ return {
541
+ content: [{ type: "text", text: "The dc_run tool is disabled. The user can enable it with /dc-tool enable." }],
542
+ details: { success: false, error: "tool_disabled" },
543
+ };
544
+ }
545
+ const mode = params.context ?? config.contextMode;
546
+ const filePath = await resolveDmlPath(paths, params.skill);
547
+ if (await isContextualPlan(filePath)) {
548
+ return {
549
+ 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." }],
550
+ details: { success: false, error: "interactive_plan_requires_user_run" },
551
+ };
552
+ }
553
+ const skillName = path.relative(paths.root, filePath);
554
+ activeDescription = `model tool running ${skillName}`;
555
+ const initialMessages = buildInitialMessages(ctx.sessionManager.getBranch(), mode, config.branchMessageLimit);
556
+ const startedAt = Date.now();
557
+ const progress = [];
502
558
  const result = await executeDml(filePath, params.args ?? [], initialMessages, config, pi, ctx, controller, {
503
559
  onEvent: (event) => {
504
560
  if (event.type === "output" && event.content)
@@ -520,7 +576,7 @@ export default function deepClauseExtension(pi) {
520
576
  onInput: async (prompt, inputSignal) => {
521
577
  if (!ctx.hasUI)
522
578
  throw new Error("dc_run cannot request user input without interactive UI");
523
- const answer = await ctx.ui.input("DeepClause input", prompt, { signal: inputSignal });
579
+ const answer = await requestDeepClauseInput(ctx, skillName, prompt, inputSignal);
524
580
  if (answer === undefined)
525
581
  throw new Error("Input cancelled");
526
582
  return answer;
@@ -547,13 +603,12 @@ export default function deepClauseExtension(pi) {
547
603
  const message = error instanceof Error ? error.message : String(error);
548
604
  return {
549
605
  content: [{ type: "text", text: `DeepClause execution failed: ${message}` }],
550
- details: { success: false, skill: skillName, contextMode: mode, error: message },
606
+ details: { success: false, error: message },
551
607
  };
552
608
  }
553
609
  finally {
554
610
  signal?.removeEventListener("abort", cancel);
555
- activeController = undefined;
556
- activeDescription = undefined;
611
+ releaseExecution(controller);
557
612
  }
558
613
  },
559
614
  });
@@ -572,11 +627,11 @@ export default function deepClauseExtension(pi) {
572
627
  pi.registerTool({
573
628
  name: DC_DIAGRAM_TOOL,
574
629
  label: "Create DeepClause Diagram",
575
- 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.",
630
+ 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.",
576
631
  promptSnippet: "Create a presentation- or specification-grade diagram from a DML file",
577
632
  promptGuidelines: [
578
633
  "Use dc_diagram whenever the user asks for a diagram, flowchart, or visual of a .dml file; pass the exact path the user named.",
579
- "Choose grade=presentation for slides and overviews and grade=specification for engineering detail; use grade=both only when the user asks for both.",
634
+ "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.",
580
635
  "Do not hand-write Mermaid or run diagram tools yourself; call dc_diagram and report the viewer result.",
581
636
  ],
582
637
  parameters: Type.Object({
@@ -585,52 +640,47 @@ export default function deepClauseExtension(pi) {
585
640
  view: Type.Optional(StringEnum(["flow", "sequence"], { description: "Base layout used to seed the grade; default flow." })),
586
641
  }),
587
642
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
588
- if (activeController) {
643
+ const controller = claimExecution("dc_diagram");
644
+ if (!controller) {
589
645
  return {
590
646
  content: [{ type: "text", text: "Another DeepClause operation is already active; wait for it to finish." }],
591
647
  details: { success: false, error: "execution_already_active" },
592
648
  };
593
649
  }
594
- const requested = resolveGrade(String(params.grade ?? "")) ?? "presentation";
595
- const grades = requested === "both" ? ["presentation", "specification"] : [requested];
596
- const view = params.view === "sequence" ? "sequence" : "flow";
597
- let sourcePath;
598
- try {
599
- sourcePath = await resolveDiagramSource(ctx.cwd, params.dml);
600
- }
601
- catch (error) {
602
- const message = error instanceof Error ? error.message : String(error);
603
- return { content: [{ type: "text", text: `dc_diagram failed: ${message}` }], details: { success: false, error: message } };
604
- }
605
- if (!ctx.model) {
606
- return {
607
- content: [{ type: "text", text: "dc_diagram requires an active pi model. Select one and try again." }],
608
- details: { success: false, error: "no_model" },
609
- };
610
- }
611
- const config = await loadConfig(getPaths(ctx.cwd).config);
612
- const source = await readFile(sourcePath, "utf8");
613
- const display = displayPath(ctx.cwd, sourcePath);
614
- const seed = view === "sequence"
615
- ? renderSequence(display, source)
616
- : renderDml(display, source, { hideOutput: true });
617
- const targets = await collectDiagramTargets(ctx.cwd, [sourcePath]);
618
- const name = diagramNameFor(sourcePath, targets, ctx.cwd);
619
- const { diagrams, vendor } = await ensureDiagramDir(ctx.cwd, viewerVendorAssetPath());
620
- const templateText = await bundledViewerTemplate();
621
- const controller = new AbortController();
622
650
  const cancel = () => controller.abort(signal?.reason ?? new Error("dc_diagram cancelled"));
623
651
  if (signal?.aborted)
624
652
  cancel();
625
653
  else
626
654
  signal?.addEventListener("abort", cancel, { once: true });
627
- activeController = controller;
628
- activeDescription = `diagram ${name} (${grades.join("+")})`;
629
- const run = (command, args, options) => pi.exec(command, args, options);
630
- let chrome;
631
- let chromeResolved = false;
632
655
  try {
656
+ const requested = resolveGrade(String(params.grade ?? "")) ?? "presentation";
657
+ const grades = requested === "both" ? ["presentation", "specification"] : [requested];
658
+ const view = params.view === "sequence" ? "sequence" : "flow";
659
+ const sourcePath = await resolveDiagramSource(ctx.cwd, params.dml);
660
+ if (!ctx.model) {
661
+ return {
662
+ content: [{ type: "text", text: "dc_diagram requires an active pi model. Select one and try again." }],
663
+ details: { success: false, error: "no_model" },
664
+ };
665
+ }
666
+ const config = await loadConfig(getPaths(ctx.cwd).config);
667
+ const source = await readFile(sourcePath, "utf8");
668
+ const display = displayPath(ctx.cwd, sourcePath);
669
+ const targets = await collectDiagramTargets(ctx.cwd, [sourcePath]);
670
+ const name = diagramNameFor(sourcePath, targets, ctx.cwd);
671
+ const { diagrams, vendor } = await ensureDiagramDir(ctx.cwd, viewerVendorAssetPath());
672
+ const templateText = await bundledViewerTemplate();
673
+ activeDescription = `diagram ${name} (${grades.join("+")})`;
674
+ const run = (command, args, options) => pi.exec(command, args, options);
675
+ let chrome;
676
+ let chromeResolved = false;
633
677
  for (const grade of grades) {
678
+ // The specification seed carries the core decision logic and rule
679
+ // facts; the presentation seed stays small so the model can keep
680
+ // it to a general-audience overview.
681
+ const seed = view === "sequence"
682
+ ? renderSequence(display, source)
683
+ : renderDml(display, source, { hideOutput: true, includeLogic: grade === "specification" });
634
684
  const result = await polishDiagram({
635
685
  grade,
636
686
  view,
@@ -676,8 +726,7 @@ export default function deepClauseExtension(pi) {
676
726
  }
677
727
  finally {
678
728
  signal?.removeEventListener("abort", cancel);
679
- activeController = undefined;
680
- activeDescription = undefined;
729
+ releaseExecution(controller);
681
730
  }
682
731
  },
683
732
  });
@@ -727,7 +776,7 @@ export default function deepClauseExtension(pi) {
727
776
  const viewer = displayPath(ctx.cwd, build.viewerPath);
728
777
  return {
729
778
  content: [{ type: "text", text: `Created spec graph (${view}). Viewer: ${viewer}${opened ? " (opened in your browser)" : ""}` }],
730
- details: { success: true, view, viewer, opened },
779
+ details: { success: true, view, viewer, viewerPath: build.viewerPath, opened },
731
780
  };
732
781
  }
733
782
  catch (error) {
@@ -744,13 +793,13 @@ export default function deepClauseExtension(pi) {
744
793
  }
745
794
  };
746
795
  const runSpecSkill = async (ctx, skill, args = [], options = {}) => {
747
- const paths = await initializeWorkspace(ctx.cwd);
748
- const config = await loadConfig(paths.config);
749
- const filePath = await resolveDmlPath(paths, skill);
750
- const controller = new AbortController();
751
- activeController = controller;
752
- activeDescription = `running ${skill}`;
796
+ const controller = claimExecution(`running ${skill}`);
797
+ if (!controller)
798
+ throw new Error("Another DeepClause execution is already active");
753
799
  try {
800
+ const paths = await initializeWorkspace(ctx.cwd);
801
+ const config = await loadConfig(paths.config);
802
+ const filePath = await resolveDmlPath(paths, skill);
754
803
  const result = await executeDml(filePath, args, [], config, pi, ctx, controller, {
755
804
  onEvent() { },
756
805
  onInput: async () => { throw new Error("spec skills do not request input"); },
@@ -760,8 +809,7 @@ export default function deepClauseExtension(pi) {
760
809
  return result.answer ?? "(no answer)";
761
810
  }
762
811
  finally {
763
- activeController = undefined;
764
- activeDescription = undefined;
812
+ releaseExecution(controller);
765
813
  }
766
814
  };
767
815
  pi.on("session_start", async (_event, ctx) => {
@@ -844,6 +892,8 @@ export default function deepClauseExtension(pi) {
844
892
  `Skills: ${path.relative(ctx.cwd, paths.skills)}`,
845
893
  `Plans: ${path.relative(ctx.cwd, paths.plans)}`,
846
894
  `Context: ${config.contextMode} (verbose default: ${config.verbose})`,
895
+ `Judgment: ${config.judgment.default} (backends: llm${judgeBackendsEnabled(config)})`,
896
+ `Jev: ${jevStatus(config)}`,
847
897
  `Model tool (${DC_RUN_TOOL}): ${config.modelToolEnabled && pi.getActiveTools().includes(DC_RUN_TOOL) ? "enabled" : "disabled"}`,
848
898
  `Model tool (${DC_DIAGRAM_TOOL}): ${pi.getActiveTools().includes(DC_DIAGRAM_TOOL) ? "enabled" : "disabled"}`,
849
899
  "Ask pi for a presentation-grade or specification-grade diagram of any .dml file;",
@@ -854,10 +904,13 @@ export default function deepClauseExtension(pi) {
854
904
  " /dc-check <change|spec> validate specs and deltas deterministically",
855
905
  " /dc-archive <change> merge a change delta into specs/ and archive it",
856
906
  " /dc-apply <change> [--abort] execute tasks.dml; --abort discards an interrupted apply",
857
- " /dc-run <skill|path> [args] [--context=turn|branch|isolated]",
907
+ " /dc-run <skill|path> [args] [--context=turn|branch|isolated] [--judge=llm|jev]",
858
908
  " /dc-run <skill|path> --verbose show lifecycle events",
859
909
  " /dc-run <skill|path> --debug show full event payloads and SDK diagnostics",
860
910
  " /dc-tool enable|disable|status control the model-callable dc_run tool",
911
+ " /dc-judge [enable|disable|status] select the judgment backend (llm|jev)",
912
+ " /dc-judge default llm|jev set the default judgment backend",
913
+ " /dc-judge model <name> | key-env <ENV_VAR>",
861
914
  " /dc-cancel",
862
915
  ].join("\n");
863
916
  ctx.ui.notify(message, "info");
@@ -949,6 +1002,73 @@ export default function deepClauseExtension(pi) {
949
1002
  }
950
1003
  },
951
1004
  });
1005
+ pi.registerCommand("dc-judge", {
1006
+ description: "Enable, disable, or select the semantic judgment backend (llm|jev)",
1007
+ handler: async (rawArgs, ctx) => {
1008
+ const tokens = rawArgs.trim().split(/\s+/).filter(Boolean);
1009
+ const action = (tokens[0] ?? "status").toLowerCase();
1010
+ const paths = await initializeWorkspace(ctx.cwd);
1011
+ const describe = (config) => {
1012
+ const jev = config.judgment.jev;
1013
+ const keyPresent = Boolean(process.env[jev.apiKeyEnv]);
1014
+ return [
1015
+ `judgment backend: ${config.judgment.default}`,
1016
+ `registered backends: llm${jev.enabled ? ", jev" : ""}`,
1017
+ `jev: ${jev.enabled ? "enabled" : "disabled"} | model=${jev.model} | ${jev.apiKeyEnv}=${keyPresent ? "set" : "not set"}`,
1018
+ ].join("\n");
1019
+ };
1020
+ try {
1021
+ if (action === "status") {
1022
+ ctx.ui.notify(describe(await loadConfig(paths.config)), "info");
1023
+ return;
1024
+ }
1025
+ if (action === "enable" || action === "on" || action === "disable" || action === "off") {
1026
+ const enabled = action === "enable" || action === "on";
1027
+ const config = await setJudgeConfig(paths.config, { jev: { enabled } });
1028
+ const jev = config.judgment.jev;
1029
+ const note = enabled && !process.env[jev.apiKeyEnv]
1030
+ ? `\n${jev.apiKeyEnv} is not set; export it before using --judge=jev.`
1031
+ : "";
1032
+ ctx.ui.notify(`Jev backend ${enabled ? "enabled" : "disabled"} for this workspace.${note}`, enabled ? "warning" : "info");
1033
+ return;
1034
+ }
1035
+ if (action === "default") {
1036
+ const name = tokens[1];
1037
+ if (name !== "llm" && name !== "jev") {
1038
+ ctx.ui.notify("Usage: /dc-judge default llm|jev", "warning");
1039
+ return;
1040
+ }
1041
+ await setJudgeConfig(paths.config, { default: name });
1042
+ ctx.ui.notify(`Default judgment backend set to '${name}'`, "info");
1043
+ return;
1044
+ }
1045
+ if (action === "model") {
1046
+ const model = tokens[1];
1047
+ if (!model) {
1048
+ ctx.ui.notify("Usage: /dc-judge model <name>", "warning");
1049
+ return;
1050
+ }
1051
+ await setJudgeConfig(paths.config, { jev: { model } });
1052
+ ctx.ui.notify(`Jev model set to '${model}'`, "info");
1053
+ return;
1054
+ }
1055
+ if (action === "key-env") {
1056
+ const envName = tokens[1];
1057
+ if (!envName) {
1058
+ ctx.ui.notify("Usage: /dc-judge key-env <ENV_VAR>", "warning");
1059
+ return;
1060
+ }
1061
+ await setJudgeConfig(paths.config, { jev: { apiKeyEnv: envName } });
1062
+ ctx.ui.notify(`Jev API key environment variable set to '${envName}'`, "info");
1063
+ return;
1064
+ }
1065
+ ctx.ui.notify("Usage: /dc-judge [enable|disable|status|default llm|jev|model <name>|key-env <ENV_VAR>]", "warning");
1066
+ }
1067
+ catch (error) {
1068
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1069
+ }
1070
+ },
1071
+ });
952
1072
  pi.registerCommand("dc-list", {
953
1073
  description: "List DeepClause DML skills and generated plans",
954
1074
  handler: async (_args, ctx) => {
@@ -1098,7 +1218,8 @@ export default function deepClauseExtension(pi) {
1098
1218
  pi.registerCommand("dc-run", {
1099
1219
  description: "Run a DML skill with pi's active model",
1100
1220
  handler: async (rawArgs, ctx) => {
1101
- if (activeController) {
1221
+ const controller = claimExecution("dc-run");
1222
+ if (!controller) {
1102
1223
  ctx.ui.notify("A DeepClause execution is already active", "warning");
1103
1224
  return;
1104
1225
  }
@@ -1133,8 +1254,6 @@ export default function deepClauseExtension(pi) {
1133
1254
  }
1134
1255
  const mode = parsed.contextMode ?? config.contextMode;
1135
1256
  const initialMessages = buildInitialMessages(ctx.sessionManager.getBranch(), mode, config.branchMessageLimit);
1136
- const controller = new AbortController();
1137
- activeController = controller;
1138
1257
  const outputLines = [];
1139
1258
  const recentEvents = [];
1140
1259
  const events = [];
@@ -1209,11 +1328,11 @@ export default function deepClauseExtension(pi) {
1209
1328
  renderExecution();
1210
1329
  };
1211
1330
  try {
1212
- const result = await executeDml(filePath, parsed.args, initialMessages, { ...config, verbose: config.verbose || parsed.debug }, pi, ctx, controller, {
1331
+ const result = await executeDml(filePath, parsed.args, initialMessages, { ...config, verbose: config.verbose || parsed.debug, judgeBackend: parsed.judge }, pi, ctx, controller, {
1213
1332
  onEvent,
1214
1333
  onDiagnostic,
1215
1334
  onInput: async (prompt, signal) => {
1216
- const answer = await ctx.ui.input("DeepClause input", prompt, { signal });
1335
+ const answer = await requestDeepClauseInput(ctx, skillName, prompt, signal);
1217
1336
  if (answer === undefined)
1218
1337
  throw new Error("Input cancelled");
1219
1338
  return answer;
@@ -1244,8 +1363,7 @@ export default function deepClauseExtension(pi) {
1244
1363
  ctx.ui.notify(message, activeController?.signal.aborted ? "warning" : "error");
1245
1364
  }
1246
1365
  finally {
1247
- activeController = undefined;
1248
- activeDescription = undefined;
1366
+ releaseExecution(controller);
1249
1367
  ctx.ui.setStatus(STATUS_KEY, undefined);
1250
1368
  ctx.ui.setWidget(WIDGET_KEY, undefined);
1251
1369
  }
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, pi: ExtensionAPI, ctx: ExtensionContext, controller: AbortController, callbacks: ExecutionCallbacks, runPiAgentStep?: (request: PiAgentStepRequest, signal: AbortSignal) => Promise<PiAgentStepResult>, verifyCommands?: string[], applyChangeJsonPath?: string): Promise<ExecutionResult>;
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.0",
3
+ "version": "0.5.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.87",
46
+ "deepclause-sdk": "npm:deepclause-sdk@0.0.89",
47
47
  "typebox": "1.3.7"
48
48
  },
49
49
  "peerDependencies": {