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/README.md +4 -4
- package/dist/config.d.ts +22 -0
- package/dist/config.js +60 -0
- package/dist/diagram/extract.d.ts +2 -0
- package/dist/diagram/extract.js +223 -3
- package/dist/diagram/grade.d.ts +1 -1
- package/dist/diagram/grade.js +4 -4
- package/dist/index.d.ts +10 -0
- package/dist/index.js +204 -86
- 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/skills/handbook-dml/SKILL.md +174 -46
- package/src/assets/AGENTS.md +43 -7
- package/src/assets/apply.dml +2 -2
- package/src/assets/specs.dml +30 -26
- package/src/config.ts +91 -0
- package/src/diagram/extract.ts +225 -3
- package/src/diagram/grade.ts +4 -4
- package/src/index.ts +224 -97
- package/src/runtime.ts +30 -2
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
|
-
|
|
524
|
+
const controller = claimExecution("dc_run");
|
|
525
|
+
if (!controller) {
|
|
468
526
|
return {
|
|
469
|
-
content: [{ type: "text", text: "DeepClause execution rejected: another execution is
|
|
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
|
|
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,
|
|
606
|
+
details: { success: false, error: message },
|
|
551
607
|
};
|
|
552
608
|
}
|
|
553
609
|
finally {
|
|
554
610
|
signal?.removeEventListener("abort", cancel);
|
|
555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.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.
|
|
46
|
+
"deepclause-sdk": "npm:deepclause-sdk@0.0.89",
|
|
47
47
|
"typebox": "1.3.7"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|