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/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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 type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -6,14 +6,16 @@ import { StringEnum } from "@earendil-works/pi-ai";
|
|
|
6
6
|
import type { DMLEvent } from "deepclause-sdk";
|
|
7
7
|
import { Type } from "typebox";
|
|
8
8
|
import { buildInitialMessages } from "./context.js";
|
|
9
|
-
import { loadConfig, setModelToolEnabled, type ContextMode } from "./config.js";
|
|
9
|
+
import { loadConfig, setJudgeConfig, setModelToolEnabled, type ContextMode, type DeepClauseConfig } from "./config.js";
|
|
10
10
|
import { renderDml, renderSequence } from "./diagram/extract.js";
|
|
11
11
|
import { polishDiagram, resolveGrade, type DiagramGrade } from "./diagram/grade.js";
|
|
12
12
|
import { findChrome, validateMermaid, type MermaidView } from "./diagram/validate.js";
|
|
13
13
|
import {
|
|
14
|
+
buildEntriesViewer,
|
|
14
15
|
buildViewer,
|
|
15
16
|
openViewerInBrowser,
|
|
16
17
|
writeSidecar,
|
|
18
|
+
type DiagramEntry,
|
|
17
19
|
} from "./diagram/viewer.js";
|
|
18
20
|
import {
|
|
19
21
|
collectDiagramTargets,
|
|
@@ -23,24 +25,29 @@ import {
|
|
|
23
25
|
resolveDiagramSource,
|
|
24
26
|
} from "./diagram/workspace.js";
|
|
25
27
|
import { completeTextWithPiModel } from "./model.js";
|
|
26
|
-
import { executeDml } from "./runtime.js";
|
|
28
|
+
import { executeDml, gitRestore } from "./runtime.js";
|
|
27
29
|
import { getPaths, initializeWorkspace, resolveDmlPath } from "./workspace.js";
|
|
28
30
|
import {
|
|
29
31
|
assemblePlanDml,
|
|
32
|
+
assembleTasksDml,
|
|
30
33
|
buildPlanningPrompt,
|
|
31
34
|
DC_PLAN_COMMIT_TOOL,
|
|
32
35
|
isContextualPlan,
|
|
36
|
+
normalizePlanSlug,
|
|
33
37
|
PI_AGENT_STEP_TOOL,
|
|
34
38
|
readPlanRequiredTools,
|
|
35
39
|
validateGeneratedPlan,
|
|
40
|
+
validateGeneratedTasks,
|
|
36
41
|
validatePlanSpec,
|
|
42
|
+
writeChangeTasks,
|
|
37
43
|
writePlanNonDestructively,
|
|
38
44
|
type PlanningSnapshot,
|
|
39
45
|
} from "./planner.js";
|
|
40
46
|
|
|
41
47
|
const DC_RUN_TOOL = "dc_run";
|
|
42
48
|
const DC_DIAGRAM_TOOL = "dc_diagram";
|
|
43
|
-
const
|
|
49
|
+
const DC_SPEC_GRAPH_TOOL = "dc_spec_graph";
|
|
50
|
+
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.`;
|
|
44
51
|
const STATUS_KEY = "deepclause";
|
|
45
52
|
const WIDGET_KEY = "deepclause-stream";
|
|
46
53
|
|
|
@@ -50,17 +57,22 @@ export interface ParsedRun {
|
|
|
50
57
|
contextMode?: ContextMode;
|
|
51
58
|
verbose: boolean;
|
|
52
59
|
debug: boolean;
|
|
60
|
+
judge?: string;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
interface ParsedPlan {
|
|
56
64
|
request: string;
|
|
57
65
|
name?: string;
|
|
66
|
+
change?: string;
|
|
67
|
+
update?: boolean;
|
|
58
68
|
debug: boolean;
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
interface PlanningTransaction {
|
|
62
72
|
snapshot: PlanningSnapshot;
|
|
63
73
|
nameOverride?: string;
|
|
74
|
+
change?: string;
|
|
75
|
+
update?: boolean;
|
|
64
76
|
committed: boolean;
|
|
65
77
|
startedAt: number;
|
|
66
78
|
}
|
|
@@ -114,11 +126,13 @@ export function parseRun(input: string): ParsedRun {
|
|
|
114
126
|
let contextMode: ContextMode | undefined;
|
|
115
127
|
let verbose = false;
|
|
116
128
|
let debug = false;
|
|
129
|
+
let judge: string | undefined;
|
|
117
130
|
const args: string[] = [];
|
|
118
131
|
|
|
119
132
|
for (let index = 0; index < tokens.length; index++) {
|
|
120
133
|
const token = tokens[index]!;
|
|
121
134
|
const contextValue = token.startsWith("--context=") ? token.slice("--context=".length) : undefined;
|
|
135
|
+
const judgeValue = token.startsWith("--judge=") ? token.slice("--judge=".length) : undefined;
|
|
122
136
|
if (token === "--verbose" || token === "-v") {
|
|
123
137
|
verbose = true;
|
|
124
138
|
} else if (token === "--debug" || token === "-d") {
|
|
@@ -135,29 +149,48 @@ export function parseRun(input: string): ParsedRun {
|
|
|
135
149
|
throw new Error("--context must be turn, branch, or isolated");
|
|
136
150
|
}
|
|
137
151
|
contextMode = value;
|
|
152
|
+
} else if (judgeValue !== undefined) {
|
|
153
|
+
if (!judgeValue.trim()) throw new Error("--judge requires a non-empty backend name");
|
|
154
|
+
judge = judgeValue.trim();
|
|
155
|
+
} else if (token === "--judge") {
|
|
156
|
+
const value = tokens[++index];
|
|
157
|
+
if (!value || !value.trim()) throw new Error("--judge requires a non-empty backend name");
|
|
158
|
+
judge = value.trim();
|
|
138
159
|
} else {
|
|
139
160
|
args.push(token);
|
|
140
161
|
}
|
|
141
162
|
}
|
|
142
|
-
return { target, args, contextMode, verbose, debug };
|
|
163
|
+
return { target, args, contextMode, verbose, debug, judge };
|
|
143
164
|
}
|
|
144
165
|
|
|
145
166
|
export function parsePlan(input: string): ParsedPlan {
|
|
146
167
|
const tokens = splitArguments(input);
|
|
147
168
|
let name: string | undefined;
|
|
169
|
+
let change: string | undefined;
|
|
170
|
+
let update = false;
|
|
148
171
|
let debug = false;
|
|
149
172
|
const requestParts: string[] = [];
|
|
150
173
|
for (let index = 0; index < tokens.length; index++) {
|
|
151
174
|
const token = tokens[index]!;
|
|
152
175
|
if (token === "--debug" || token === "-d") debug = true;
|
|
176
|
+
else if (token === "--update") update = true;
|
|
153
177
|
else if (token.startsWith("--name=")) name = token.slice("--name=".length);
|
|
154
178
|
else if (token === "--name") name = tokens[++index];
|
|
179
|
+
else if (token.startsWith("--change=")) change = token.slice("--change=".length);
|
|
180
|
+
else if (token === "--change") change = tokens[++index];
|
|
155
181
|
else requestParts.push(token);
|
|
156
182
|
}
|
|
183
|
+
// allow the leading "update" keyword form: /dc-plan update --change=<slug> <request>
|
|
184
|
+
if (change && requestParts[0] === "update") {
|
|
185
|
+
update = true;
|
|
186
|
+
requestParts.shift();
|
|
187
|
+
}
|
|
157
188
|
const request = requestParts.join(" ").trim();
|
|
158
|
-
if (!request) throw new Error("Usage: /dc-plan <request> [--name=slug] [--debug]");
|
|
189
|
+
if (!request) throw new Error("Usage: /dc-plan <request> [--name=slug] [--change=slug] [--update] [--debug]");
|
|
159
190
|
if (name !== undefined && !name.trim()) throw new Error("--name requires a non-empty slug");
|
|
160
|
-
|
|
191
|
+
if (change !== undefined && !change.trim()) throw new Error("--change requires a non-empty slug");
|
|
192
|
+
if (update && !change) throw new Error("--update requires --change=<slug>");
|
|
193
|
+
return { request, name: name?.trim(), change: change?.trim(), update, debug };
|
|
161
194
|
}
|
|
162
195
|
|
|
163
196
|
function messageText(message: unknown): string {
|
|
@@ -194,6 +227,56 @@ function eventSummary(event: DMLEvent, debug: boolean): string {
|
|
|
194
227
|
}
|
|
195
228
|
}
|
|
196
229
|
|
|
230
|
+
async function isMutatingSpecSkill(filePath: string): Promise<boolean> {
|
|
231
|
+
try {
|
|
232
|
+
return /^%\s*Mutating:\s*true\s*$/m.test(await readFile(filePath, "utf8"));
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* After a step that leaves the tree dirty, offer to commit it (or remind the user).
|
|
240
|
+
* A clean tree is what lets the next /dc-apply take a rollback snapshot.
|
|
241
|
+
*/
|
|
242
|
+
export async function offerCommit(
|
|
243
|
+
pi: ExtensionAPI,
|
|
244
|
+
ctx: ExtensionContext,
|
|
245
|
+
action: string,
|
|
246
|
+
change: string,
|
|
247
|
+
): Promise<void> {
|
|
248
|
+
let status;
|
|
249
|
+
try {
|
|
250
|
+
status = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
|
|
251
|
+
} catch {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (status.code !== 0) return; // not a repository: nothing to say
|
|
255
|
+
const files = status.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
256
|
+
if (files.length === 0) return; // clean
|
|
257
|
+
|
|
258
|
+
const message = `${action}: ${change}`;
|
|
259
|
+
const listed = files.slice(0, 12).join("\n");
|
|
260
|
+
const more = files.length > 12 ? `\n… and ${files.length - 12} more` : "";
|
|
261
|
+
const reminder = `${files.length} changed file(s):\n${listed}${more}\n\ngit add -A && git commit -m "${message}"`;
|
|
262
|
+
|
|
263
|
+
if (!ctx.hasUI) {
|
|
264
|
+
ctx.ui.notify(`Uncommitted changes. ${reminder}`, "warning");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (!await ctx.ui.confirm("Commit these changes?", `${reminder}\n\nCommit now?`)) {
|
|
268
|
+
ctx.ui.notify(`Remember to commit before continuing. ${reminder}`, "warning");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
await pi.exec("git", ["add", "-A"], { cwd: ctx.cwd });
|
|
272
|
+
const commit = await pi.exec("git", ["commit", "-m", message], { cwd: ctx.cwd });
|
|
273
|
+
if (commit.code === 0) {
|
|
274
|
+
ctx.ui.notify(`Committed: ${message}`, "info");
|
|
275
|
+
} else {
|
|
276
|
+
ctx.ui.notify(`Commit failed: ${commit.stderr.trim() || "see git output"}`, "error");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
197
280
|
async function listDmlFiles(directory: string, prefix = ""): Promise<string[]> {
|
|
198
281
|
let entries;
|
|
199
282
|
try {
|
|
@@ -215,6 +298,18 @@ function modelLabel(ctx: ExtensionCommandContext): string {
|
|
|
215
298
|
return ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "none selected";
|
|
216
299
|
}
|
|
217
300
|
|
|
301
|
+
function judgeBackendsEnabled(config: DeepClauseConfig): string {
|
|
302
|
+
return config.judgment.jev.enabled ? ", jev" : "";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function jevStatus(config: DeepClauseConfig): string {
|
|
306
|
+
const jev = config.judgment.jev;
|
|
307
|
+
if (!jev.enabled) return "disabled";
|
|
308
|
+
return process.env[jev.apiKeyEnv]
|
|
309
|
+
? `enabled (${jev.model})`
|
|
310
|
+
: `enabled but ${jev.apiKeyEnv} is not set`;
|
|
311
|
+
}
|
|
312
|
+
|
|
218
313
|
async function bundledViewerTemplate(): Promise<string> {
|
|
219
314
|
return readFile(fileURLToPath(new URL("./assets/viewer.template.html", import.meta.url)), "utf8");
|
|
220
315
|
}
|
|
@@ -232,6 +327,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
232
327
|
let activeDescription: string | undefined;
|
|
233
328
|
let modelToolRegistered = false;
|
|
234
329
|
let diagramToolRegistered = false;
|
|
330
|
+
let specGraphToolRegistered = false;
|
|
235
331
|
let planCommitRegistered = false;
|
|
236
332
|
let planningTransaction: PlanningTransaction | undefined;
|
|
237
333
|
let pendingAgentStep: PendingAgentStep | undefined;
|
|
@@ -260,6 +356,8 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
260
356
|
requiredTools: Type.Array(Type.String()),
|
|
261
357
|
relevantSkills: Type.Array(Type.String()),
|
|
262
358
|
expectedResult: Type.String(),
|
|
359
|
+
satisfies: Type.Optional(Type.Array(Type.String())),
|
|
360
|
+
checks: Type.Optional(Type.Array(Type.String())),
|
|
263
361
|
}), { minItems: 1, maxItems: 12 }),
|
|
264
362
|
finalSynthesis: Type.Optional(Type.String()),
|
|
265
363
|
failureMessage: Type.String(),
|
|
@@ -280,14 +378,18 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
280
378
|
}
|
|
281
379
|
|
|
282
380
|
try {
|
|
283
|
-
const plan = validatePlanSpec(params, transaction.snapshot, transaction.nameOverride
|
|
381
|
+
const plan = validatePlanSpec(params, transaction.snapshot, transaction.nameOverride, {
|
|
382
|
+
requireChecks: Boolean(transaction.change),
|
|
383
|
+
change: transaction.change,
|
|
384
|
+
});
|
|
284
385
|
const preview = [
|
|
285
386
|
plan.spec.title,
|
|
286
387
|
`Objective: ${plan.spec.objective}`,
|
|
388
|
+
transaction.change ? `Change: ${transaction.change}` : "",
|
|
287
389
|
`Steps: ${plan.spec.steps.length}`,
|
|
288
390
|
`Pi tools: ${plan.requiredTools.join(", ") || "none"}`,
|
|
289
|
-
...plan.spec.steps.map((step, index) => `${index + 1}. [${step.executor}] ${step.title}`),
|
|
290
|
-
].join("\n");
|
|
391
|
+
...plan.spec.steps.map((step, index) => `${index + 1}. [${step.executor}] ${step.title}${step.checks.length ? ` (${step.checks.length} checks)` : ""}`),
|
|
392
|
+
].filter(Boolean).join("\n");
|
|
291
393
|
if (!ctx.hasUI || !await ctx.ui.confirm("Create executable DeepClause plan?", preview)) {
|
|
292
394
|
return {
|
|
293
395
|
content: [{ type: "text", text: "Plan creation was not approved." }],
|
|
@@ -296,15 +398,25 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
296
398
|
}
|
|
297
399
|
|
|
298
400
|
const paths = await initializeWorkspace(ctx.cwd);
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
401
|
+
const content = transaction.change
|
|
402
|
+
? assembleTasksDml(plan, transaction.snapshot)
|
|
403
|
+
: assemblePlanDml(plan, transaction.snapshot);
|
|
404
|
+
if (transaction.change) await validateGeneratedTasks(content);
|
|
405
|
+
else await validateGeneratedPlan(content);
|
|
406
|
+
const filePath = transaction.change
|
|
407
|
+
? await writeChangeTasks(paths, normalizePlanSlug(transaction.change), content, Boolean(transaction.update))
|
|
408
|
+
: await writePlanNonDestructively(paths, plan.spec.slug, content);
|
|
302
409
|
transaction.committed = true;
|
|
303
410
|
setPlanCommitActive(false);
|
|
411
|
+
await offerCommit(pi, ctx, "plan", transaction.change ? normalizePlanSlug(transaction.change) : plan.spec.slug);
|
|
304
412
|
const relativePath = path.relative(paths.root, filePath).split(path.sep).join("/");
|
|
305
413
|
const text = [
|
|
306
|
-
|
|
307
|
-
|
|
414
|
+
transaction.change
|
|
415
|
+
? `Created change plan: .pi/deepclause/${relativePath}`
|
|
416
|
+
: `Created executable DML plan: .pi/deepclause/${relativePath}`,
|
|
417
|
+
transaction.change
|
|
418
|
+
? `Next: /dc-check ${normalizePlanSlug(transaction.change)}`
|
|
419
|
+
: `Run it with: /dc-run ${relativePath}`,
|
|
308
420
|
plan.warnings.length ? `Warnings:\n${plan.warnings.join("\n")}` : "",
|
|
309
421
|
].filter(Boolean).join("\n\n");
|
|
310
422
|
return {
|
|
@@ -312,6 +424,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
312
424
|
details: {
|
|
313
425
|
success: true,
|
|
314
426
|
path: relativePath,
|
|
427
|
+
change: transaction.change,
|
|
315
428
|
contextual: plan.spec.steps.some((step) => step.executor === "pi"),
|
|
316
429
|
requiredTools: plan.requiredTools,
|
|
317
430
|
warnings: plan.warnings,
|
|
@@ -659,10 +772,103 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
659
772
|
}
|
|
660
773
|
};
|
|
661
774
|
|
|
775
|
+
const setSpecGraphActive = () => {
|
|
776
|
+
if (!specGraphToolRegistered) {
|
|
777
|
+
pi.registerTool({
|
|
778
|
+
name: DC_SPEC_GRAPH_TOOL,
|
|
779
|
+
label: "Spec Graph",
|
|
780
|
+
description: "Create a Mermaid graph of DeepClause capabilities, requirements, scenarios and changes, write a viewer under .pi/deepclause/diagrams/, and open it.",
|
|
781
|
+
promptSnippet: "Create a capability/change graph from DeepClause spec facts",
|
|
782
|
+
promptGuidelines: [
|
|
783
|
+
"Call dc_spec_graph when the user asks for a graph or visual of capabilities, requirements, changes, or spec coverage.",
|
|
784
|
+
],
|
|
785
|
+
parameters: Type.Object({
|
|
786
|
+
view: Type.Optional(StringEnum(["capabilities", "changes"] as const)),
|
|
787
|
+
}),
|
|
788
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
789
|
+
try {
|
|
790
|
+
const view = params.view ?? "capabilities";
|
|
791
|
+
const mermaid = await runSpecSkill(ctx, "spec_graph", [view]);
|
|
792
|
+
const name = `spec-${view}`;
|
|
793
|
+
const entry: DiagramEntry = {
|
|
794
|
+
name,
|
|
795
|
+
path: `specs (${view})`,
|
|
796
|
+
flow: mermaid,
|
|
797
|
+
seq: "",
|
|
798
|
+
dml: "",
|
|
799
|
+
presentation: mermaid,
|
|
800
|
+
specification: null,
|
|
801
|
+
};
|
|
802
|
+
const build = await buildEntriesViewer({
|
|
803
|
+
cwd: ctx.cwd,
|
|
804
|
+
templateText: await bundledViewerTemplate(),
|
|
805
|
+
vendorAssetPath: viewerVendorAssetPath(),
|
|
806
|
+
entries: [entry],
|
|
807
|
+
});
|
|
808
|
+
const opened = ctx.hasUI
|
|
809
|
+
? await openViewerInBrowser(pi, build.viewerPath, name, "presentation")
|
|
810
|
+
: false;
|
|
811
|
+
const viewer = displayPath(ctx.cwd, build.viewerPath);
|
|
812
|
+
return {
|
|
813
|
+
content: [{ type: "text", text: `Created spec graph (${view}). Viewer: ${viewer}${opened ? " (opened in your browser)" : ""}` }],
|
|
814
|
+
details: { success: true, view, viewer, viewerPath: build.viewerPath, opened },
|
|
815
|
+
};
|
|
816
|
+
} catch (error) {
|
|
817
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
818
|
+
return { content: [{ type: "text", text: `dc_spec_graph failed: ${message}` }], details: { success: false, error: message } };
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
});
|
|
822
|
+
specGraphToolRegistered = true;
|
|
823
|
+
}
|
|
824
|
+
const activeTools = pi.getActiveTools();
|
|
825
|
+
if (!activeTools.includes(DC_SPEC_GRAPH_TOOL)) {
|
|
826
|
+
pi.setActiveTools([...activeTools, DC_SPEC_GRAPH_TOOL]);
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
const runSpecSkill = async (
|
|
831
|
+
ctx: ExtensionContext,
|
|
832
|
+
skill: string,
|
|
833
|
+
args: string[] = [],
|
|
834
|
+
options: { verifyCommands?: string[]; piAgentStep?: boolean; changeJsonPath?: string } = {},
|
|
835
|
+
): Promise<string> => {
|
|
836
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
837
|
+
const config = await loadConfig(paths.config);
|
|
838
|
+
const filePath = await resolveDmlPath(paths, skill);
|
|
839
|
+
const controller = new AbortController();
|
|
840
|
+
activeController = controller;
|
|
841
|
+
activeDescription = `running ${skill}`;
|
|
842
|
+
try {
|
|
843
|
+
const result = await executeDml(
|
|
844
|
+
filePath,
|
|
845
|
+
args,
|
|
846
|
+
[],
|
|
847
|
+
config,
|
|
848
|
+
pi,
|
|
849
|
+
ctx,
|
|
850
|
+
controller,
|
|
851
|
+
{
|
|
852
|
+
onEvent() {},
|
|
853
|
+
onInput: async () => { throw new Error("spec skills do not request input"); },
|
|
854
|
+
},
|
|
855
|
+
options.piAgentStep ? (request, signal) => runPiAgentStep(request, signal, ctx) : undefined,
|
|
856
|
+
options.verifyCommands ?? [],
|
|
857
|
+
options.changeJsonPath,
|
|
858
|
+
);
|
|
859
|
+
if (result.errors.length) throw new Error(result.errors.join("\n"));
|
|
860
|
+
return result.answer ?? "(no answer)";
|
|
861
|
+
} finally {
|
|
862
|
+
activeController = undefined;
|
|
863
|
+
activeDescription = undefined;
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
|
|
662
867
|
pi.on("session_start", async (_event, ctx) => {
|
|
663
868
|
const config = await loadConfig(getPaths(ctx.cwd).config);
|
|
664
869
|
setModelToolActive(config.modelToolEnabled);
|
|
665
870
|
setDiagramToolActive();
|
|
871
|
+
setSpecGraphActive();
|
|
666
872
|
});
|
|
667
873
|
|
|
668
874
|
pi.on("tool_execution_start", (event) => {
|
|
@@ -742,17 +948,25 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
742
948
|
`Skills: ${path.relative(ctx.cwd, paths.skills)}`,
|
|
743
949
|
`Plans: ${path.relative(ctx.cwd, paths.plans)}`,
|
|
744
950
|
`Context: ${config.contextMode} (verbose default: ${config.verbose})`,
|
|
951
|
+
`Judgment: ${config.judgment.default} (backends: llm${judgeBackendsEnabled(config)})`,
|
|
952
|
+
`Jev: ${jevStatus(config)}`,
|
|
745
953
|
`Model tool (${DC_RUN_TOOL}): ${config.modelToolEnabled && pi.getActiveTools().includes(DC_RUN_TOOL) ? "enabled" : "disabled"}`,
|
|
746
954
|
`Model tool (${DC_DIAGRAM_TOOL}): ${pi.getActiveTools().includes(DC_DIAGRAM_TOOL) ? "enabled" : "disabled"}`,
|
|
747
955
|
"Ask pi for a presentation-grade or specification-grade diagram of any .dml file;",
|
|
748
956
|
"it writes the viewer under .pi/deepclause/diagrams/ and opens it.",
|
|
749
957
|
"Commands:",
|
|
750
958
|
" /dc-list",
|
|
751
|
-
" /dc-plan <request> [--name=slug] create
|
|
752
|
-
" /dc-
|
|
959
|
+
" /dc-plan <request> [--change=slug] [--update] [--name=slug] create or regenerate a plan",
|
|
960
|
+
" /dc-check <change|spec> validate specs and deltas deterministically",
|
|
961
|
+
" /dc-archive <change> merge a change delta into specs/ and archive it",
|
|
962
|
+
" /dc-apply <change> [--abort] execute tasks.dml; --abort discards an interrupted apply",
|
|
963
|
+
" /dc-run <skill|path> [args] [--context=turn|branch|isolated] [--judge=llm|jev]",
|
|
753
964
|
" /dc-run <skill|path> --verbose show lifecycle events",
|
|
754
965
|
" /dc-run <skill|path> --debug show full event payloads and SDK diagnostics",
|
|
755
966
|
" /dc-tool enable|disable|status control the model-callable dc_run tool",
|
|
967
|
+
" /dc-judge [enable|disable|status] select the judgment backend (llm|jev)",
|
|
968
|
+
" /dc-judge default llm|jev set the default judgment backend",
|
|
969
|
+
" /dc-judge model <name> | key-env <ENV_VAR>",
|
|
756
970
|
" /dc-cancel",
|
|
757
971
|
].join("\n");
|
|
758
972
|
ctx.ui.notify(message, "info");
|
|
@@ -770,6 +984,16 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
770
984
|
const parsed = parsePlan(rawArgs);
|
|
771
985
|
if (!ctx.model) throw new Error("Select a pi model before creating a plan");
|
|
772
986
|
const paths = await initializeWorkspace(ctx.cwd);
|
|
987
|
+
if (parsed.change && !parsed.update) {
|
|
988
|
+
const changeSlug = normalizePlanSlug(parsed.change);
|
|
989
|
+
try {
|
|
990
|
+
await access(path.join(paths.changes, changeSlug, "tasks.dml"));
|
|
991
|
+
ctx.ui.notify(`changes/${changeSlug}/tasks.dml already exists. Re-run with --update to regenerate it, or edit tasks.dml directly.`, "error");
|
|
992
|
+
return;
|
|
993
|
+
} catch {
|
|
994
|
+
// no existing plan: proceed
|
|
995
|
+
}
|
|
996
|
+
}
|
|
773
997
|
const promptOptions = ctx.getSystemPromptOptions();
|
|
774
998
|
const snapshot: PlanningSnapshot = {
|
|
775
999
|
model: `${ctx.model.provider}/${ctx.model.id}`,
|
|
@@ -784,12 +1008,21 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
784
1008
|
planningTransaction = {
|
|
785
1009
|
snapshot,
|
|
786
1010
|
nameOverride: parsed.name,
|
|
1011
|
+
change: parsed.change,
|
|
1012
|
+
update: parsed.update,
|
|
787
1013
|
committed: false,
|
|
788
1014
|
startedAt: Date.now(),
|
|
789
1015
|
};
|
|
790
1016
|
setPlanCommitActive(true);
|
|
791
|
-
ctx.ui.notify(
|
|
792
|
-
|
|
1017
|
+
ctx.ui.notify(
|
|
1018
|
+
parsed.change
|
|
1019
|
+
? parsed.update
|
|
1020
|
+
? `Regenerating the change plan for '${parsed.change}'. Existing artifacts are read first and tasks.dml statuses reset to pending.`
|
|
1021
|
+
: `Starting a change planning turn for '${parsed.change}'. Review the delta and plan before it is written.`
|
|
1022
|
+
: "Starting a contextual pi planning turn. Review the generated plan before it is written.",
|
|
1023
|
+
"info",
|
|
1024
|
+
);
|
|
1025
|
+
pi.sendUserMessage(buildPlanningPrompt(parsed.request, snapshot, parsed.name, parsed.change, parsed.update));
|
|
793
1026
|
} catch (error) {
|
|
794
1027
|
planningTransaction = undefined;
|
|
795
1028
|
setPlanCommitActive(false);
|
|
@@ -826,6 +1059,78 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
826
1059
|
},
|
|
827
1060
|
});
|
|
828
1061
|
|
|
1062
|
+
pi.registerCommand("dc-judge", {
|
|
1063
|
+
description: "Enable, disable, or select the semantic judgment backend (llm|jev)",
|
|
1064
|
+
handler: async (rawArgs, ctx) => {
|
|
1065
|
+
const tokens = rawArgs.trim().split(/\s+/).filter(Boolean);
|
|
1066
|
+
const action = (tokens[0] ?? "status").toLowerCase();
|
|
1067
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
1068
|
+
|
|
1069
|
+
const describe = (config: DeepClauseConfig): string => {
|
|
1070
|
+
const jev = config.judgment.jev;
|
|
1071
|
+
const keyPresent = Boolean(process.env[jev.apiKeyEnv]);
|
|
1072
|
+
return [
|
|
1073
|
+
`judgment backend: ${config.judgment.default}`,
|
|
1074
|
+
`registered backends: llm${jev.enabled ? ", jev" : ""}`,
|
|
1075
|
+
`jev: ${jev.enabled ? "enabled" : "disabled"} | model=${jev.model} | ${jev.apiKeyEnv}=${keyPresent ? "set" : "not set"}`,
|
|
1076
|
+
].join("\n");
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
try {
|
|
1080
|
+
if (action === "status") {
|
|
1081
|
+
ctx.ui.notify(describe(await loadConfig(paths.config)), "info");
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (action === "enable" || action === "on" || action === "disable" || action === "off") {
|
|
1085
|
+
const enabled = action === "enable" || action === "on";
|
|
1086
|
+
const config = await setJudgeConfig(paths.config, { jev: { enabled } });
|
|
1087
|
+
const jev = config.judgment.jev;
|
|
1088
|
+
const note = enabled && !process.env[jev.apiKeyEnv]
|
|
1089
|
+
? `\n${jev.apiKeyEnv} is not set; export it before using --judge=jev.`
|
|
1090
|
+
: "";
|
|
1091
|
+
ctx.ui.notify(`Jev backend ${enabled ? "enabled" : "disabled"} for this workspace.${note}`, enabled ? "warning" : "info");
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (action === "default") {
|
|
1095
|
+
const name = tokens[1];
|
|
1096
|
+
if (name !== "llm" && name !== "jev") {
|
|
1097
|
+
ctx.ui.notify("Usage: /dc-judge default llm|jev", "warning");
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
await setJudgeConfig(paths.config, { default: name });
|
|
1101
|
+
ctx.ui.notify(`Default judgment backend set to '${name}'`, "info");
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (action === "model") {
|
|
1105
|
+
const model = tokens[1];
|
|
1106
|
+
if (!model) {
|
|
1107
|
+
ctx.ui.notify("Usage: /dc-judge model <name>", "warning");
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
await setJudgeConfig(paths.config, { jev: { model } });
|
|
1111
|
+
ctx.ui.notify(`Jev model set to '${model}'`, "info");
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (action === "key-env") {
|
|
1115
|
+
const envName = tokens[1];
|
|
1116
|
+
if (!envName) {
|
|
1117
|
+
ctx.ui.notify("Usage: /dc-judge key-env <ENV_VAR>", "warning");
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
await setJudgeConfig(paths.config, { jev: { apiKeyEnv: envName } });
|
|
1121
|
+
ctx.ui.notify(`Jev API key environment variable set to '${envName}'`, "info");
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
ctx.ui.notify(
|
|
1125
|
+
"Usage: /dc-judge [enable|disable|status|default llm|jev|model <name>|key-env <ENV_VAR>]",
|
|
1126
|
+
"warning",
|
|
1127
|
+
);
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
});
|
|
1133
|
+
|
|
829
1134
|
pi.registerCommand("dc-list", {
|
|
830
1135
|
description: "List DeepClause DML skills and generated plans",
|
|
831
1136
|
handler: async (_args, ctx) => {
|
|
@@ -854,6 +1159,131 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
854
1159
|
},
|
|
855
1160
|
});
|
|
856
1161
|
|
|
1162
|
+
pi.registerCommand("dc-check", {
|
|
1163
|
+
description: "Validate DeepClause specs and change deltas deterministically (no model calls)",
|
|
1164
|
+
handler: async (_rawArgs, ctx) => {
|
|
1165
|
+
if (activeController || !ctx.isIdle()) {
|
|
1166
|
+
ctx.ui.notify("DeepClause or pi is already active; wait before running /dc-check", "warning");
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
try {
|
|
1170
|
+
const answer = await runSpecSkill(ctx, "spec_validate");
|
|
1171
|
+
publishResult(pi, answer, { skill: "spec_validate" });
|
|
1172
|
+
ctx.ui.notify(answer.startsWith("spec check: OK") ? "Spec check passed" : "Spec check reported errors", answer.startsWith("spec check: OK") ? "info" : "warning");
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1175
|
+
publishResult(pi, `Spec check failed: ${message}`, { error: message });
|
|
1176
|
+
ctx.ui.notify(message, "error");
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
pi.registerCommand("dc-archive", {
|
|
1182
|
+
description: "Merge a change delta into specs/ after review, then move the change into changes/archive/",
|
|
1183
|
+
handler: async (rawArgs, ctx) => {
|
|
1184
|
+
if (activeController || !ctx.isIdle()) {
|
|
1185
|
+
ctx.ui.notify("DeepClause or pi is already active; wait before running /dc-archive", "warning");
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const change = rawArgs.trim();
|
|
1189
|
+
if (!change) {
|
|
1190
|
+
ctx.ui.notify("Usage: /dc-archive <change>", "warning");
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
try {
|
|
1194
|
+
const plan = await runSpecSkill(ctx, "spec_merge", [change]);
|
|
1195
|
+
if (!ctx.hasUI || !await ctx.ui.confirm("Archive change into specs?", plan)) {
|
|
1196
|
+
ctx.ui.notify("Archive cancelled", "warning");
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const applied = await runSpecSkill(ctx, "spec_archive", [change]);
|
|
1200
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
1201
|
+
const from = path.join(paths.changes, change);
|
|
1202
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
1203
|
+
await mkdir(path.join(paths.changes, "archive"), { recursive: true });
|
|
1204
|
+
let target = path.join(paths.changes, "archive", `${stamp}-${change}`);
|
|
1205
|
+
try {
|
|
1206
|
+
await access(target);
|
|
1207
|
+
target = `${target}-2`;
|
|
1208
|
+
} catch {
|
|
1209
|
+
// target is free
|
|
1210
|
+
}
|
|
1211
|
+
await rename(from, target);
|
|
1212
|
+
const archivedTo = path.relative(ctx.cwd, target).split(path.sep).join("/");
|
|
1213
|
+
publishResult(pi, `${applied}\n\n moved to ${archivedTo}`, { skill: "spec_archive", change, archivedTo });
|
|
1214
|
+
ctx.ui.notify(`Archived ${change}`, "info");
|
|
1215
|
+
await offerCommit(pi, ctx, "archive", change);
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1218
|
+
publishResult(pi, `Archive failed: ${message}`, { error: message });
|
|
1219
|
+
ctx.ui.notify(message, "error");
|
|
1220
|
+
}
|
|
1221
|
+
},
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
pi.registerCommand("dc-apply", {
|
|
1225
|
+
description: "Execute a change's tasks.dml with per-task verification and bounded retries",
|
|
1226
|
+
handler: async (rawArgs, ctx) => {
|
|
1227
|
+
if (activeController || !ctx.isIdle()) {
|
|
1228
|
+
ctx.ui.notify("DeepClause or pi is already active; wait before running /dc-apply", "warning");
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
const tokens = splitArguments(rawArgs);
|
|
1232
|
+
const abort = tokens.includes("--abort");
|
|
1233
|
+
const change = tokens.filter((token) => token !== "--abort").join(" ").trim();
|
|
1234
|
+
if (!change) {
|
|
1235
|
+
ctx.ui.notify("Usage: /dc-apply <change> [--abort]", "warning");
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
try {
|
|
1239
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
1240
|
+
const changeJson = path.join(paths.changes, change, "change.json");
|
|
1241
|
+
|
|
1242
|
+
if (abort) {
|
|
1243
|
+
const restored = await gitRestore(pi, ctx.cwd, changeJson).catch(() => null);
|
|
1244
|
+
const message = restored
|
|
1245
|
+
? `Discarded the apply and restored the working tree to ${restored}.`
|
|
1246
|
+
: "No recorded apply snapshot to discard.";
|
|
1247
|
+
publishResult(pi, message, { skill: "spec_apply", change, aborted: Boolean(restored) });
|
|
1248
|
+
ctx.ui.notify(message, restored ? "warning" : "info");
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
let started = false;
|
|
1253
|
+
let succeeded = false;
|
|
1254
|
+
try {
|
|
1255
|
+
const plan = await runSpecSkill(ctx, "spec_apply", [change, "plan"]);
|
|
1256
|
+
const commands = [...new Set([...plan.matchAll(/^command:\s*(.+)$/gm)].map((match) => match[1]!.trim()))];
|
|
1257
|
+
const preview = [plan, "", `Approved verification commands: ${commands.join(", ") || "none"}`].join("\n");
|
|
1258
|
+
if (!ctx.hasUI || !await ctx.ui.confirm("Apply change tasks?", preview)) {
|
|
1259
|
+
ctx.ui.notify("Apply cancelled", "warning");
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
started = true;
|
|
1263
|
+
const answer = await runSpecSkill(ctx, "spec_apply", [change, "apply"], { verifyCommands: commands, piAgentStep: true, changeJsonPath: changeJson });
|
|
1264
|
+
succeeded = answer.includes("status: OK");
|
|
1265
|
+
publishResult(pi, answer, { skill: "spec_apply", change });
|
|
1266
|
+
ctx.ui.notify(
|
|
1267
|
+
succeeded ? `Applied ${change}` : `Apply incomplete for ${change}`,
|
|
1268
|
+
succeeded ? "info" : "warning",
|
|
1269
|
+
);
|
|
1270
|
+
} finally {
|
|
1271
|
+
if (started && !succeeded) {
|
|
1272
|
+
ctx.ui.notify(
|
|
1273
|
+
`Apply interrupted; the working tree and task statuses were preserved. Resume with /dc-apply ${change}, or discard with /dc-apply ${change} --abort.`,
|
|
1274
|
+
"warning",
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (succeeded) await offerCommit(pi, ctx, "apply", change);
|
|
1279
|
+
} catch (error) {
|
|
1280
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1281
|
+
publishResult(pi, `Apply failed: ${message}`, { error: message });
|
|
1282
|
+
ctx.ui.notify(message, "error");
|
|
1283
|
+
}
|
|
1284
|
+
},
|
|
1285
|
+
});
|
|
1286
|
+
|
|
857
1287
|
pi.registerCommand("dc-run", {
|
|
858
1288
|
description: "Run a DML skill with pi's active model",
|
|
859
1289
|
handler: async (rawArgs, ctx) => {
|
|
@@ -868,6 +1298,10 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
868
1298
|
const paths = await initializeWorkspace(ctx.cwd);
|
|
869
1299
|
const config = await loadConfig(paths.config);
|
|
870
1300
|
const filePath = await resolveDmlPath(paths, parsed.target);
|
|
1301
|
+
if (await isMutatingSpecSkill(filePath)) {
|
|
1302
|
+
ctx.ui.notify(`${parsed.target} modifies specs/. Use /dc-archive <change> so you can review the merge first.`, "warning");
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
871
1305
|
const contextualPlan = await isContextualPlan(filePath);
|
|
872
1306
|
if (contextualPlan) {
|
|
873
1307
|
const requiredTools = await readPlanRequiredTools(filePath);
|
|
@@ -967,7 +1401,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
967
1401
|
filePath,
|
|
968
1402
|
parsed.args,
|
|
969
1403
|
initialMessages,
|
|
970
|
-
{ ...config, verbose: config.verbose || parsed.debug },
|
|
1404
|
+
{ ...config, verbose: config.verbose || parsed.debug, judgeBackend: parsed.judge },
|
|
971
1405
|
pi,
|
|
972
1406
|
ctx,
|
|
973
1407
|
controller,
|