deepclause-pi 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +60 -0
- package/dist/diagram/viewer.d.ts +6 -0
- package/dist/diagram/viewer.js +5 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +423 -22
- package/dist/planner.d.ts +27 -2
- package/dist/planner.js +109 -4
- package/dist/runtime.d.ts +17 -1
- package/dist/runtime.js +139 -4
- package/dist/workspace.d.ts +3 -0
- package/dist/workspace.js +20 -0
- package/docs/SPECKIT.md +232 -0
- package/docs/SPEC_LAYER_PROPOSAL.md +1893 -0
- package/package.json +2 -2
- package/src/assets/AGENTS.md +66 -0
- package/src/assets/apply.dml +188 -0
- package/src/assets/spec_apply.dml +20 -0
- package/src/assets/spec_archive.dml +11 -0
- package/src/assets/spec_coverage.dml +26 -0
- package/src/assets/spec_graph.dml +12 -0
- package/src/assets/spec_merge.dml +10 -0
- package/src/assets/spec_query.dml +10 -0
- package/src/assets/spec_scaffold.dml +10 -0
- package/src/assets/spec_status.dml +7 -0
- package/src/assets/spec_validate.dml +9 -0
- package/src/assets/specs.dml +995 -0
- package/src/config.ts +91 -0
- package/src/diagram/viewer.ts +11 -0
- package/src/index.ts +454 -20
- package/src/planner.ts +123 -3
- package/src/runtime.ts +147 -4
- package/src/workspace.ts +24 -0
package/dist/index.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
1
|
+
import { access, mkdir, readFile, readdir, rename } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
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";
|
|
11
|
-
import { buildViewer, openViewerInBrowser, writeSidecar, } from "./diagram/viewer.js";
|
|
11
|
+
import { buildEntriesViewer, buildViewer, openViewerInBrowser, writeSidecar, } from "./diagram/viewer.js";
|
|
12
12
|
import { collectDiagramTargets, diagramNameFor, displayPath, ensureDiagramDir, resolveDiagramSource, } from "./diagram/workspace.js";
|
|
13
13
|
import { completeTextWithPiModel } from "./model.js";
|
|
14
|
-
import { executeDml } from "./runtime.js";
|
|
14
|
+
import { executeDml, gitRestore } from "./runtime.js";
|
|
15
15
|
import { getPaths, initializeWorkspace, resolveDmlPath } from "./workspace.js";
|
|
16
|
-
import { assemblePlanDml, buildPlanningPrompt, DC_PLAN_COMMIT_TOOL, isContextualPlan, PI_AGENT_STEP_TOOL, readPlanRequiredTools, validateGeneratedPlan, validatePlanSpec, writePlanNonDestructively, } from "./planner.js";
|
|
16
|
+
import { assemblePlanDml, assembleTasksDml, buildPlanningPrompt, DC_PLAN_COMMIT_TOOL, isContextualPlan, normalizePlanSlug, PI_AGENT_STEP_TOOL, readPlanRequiredTools, validateGeneratedPlan, validateGeneratedTasks, validatePlanSpec, writeChangeTasks, writePlanNonDestructively, } from "./planner.js";
|
|
17
17
|
const DC_RUN_TOOL = "dc_run";
|
|
18
18
|
const DC_DIAGRAM_TOOL = "dc_diagram";
|
|
19
|
-
const
|
|
19
|
+
const DC_SPEC_GRAPH_TOOL = "dc_spec_graph";
|
|
20
|
+
const AUTHORING_INSTRUCTION = `DeepClause programs live in .pi/deepclause/skills/ and executable generated plans live in .pi/deepclause/plans/. You may create and edit DML skills directly after consulting .pi/deepclause/AGENTS.md and DML_REFERENCE.md. Use /dc-plan when the user asks pi to design a contextual executable plan; finish that planning turn with dc_plan_commit. When the user asks for a diagram, flowchart, or visual of a .dml file, call the dc_diagram tool with the exact path and the requested grade (presentation or specification); it writes the viewer under .pi/deepclause/diagrams/ and opens it, so do not hand-write Mermaid. DeepClause compilation is unavailable, so generated content must already be valid DML. Users execute programs through /dc-run. If the opt-in dc_run tool is active, you may execute an ordinary skill with it, but contextual plans requiring pi_agent_step must be started by the user. Never invoke a compiler or create .deepclause/. Capability specs live in .pi/deepclause/specs/ and change deltas in .pi/deepclause/changes/<slug>/specs/; validate them deterministically with /dc-check, and call dc_spec_graph when the user wants a graph of capabilities, requirements, scenarios or changes.`;
|
|
20
21
|
const STATUS_KEY = "deepclause";
|
|
21
22
|
const WIDGET_KEY = "deepclause-stream";
|
|
22
23
|
export function splitArguments(input) {
|
|
@@ -67,10 +68,12 @@ export function parseRun(input) {
|
|
|
67
68
|
let contextMode;
|
|
68
69
|
let verbose = false;
|
|
69
70
|
let debug = false;
|
|
71
|
+
let judge;
|
|
70
72
|
const args = [];
|
|
71
73
|
for (let index = 0; index < tokens.length; index++) {
|
|
72
74
|
const token = tokens[index];
|
|
73
75
|
const contextValue = token.startsWith("--context=") ? token.slice("--context=".length) : undefined;
|
|
76
|
+
const judgeValue = token.startsWith("--judge=") ? token.slice("--judge=".length) : undefined;
|
|
74
77
|
if (token === "--verbose" || token === "-v") {
|
|
75
78
|
verbose = true;
|
|
76
79
|
}
|
|
@@ -91,34 +94,62 @@ export function parseRun(input) {
|
|
|
91
94
|
}
|
|
92
95
|
contextMode = value;
|
|
93
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
|
+
}
|
|
94
108
|
else {
|
|
95
109
|
args.push(token);
|
|
96
110
|
}
|
|
97
111
|
}
|
|
98
|
-
return { target, args, contextMode, verbose, debug };
|
|
112
|
+
return { target, args, contextMode, verbose, debug, judge };
|
|
99
113
|
}
|
|
100
114
|
export function parsePlan(input) {
|
|
101
115
|
const tokens = splitArguments(input);
|
|
102
116
|
let name;
|
|
117
|
+
let change;
|
|
118
|
+
let update = false;
|
|
103
119
|
let debug = false;
|
|
104
120
|
const requestParts = [];
|
|
105
121
|
for (let index = 0; index < tokens.length; index++) {
|
|
106
122
|
const token = tokens[index];
|
|
107
123
|
if (token === "--debug" || token === "-d")
|
|
108
124
|
debug = true;
|
|
125
|
+
else if (token === "--update")
|
|
126
|
+
update = true;
|
|
109
127
|
else if (token.startsWith("--name="))
|
|
110
128
|
name = token.slice("--name=".length);
|
|
111
129
|
else if (token === "--name")
|
|
112
130
|
name = tokens[++index];
|
|
131
|
+
else if (token.startsWith("--change="))
|
|
132
|
+
change = token.slice("--change=".length);
|
|
133
|
+
else if (token === "--change")
|
|
134
|
+
change = tokens[++index];
|
|
113
135
|
else
|
|
114
136
|
requestParts.push(token);
|
|
115
137
|
}
|
|
138
|
+
// allow the leading "update" keyword form: /dc-plan update --change=<slug> <request>
|
|
139
|
+
if (change && requestParts[0] === "update") {
|
|
140
|
+
update = true;
|
|
141
|
+
requestParts.shift();
|
|
142
|
+
}
|
|
116
143
|
const request = requestParts.join(" ").trim();
|
|
117
144
|
if (!request)
|
|
118
|
-
throw new Error("Usage: /dc-plan <request> [--name=slug] [--debug]");
|
|
145
|
+
throw new Error("Usage: /dc-plan <request> [--name=slug] [--change=slug] [--update] [--debug]");
|
|
119
146
|
if (name !== undefined && !name.trim())
|
|
120
147
|
throw new Error("--name requires a non-empty slug");
|
|
121
|
-
|
|
148
|
+
if (change !== undefined && !change.trim())
|
|
149
|
+
throw new Error("--change requires a non-empty slug");
|
|
150
|
+
if (update && !change)
|
|
151
|
+
throw new Error("--update requires --change=<slug>");
|
|
152
|
+
return { request, name: name?.trim(), change: change?.trim(), update, debug };
|
|
122
153
|
}
|
|
123
154
|
function messageText(message) {
|
|
124
155
|
if (!message || typeof message !== "object")
|
|
@@ -155,6 +186,52 @@ function eventSummary(event, debug) {
|
|
|
155
186
|
case "memory_compaction": return `compaction ${event.compactionAction ?? "event"}`;
|
|
156
187
|
}
|
|
157
188
|
}
|
|
189
|
+
async function isMutatingSpecSkill(filePath) {
|
|
190
|
+
try {
|
|
191
|
+
return /^%\s*Mutating:\s*true\s*$/m.test(await readFile(filePath, "utf8"));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* After a step that leaves the tree dirty, offer to commit it (or remind the user).
|
|
199
|
+
* A clean tree is what lets the next /dc-apply take a rollback snapshot.
|
|
200
|
+
*/
|
|
201
|
+
export async function offerCommit(pi, ctx, action, change) {
|
|
202
|
+
let status;
|
|
203
|
+
try {
|
|
204
|
+
status = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (status.code !== 0)
|
|
210
|
+
return; // not a repository: nothing to say
|
|
211
|
+
const files = status.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
212
|
+
if (files.length === 0)
|
|
213
|
+
return; // clean
|
|
214
|
+
const message = `${action}: ${change}`;
|
|
215
|
+
const listed = files.slice(0, 12).join("\n");
|
|
216
|
+
const more = files.length > 12 ? `\n… and ${files.length - 12} more` : "";
|
|
217
|
+
const reminder = `${files.length} changed file(s):\n${listed}${more}\n\ngit add -A && git commit -m "${message}"`;
|
|
218
|
+
if (!ctx.hasUI) {
|
|
219
|
+
ctx.ui.notify(`Uncommitted changes. ${reminder}`, "warning");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!await ctx.ui.confirm("Commit these changes?", `${reminder}\n\nCommit now?`)) {
|
|
223
|
+
ctx.ui.notify(`Remember to commit before continuing. ${reminder}`, "warning");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
await pi.exec("git", ["add", "-A"], { cwd: ctx.cwd });
|
|
227
|
+
const commit = await pi.exec("git", ["commit", "-m", message], { cwd: ctx.cwd });
|
|
228
|
+
if (commit.code === 0) {
|
|
229
|
+
ctx.ui.notify(`Committed: ${message}`, "info");
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
ctx.ui.notify(`Commit failed: ${commit.stderr.trim() || "see git output"}`, "error");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
158
235
|
async function listDmlFiles(directory, prefix = "") {
|
|
159
236
|
let entries;
|
|
160
237
|
try {
|
|
@@ -178,6 +255,17 @@ async function listDmlFiles(directory, prefix = "") {
|
|
|
178
255
|
function modelLabel(ctx) {
|
|
179
256
|
return ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "none selected";
|
|
180
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
|
+
}
|
|
181
269
|
async function bundledViewerTemplate() {
|
|
182
270
|
return readFile(fileURLToPath(new URL("./assets/viewer.template.html", import.meta.url)), "utf8");
|
|
183
271
|
}
|
|
@@ -192,6 +280,7 @@ export default function deepClauseExtension(pi) {
|
|
|
192
280
|
let activeDescription;
|
|
193
281
|
let modelToolRegistered = false;
|
|
194
282
|
let diagramToolRegistered = false;
|
|
283
|
+
let specGraphToolRegistered = false;
|
|
195
284
|
let planCommitRegistered = false;
|
|
196
285
|
let planningTransaction;
|
|
197
286
|
let pendingAgentStep;
|
|
@@ -219,6 +308,8 @@ export default function deepClauseExtension(pi) {
|
|
|
219
308
|
requiredTools: Type.Array(Type.String()),
|
|
220
309
|
relevantSkills: Type.Array(Type.String()),
|
|
221
310
|
expectedResult: Type.String(),
|
|
311
|
+
satisfies: Type.Optional(Type.Array(Type.String())),
|
|
312
|
+
checks: Type.Optional(Type.Array(Type.String())),
|
|
222
313
|
}), { minItems: 1, maxItems: 12 }),
|
|
223
314
|
finalSynthesis: Type.Optional(Type.String()),
|
|
224
315
|
failureMessage: Type.String(),
|
|
@@ -238,14 +329,18 @@ export default function deepClauseExtension(pi) {
|
|
|
238
329
|
};
|
|
239
330
|
}
|
|
240
331
|
try {
|
|
241
|
-
const plan = validatePlanSpec(params, transaction.snapshot, transaction.nameOverride
|
|
332
|
+
const plan = validatePlanSpec(params, transaction.snapshot, transaction.nameOverride, {
|
|
333
|
+
requireChecks: Boolean(transaction.change),
|
|
334
|
+
change: transaction.change,
|
|
335
|
+
});
|
|
242
336
|
const preview = [
|
|
243
337
|
plan.spec.title,
|
|
244
338
|
`Objective: ${plan.spec.objective}`,
|
|
339
|
+
transaction.change ? `Change: ${transaction.change}` : "",
|
|
245
340
|
`Steps: ${plan.spec.steps.length}`,
|
|
246
341
|
`Pi tools: ${plan.requiredTools.join(", ") || "none"}`,
|
|
247
|
-
...plan.spec.steps.map((step, index) => `${index + 1}. [${step.executor}] ${step.title}`),
|
|
248
|
-
].join("\n");
|
|
342
|
+
...plan.spec.steps.map((step, index) => `${index + 1}. [${step.executor}] ${step.title}${step.checks.length ? ` (${step.checks.length} checks)` : ""}`),
|
|
343
|
+
].filter(Boolean).join("\n");
|
|
249
344
|
if (!ctx.hasUI || !await ctx.ui.confirm("Create executable DeepClause plan?", preview)) {
|
|
250
345
|
return {
|
|
251
346
|
content: [{ type: "text", text: "Plan creation was not approved." }],
|
|
@@ -253,15 +348,27 @@ export default function deepClauseExtension(pi) {
|
|
|
253
348
|
};
|
|
254
349
|
}
|
|
255
350
|
const paths = await initializeWorkspace(ctx.cwd);
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
351
|
+
const content = transaction.change
|
|
352
|
+
? assembleTasksDml(plan, transaction.snapshot)
|
|
353
|
+
: assemblePlanDml(plan, transaction.snapshot);
|
|
354
|
+
if (transaction.change)
|
|
355
|
+
await validateGeneratedTasks(content);
|
|
356
|
+
else
|
|
357
|
+
await validateGeneratedPlan(content);
|
|
358
|
+
const filePath = transaction.change
|
|
359
|
+
? await writeChangeTasks(paths, normalizePlanSlug(transaction.change), content, Boolean(transaction.update))
|
|
360
|
+
: await writePlanNonDestructively(paths, plan.spec.slug, content);
|
|
259
361
|
transaction.committed = true;
|
|
260
362
|
setPlanCommitActive(false);
|
|
363
|
+
await offerCommit(pi, ctx, "plan", transaction.change ? normalizePlanSlug(transaction.change) : plan.spec.slug);
|
|
261
364
|
const relativePath = path.relative(paths.root, filePath).split(path.sep).join("/");
|
|
262
365
|
const text = [
|
|
263
|
-
|
|
264
|
-
|
|
366
|
+
transaction.change
|
|
367
|
+
? `Created change plan: .pi/deepclause/${relativePath}`
|
|
368
|
+
: `Created executable DML plan: .pi/deepclause/${relativePath}`,
|
|
369
|
+
transaction.change
|
|
370
|
+
? `Next: /dc-check ${normalizePlanSlug(transaction.change)}`
|
|
371
|
+
: `Run it with: /dc-run ${relativePath}`,
|
|
265
372
|
plan.warnings.length ? `Warnings:\n${plan.warnings.join("\n")}` : "",
|
|
266
373
|
].filter(Boolean).join("\n\n");
|
|
267
374
|
return {
|
|
@@ -269,6 +376,7 @@ export default function deepClauseExtension(pi) {
|
|
|
269
376
|
details: {
|
|
270
377
|
success: true,
|
|
271
378
|
path: relativePath,
|
|
379
|
+
change: transaction.change,
|
|
272
380
|
contextual: plan.spec.steps.some((step) => step.executor === "pi"),
|
|
273
381
|
requiredTools: plan.requiredTools,
|
|
274
382
|
warnings: plan.warnings,
|
|
@@ -604,10 +712,87 @@ export default function deepClauseExtension(pi) {
|
|
|
604
712
|
pi.setActiveTools([...activeTools, DC_DIAGRAM_TOOL]);
|
|
605
713
|
}
|
|
606
714
|
};
|
|
715
|
+
const setSpecGraphActive = () => {
|
|
716
|
+
if (!specGraphToolRegistered) {
|
|
717
|
+
pi.registerTool({
|
|
718
|
+
name: DC_SPEC_GRAPH_TOOL,
|
|
719
|
+
label: "Spec Graph",
|
|
720
|
+
description: "Create a Mermaid graph of DeepClause capabilities, requirements, scenarios and changes, write a viewer under .pi/deepclause/diagrams/, and open it.",
|
|
721
|
+
promptSnippet: "Create a capability/change graph from DeepClause spec facts",
|
|
722
|
+
promptGuidelines: [
|
|
723
|
+
"Call dc_spec_graph when the user asks for a graph or visual of capabilities, requirements, changes, or spec coverage.",
|
|
724
|
+
],
|
|
725
|
+
parameters: Type.Object({
|
|
726
|
+
view: Type.Optional(StringEnum(["capabilities", "changes"])),
|
|
727
|
+
}),
|
|
728
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
729
|
+
try {
|
|
730
|
+
const view = params.view ?? "capabilities";
|
|
731
|
+
const mermaid = await runSpecSkill(ctx, "spec_graph", [view]);
|
|
732
|
+
const name = `spec-${view}`;
|
|
733
|
+
const entry = {
|
|
734
|
+
name,
|
|
735
|
+
path: `specs (${view})`,
|
|
736
|
+
flow: mermaid,
|
|
737
|
+
seq: "",
|
|
738
|
+
dml: "",
|
|
739
|
+
presentation: mermaid,
|
|
740
|
+
specification: null,
|
|
741
|
+
};
|
|
742
|
+
const build = await buildEntriesViewer({
|
|
743
|
+
cwd: ctx.cwd,
|
|
744
|
+
templateText: await bundledViewerTemplate(),
|
|
745
|
+
vendorAssetPath: viewerVendorAssetPath(),
|
|
746
|
+
entries: [entry],
|
|
747
|
+
});
|
|
748
|
+
const opened = ctx.hasUI
|
|
749
|
+
? await openViewerInBrowser(pi, build.viewerPath, name, "presentation")
|
|
750
|
+
: false;
|
|
751
|
+
const viewer = displayPath(ctx.cwd, build.viewerPath);
|
|
752
|
+
return {
|
|
753
|
+
content: [{ type: "text", text: `Created spec graph (${view}). Viewer: ${viewer}${opened ? " (opened in your browser)" : ""}` }],
|
|
754
|
+
details: { success: true, view, viewer, viewerPath: build.viewerPath, opened },
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
759
|
+
return { content: [{ type: "text", text: `dc_spec_graph failed: ${message}` }], details: { success: false, error: message } };
|
|
760
|
+
}
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
specGraphToolRegistered = true;
|
|
764
|
+
}
|
|
765
|
+
const activeTools = pi.getActiveTools();
|
|
766
|
+
if (!activeTools.includes(DC_SPEC_GRAPH_TOOL)) {
|
|
767
|
+
pi.setActiveTools([...activeTools, DC_SPEC_GRAPH_TOOL]);
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
const runSpecSkill = async (ctx, skill, args = [], options = {}) => {
|
|
771
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
772
|
+
const config = await loadConfig(paths.config);
|
|
773
|
+
const filePath = await resolveDmlPath(paths, skill);
|
|
774
|
+
const controller = new AbortController();
|
|
775
|
+
activeController = controller;
|
|
776
|
+
activeDescription = `running ${skill}`;
|
|
777
|
+
try {
|
|
778
|
+
const result = await executeDml(filePath, args, [], config, pi, ctx, controller, {
|
|
779
|
+
onEvent() { },
|
|
780
|
+
onInput: async () => { throw new Error("spec skills do not request input"); },
|
|
781
|
+
}, options.piAgentStep ? (request, signal) => runPiAgentStep(request, signal, ctx) : undefined, options.verifyCommands ?? [], options.changeJsonPath);
|
|
782
|
+
if (result.errors.length)
|
|
783
|
+
throw new Error(result.errors.join("\n"));
|
|
784
|
+
return result.answer ?? "(no answer)";
|
|
785
|
+
}
|
|
786
|
+
finally {
|
|
787
|
+
activeController = undefined;
|
|
788
|
+
activeDescription = undefined;
|
|
789
|
+
}
|
|
790
|
+
};
|
|
607
791
|
pi.on("session_start", async (_event, ctx) => {
|
|
608
792
|
const config = await loadConfig(getPaths(ctx.cwd).config);
|
|
609
793
|
setModelToolActive(config.modelToolEnabled);
|
|
610
794
|
setDiagramToolActive();
|
|
795
|
+
setSpecGraphActive();
|
|
611
796
|
});
|
|
612
797
|
pi.on("tool_execution_start", (event) => {
|
|
613
798
|
if (pendingAgentStep && !pendingAgentStep.toolsUsed.includes(event.toolName)) {
|
|
@@ -683,17 +868,25 @@ export default function deepClauseExtension(pi) {
|
|
|
683
868
|
`Skills: ${path.relative(ctx.cwd, paths.skills)}`,
|
|
684
869
|
`Plans: ${path.relative(ctx.cwd, paths.plans)}`,
|
|
685
870
|
`Context: ${config.contextMode} (verbose default: ${config.verbose})`,
|
|
871
|
+
`Judgment: ${config.judgment.default} (backends: llm${judgeBackendsEnabled(config)})`,
|
|
872
|
+
`Jev: ${jevStatus(config)}`,
|
|
686
873
|
`Model tool (${DC_RUN_TOOL}): ${config.modelToolEnabled && pi.getActiveTools().includes(DC_RUN_TOOL) ? "enabled" : "disabled"}`,
|
|
687
874
|
`Model tool (${DC_DIAGRAM_TOOL}): ${pi.getActiveTools().includes(DC_DIAGRAM_TOOL) ? "enabled" : "disabled"}`,
|
|
688
875
|
"Ask pi for a presentation-grade or specification-grade diagram of any .dml file;",
|
|
689
876
|
"it writes the viewer under .pi/deepclause/diagrams/ and opens it.",
|
|
690
877
|
"Commands:",
|
|
691
878
|
" /dc-list",
|
|
692
|
-
" /dc-plan <request> [--name=slug] create
|
|
693
|
-
" /dc-
|
|
879
|
+
" /dc-plan <request> [--change=slug] [--update] [--name=slug] create or regenerate a plan",
|
|
880
|
+
" /dc-check <change|spec> validate specs and deltas deterministically",
|
|
881
|
+
" /dc-archive <change> merge a change delta into specs/ and archive it",
|
|
882
|
+
" /dc-apply <change> [--abort] execute tasks.dml; --abort discards an interrupted apply",
|
|
883
|
+
" /dc-run <skill|path> [args] [--context=turn|branch|isolated] [--judge=llm|jev]",
|
|
694
884
|
" /dc-run <skill|path> --verbose show lifecycle events",
|
|
695
885
|
" /dc-run <skill|path> --debug show full event payloads and SDK diagnostics",
|
|
696
886
|
" /dc-tool enable|disable|status control the model-callable dc_run tool",
|
|
887
|
+
" /dc-judge [enable|disable|status] select the judgment backend (llm|jev)",
|
|
888
|
+
" /dc-judge default llm|jev set the default judgment backend",
|
|
889
|
+
" /dc-judge model <name> | key-env <ENV_VAR>",
|
|
697
890
|
" /dc-cancel",
|
|
698
891
|
].join("\n");
|
|
699
892
|
ctx.ui.notify(message, "info");
|
|
@@ -711,6 +904,17 @@ export default function deepClauseExtension(pi) {
|
|
|
711
904
|
if (!ctx.model)
|
|
712
905
|
throw new Error("Select a pi model before creating a plan");
|
|
713
906
|
const paths = await initializeWorkspace(ctx.cwd);
|
|
907
|
+
if (parsed.change && !parsed.update) {
|
|
908
|
+
const changeSlug = normalizePlanSlug(parsed.change);
|
|
909
|
+
try {
|
|
910
|
+
await access(path.join(paths.changes, changeSlug, "tasks.dml"));
|
|
911
|
+
ctx.ui.notify(`changes/${changeSlug}/tasks.dml already exists. Re-run with --update to regenerate it, or edit tasks.dml directly.`, "error");
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
catch {
|
|
915
|
+
// no existing plan: proceed
|
|
916
|
+
}
|
|
917
|
+
}
|
|
714
918
|
const promptOptions = ctx.getSystemPromptOptions();
|
|
715
919
|
const snapshot = {
|
|
716
920
|
model: `${ctx.model.provider}/${ctx.model.id}`,
|
|
@@ -725,12 +929,18 @@ export default function deepClauseExtension(pi) {
|
|
|
725
929
|
planningTransaction = {
|
|
726
930
|
snapshot,
|
|
727
931
|
nameOverride: parsed.name,
|
|
932
|
+
change: parsed.change,
|
|
933
|
+
update: parsed.update,
|
|
728
934
|
committed: false,
|
|
729
935
|
startedAt: Date.now(),
|
|
730
936
|
};
|
|
731
937
|
setPlanCommitActive(true);
|
|
732
|
-
ctx.ui.notify(
|
|
733
|
-
|
|
938
|
+
ctx.ui.notify(parsed.change
|
|
939
|
+
? parsed.update
|
|
940
|
+
? `Regenerating the change plan for '${parsed.change}'. Existing artifacts are read first and tasks.dml statuses reset to pending.`
|
|
941
|
+
: `Starting a change planning turn for '${parsed.change}'. Review the delta and plan before it is written.`
|
|
942
|
+
: "Starting a contextual pi planning turn. Review the generated plan before it is written.", "info");
|
|
943
|
+
pi.sendUserMessage(buildPlanningPrompt(parsed.request, snapshot, parsed.name, parsed.change, parsed.update));
|
|
734
944
|
}
|
|
735
945
|
catch (error) {
|
|
736
946
|
planningTransaction = undefined;
|
|
@@ -768,6 +978,73 @@ export default function deepClauseExtension(pi) {
|
|
|
768
978
|
}
|
|
769
979
|
},
|
|
770
980
|
});
|
|
981
|
+
pi.registerCommand("dc-judge", {
|
|
982
|
+
description: "Enable, disable, or select the semantic judgment backend (llm|jev)",
|
|
983
|
+
handler: async (rawArgs, ctx) => {
|
|
984
|
+
const tokens = rawArgs.trim().split(/\s+/).filter(Boolean);
|
|
985
|
+
const action = (tokens[0] ?? "status").toLowerCase();
|
|
986
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
987
|
+
const describe = (config) => {
|
|
988
|
+
const jev = config.judgment.jev;
|
|
989
|
+
const keyPresent = Boolean(process.env[jev.apiKeyEnv]);
|
|
990
|
+
return [
|
|
991
|
+
`judgment backend: ${config.judgment.default}`,
|
|
992
|
+
`registered backends: llm${jev.enabled ? ", jev" : ""}`,
|
|
993
|
+
`jev: ${jev.enabled ? "enabled" : "disabled"} | model=${jev.model} | ${jev.apiKeyEnv}=${keyPresent ? "set" : "not set"}`,
|
|
994
|
+
].join("\n");
|
|
995
|
+
};
|
|
996
|
+
try {
|
|
997
|
+
if (action === "status") {
|
|
998
|
+
ctx.ui.notify(describe(await loadConfig(paths.config)), "info");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (action === "enable" || action === "on" || action === "disable" || action === "off") {
|
|
1002
|
+
const enabled = action === "enable" || action === "on";
|
|
1003
|
+
const config = await setJudgeConfig(paths.config, { jev: { enabled } });
|
|
1004
|
+
const jev = config.judgment.jev;
|
|
1005
|
+
const note = enabled && !process.env[jev.apiKeyEnv]
|
|
1006
|
+
? `\n${jev.apiKeyEnv} is not set; export it before using --judge=jev.`
|
|
1007
|
+
: "";
|
|
1008
|
+
ctx.ui.notify(`Jev backend ${enabled ? "enabled" : "disabled"} for this workspace.${note}`, enabled ? "warning" : "info");
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (action === "default") {
|
|
1012
|
+
const name = tokens[1];
|
|
1013
|
+
if (name !== "llm" && name !== "jev") {
|
|
1014
|
+
ctx.ui.notify("Usage: /dc-judge default llm|jev", "warning");
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
await setJudgeConfig(paths.config, { default: name });
|
|
1018
|
+
ctx.ui.notify(`Default judgment backend set to '${name}'`, "info");
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (action === "model") {
|
|
1022
|
+
const model = tokens[1];
|
|
1023
|
+
if (!model) {
|
|
1024
|
+
ctx.ui.notify("Usage: /dc-judge model <name>", "warning");
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
await setJudgeConfig(paths.config, { jev: { model } });
|
|
1028
|
+
ctx.ui.notify(`Jev model set to '${model}'`, "info");
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (action === "key-env") {
|
|
1032
|
+
const envName = tokens[1];
|
|
1033
|
+
if (!envName) {
|
|
1034
|
+
ctx.ui.notify("Usage: /dc-judge key-env <ENV_VAR>", "warning");
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
await setJudgeConfig(paths.config, { jev: { apiKeyEnv: envName } });
|
|
1038
|
+
ctx.ui.notify(`Jev API key environment variable set to '${envName}'`, "info");
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
ctx.ui.notify("Usage: /dc-judge [enable|disable|status|default llm|jev|model <name>|key-env <ENV_VAR>]", "warning");
|
|
1042
|
+
}
|
|
1043
|
+
catch (error) {
|
|
1044
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
1045
|
+
}
|
|
1046
|
+
},
|
|
1047
|
+
});
|
|
771
1048
|
pi.registerCommand("dc-list", {
|
|
772
1049
|
description: "List DeepClause DML skills and generated plans",
|
|
773
1050
|
handler: async (_args, ctx) => {
|
|
@@ -794,6 +1071,126 @@ export default function deepClauseExtension(pi) {
|
|
|
794
1071
|
ctx.ui.notify("Cancelling DeepClause execution", "warning");
|
|
795
1072
|
},
|
|
796
1073
|
});
|
|
1074
|
+
pi.registerCommand("dc-check", {
|
|
1075
|
+
description: "Validate DeepClause specs and change deltas deterministically (no model calls)",
|
|
1076
|
+
handler: async (_rawArgs, ctx) => {
|
|
1077
|
+
if (activeController || !ctx.isIdle()) {
|
|
1078
|
+
ctx.ui.notify("DeepClause or pi is already active; wait before running /dc-check", "warning");
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
try {
|
|
1082
|
+
const answer = await runSpecSkill(ctx, "spec_validate");
|
|
1083
|
+
publishResult(pi, answer, { skill: "spec_validate" });
|
|
1084
|
+
ctx.ui.notify(answer.startsWith("spec check: OK") ? "Spec check passed" : "Spec check reported errors", answer.startsWith("spec check: OK") ? "info" : "warning");
|
|
1085
|
+
}
|
|
1086
|
+
catch (error) {
|
|
1087
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1088
|
+
publishResult(pi, `Spec check failed: ${message}`, { error: message });
|
|
1089
|
+
ctx.ui.notify(message, "error");
|
|
1090
|
+
}
|
|
1091
|
+
},
|
|
1092
|
+
});
|
|
1093
|
+
pi.registerCommand("dc-archive", {
|
|
1094
|
+
description: "Merge a change delta into specs/ after review, then move the change into changes/archive/",
|
|
1095
|
+
handler: async (rawArgs, ctx) => {
|
|
1096
|
+
if (activeController || !ctx.isIdle()) {
|
|
1097
|
+
ctx.ui.notify("DeepClause or pi is already active; wait before running /dc-archive", "warning");
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
const change = rawArgs.trim();
|
|
1101
|
+
if (!change) {
|
|
1102
|
+
ctx.ui.notify("Usage: /dc-archive <change>", "warning");
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
try {
|
|
1106
|
+
const plan = await runSpecSkill(ctx, "spec_merge", [change]);
|
|
1107
|
+
if (!ctx.hasUI || !await ctx.ui.confirm("Archive change into specs?", plan)) {
|
|
1108
|
+
ctx.ui.notify("Archive cancelled", "warning");
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
const applied = await runSpecSkill(ctx, "spec_archive", [change]);
|
|
1112
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
1113
|
+
const from = path.join(paths.changes, change);
|
|
1114
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
1115
|
+
await mkdir(path.join(paths.changes, "archive"), { recursive: true });
|
|
1116
|
+
let target = path.join(paths.changes, "archive", `${stamp}-${change}`);
|
|
1117
|
+
try {
|
|
1118
|
+
await access(target);
|
|
1119
|
+
target = `${target}-2`;
|
|
1120
|
+
}
|
|
1121
|
+
catch {
|
|
1122
|
+
// target is free
|
|
1123
|
+
}
|
|
1124
|
+
await rename(from, target);
|
|
1125
|
+
const archivedTo = path.relative(ctx.cwd, target).split(path.sep).join("/");
|
|
1126
|
+
publishResult(pi, `${applied}\n\n moved to ${archivedTo}`, { skill: "spec_archive", change, archivedTo });
|
|
1127
|
+
ctx.ui.notify(`Archived ${change}`, "info");
|
|
1128
|
+
await offerCommit(pi, ctx, "archive", change);
|
|
1129
|
+
}
|
|
1130
|
+
catch (error) {
|
|
1131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1132
|
+
publishResult(pi, `Archive failed: ${message}`, { error: message });
|
|
1133
|
+
ctx.ui.notify(message, "error");
|
|
1134
|
+
}
|
|
1135
|
+
},
|
|
1136
|
+
});
|
|
1137
|
+
pi.registerCommand("dc-apply", {
|
|
1138
|
+
description: "Execute a change's tasks.dml with per-task verification and bounded retries",
|
|
1139
|
+
handler: async (rawArgs, ctx) => {
|
|
1140
|
+
if (activeController || !ctx.isIdle()) {
|
|
1141
|
+
ctx.ui.notify("DeepClause or pi is already active; wait before running /dc-apply", "warning");
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const tokens = splitArguments(rawArgs);
|
|
1145
|
+
const abort = tokens.includes("--abort");
|
|
1146
|
+
const change = tokens.filter((token) => token !== "--abort").join(" ").trim();
|
|
1147
|
+
if (!change) {
|
|
1148
|
+
ctx.ui.notify("Usage: /dc-apply <change> [--abort]", "warning");
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
try {
|
|
1152
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
1153
|
+
const changeJson = path.join(paths.changes, change, "change.json");
|
|
1154
|
+
if (abort) {
|
|
1155
|
+
const restored = await gitRestore(pi, ctx.cwd, changeJson).catch(() => null);
|
|
1156
|
+
const message = restored
|
|
1157
|
+
? `Discarded the apply and restored the working tree to ${restored}.`
|
|
1158
|
+
: "No recorded apply snapshot to discard.";
|
|
1159
|
+
publishResult(pi, message, { skill: "spec_apply", change, aborted: Boolean(restored) });
|
|
1160
|
+
ctx.ui.notify(message, restored ? "warning" : "info");
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
let started = false;
|
|
1164
|
+
let succeeded = false;
|
|
1165
|
+
try {
|
|
1166
|
+
const plan = await runSpecSkill(ctx, "spec_apply", [change, "plan"]);
|
|
1167
|
+
const commands = [...new Set([...plan.matchAll(/^command:\s*(.+)$/gm)].map((match) => match[1].trim()))];
|
|
1168
|
+
const preview = [plan, "", `Approved verification commands: ${commands.join(", ") || "none"}`].join("\n");
|
|
1169
|
+
if (!ctx.hasUI || !await ctx.ui.confirm("Apply change tasks?", preview)) {
|
|
1170
|
+
ctx.ui.notify("Apply cancelled", "warning");
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
started = true;
|
|
1174
|
+
const answer = await runSpecSkill(ctx, "spec_apply", [change, "apply"], { verifyCommands: commands, piAgentStep: true, changeJsonPath: changeJson });
|
|
1175
|
+
succeeded = answer.includes("status: OK");
|
|
1176
|
+
publishResult(pi, answer, { skill: "spec_apply", change });
|
|
1177
|
+
ctx.ui.notify(succeeded ? `Applied ${change}` : `Apply incomplete for ${change}`, succeeded ? "info" : "warning");
|
|
1178
|
+
}
|
|
1179
|
+
finally {
|
|
1180
|
+
if (started && !succeeded) {
|
|
1181
|
+
ctx.ui.notify(`Apply interrupted; the working tree and task statuses were preserved. Resume with /dc-apply ${change}, or discard with /dc-apply ${change} --abort.`, "warning");
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
if (succeeded)
|
|
1185
|
+
await offerCommit(pi, ctx, "apply", change);
|
|
1186
|
+
}
|
|
1187
|
+
catch (error) {
|
|
1188
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1189
|
+
publishResult(pi, `Apply failed: ${message}`, { error: message });
|
|
1190
|
+
ctx.ui.notify(message, "error");
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
});
|
|
797
1194
|
pi.registerCommand("dc-run", {
|
|
798
1195
|
description: "Run a DML skill with pi's active model",
|
|
799
1196
|
handler: async (rawArgs, ctx) => {
|
|
@@ -806,6 +1203,10 @@ export default function deepClauseExtension(pi) {
|
|
|
806
1203
|
const paths = await initializeWorkspace(ctx.cwd);
|
|
807
1204
|
const config = await loadConfig(paths.config);
|
|
808
1205
|
const filePath = await resolveDmlPath(paths, parsed.target);
|
|
1206
|
+
if (await isMutatingSpecSkill(filePath)) {
|
|
1207
|
+
ctx.ui.notify(`${parsed.target} modifies specs/. Use /dc-archive <change> so you can review the merge first.`, "warning");
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
809
1210
|
const contextualPlan = await isContextualPlan(filePath);
|
|
810
1211
|
if (contextualPlan) {
|
|
811
1212
|
const requiredTools = await readPlanRequiredTools(filePath);
|
|
@@ -904,7 +1305,7 @@ export default function deepClauseExtension(pi) {
|
|
|
904
1305
|
renderExecution();
|
|
905
1306
|
};
|
|
906
1307
|
try {
|
|
907
|
-
const result = await executeDml(filePath, parsed.args, initialMessages, { ...config, verbose: config.verbose || parsed.debug }, pi, ctx, controller, {
|
|
1308
|
+
const result = await executeDml(filePath, parsed.args, initialMessages, { ...config, verbose: config.verbose || parsed.debug, judgeBackend: parsed.judge }, pi, ctx, controller, {
|
|
908
1309
|
onEvent,
|
|
909
1310
|
onDiagnostic,
|
|
910
1311
|
onInput: async (prompt, signal) => {
|