deepclause-pi 0.2.0 → 0.3.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/diagram/viewer.d.ts +6 -0
- package/dist/diagram/viewer.js +5 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +323 -18
- package/dist/planner.d.ts +27 -2
- package/dist/planner.js +109 -4
- package/dist/runtime.d.ts +15 -1
- package/dist/runtime.js +116 -3
- package/dist/workspace.d.ts +3 -0
- package/dist/workspace.js +20 -0
- package/docs/SPECKIT.md +222 -0
- package/docs/SPEC_LAYER_PROPOSAL.md +1893 -0
- package/package.json +1 -1
- 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 +991 -0
- package/src/diagram/viewer.ts +11 -0
- package/src/index.ts +351 -16
- package/src/planner.ts +123 -3
- package/src/runtime.ts +117 -2
- package/src/workspace.ts +24 -0
package/src/diagram/viewer.ts
CHANGED
|
@@ -108,6 +108,17 @@ export async function buildViewer(options: BuildViewerOptions): Promise<ViewerBu
|
|
|
108
108
|
return { viewerPath, diagramsDir: diagrams, entries };
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
export async function buildEntriesViewer(options: {
|
|
112
|
+
cwd: string;
|
|
113
|
+
templateText: string;
|
|
114
|
+
vendorAssetPath: string;
|
|
115
|
+
entries: DiagramEntry[];
|
|
116
|
+
}): Promise<ViewerBuildResult> {
|
|
117
|
+
const { diagrams, vendorFile } = await ensureDiagramDir(options.cwd, options.vendorAssetPath);
|
|
118
|
+
const viewerPath = await renderViewer(diagrams, options.entries, options.templateText, vendorFile);
|
|
119
|
+
return { viewerPath, diagramsDir: diagrams, entries: options.entries };
|
|
120
|
+
}
|
|
121
|
+
|
|
111
122
|
export async function writeSidecar(diagramsDir: string, name: string, grade: DiagramGrade, code: string): Promise<string> {
|
|
112
123
|
const file = path.join(diagramsDir, `${name}.${grade}.mmd`);
|
|
113
124
|
await writeFile(file, code.endsWith("\n") ? code : `${code}\n`, "utf8");
|
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";
|
|
@@ -11,9 +11,11 @@ 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
|
|
|
@@ -55,12 +62,16 @@ export interface ParsedRun {
|
|
|
55
62
|
interface ParsedPlan {
|
|
56
63
|
request: string;
|
|
57
64
|
name?: string;
|
|
65
|
+
change?: string;
|
|
66
|
+
update?: boolean;
|
|
58
67
|
debug: boolean;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
interface PlanningTransaction {
|
|
62
71
|
snapshot: PlanningSnapshot;
|
|
63
72
|
nameOverride?: string;
|
|
73
|
+
change?: string;
|
|
74
|
+
update?: boolean;
|
|
64
75
|
committed: boolean;
|
|
65
76
|
startedAt: number;
|
|
66
77
|
}
|
|
@@ -145,19 +156,31 @@ export function parseRun(input: string): ParsedRun {
|
|
|
145
156
|
export function parsePlan(input: string): ParsedPlan {
|
|
146
157
|
const tokens = splitArguments(input);
|
|
147
158
|
let name: string | undefined;
|
|
159
|
+
let change: string | undefined;
|
|
160
|
+
let update = false;
|
|
148
161
|
let debug = false;
|
|
149
162
|
const requestParts: string[] = [];
|
|
150
163
|
for (let index = 0; index < tokens.length; index++) {
|
|
151
164
|
const token = tokens[index]!;
|
|
152
165
|
if (token === "--debug" || token === "-d") debug = true;
|
|
166
|
+
else if (token === "--update") update = true;
|
|
153
167
|
else if (token.startsWith("--name=")) name = token.slice("--name=".length);
|
|
154
168
|
else if (token === "--name") name = tokens[++index];
|
|
169
|
+
else if (token.startsWith("--change=")) change = token.slice("--change=".length);
|
|
170
|
+
else if (token === "--change") change = tokens[++index];
|
|
155
171
|
else requestParts.push(token);
|
|
156
172
|
}
|
|
173
|
+
// allow the leading "update" keyword form: /dc-plan update --change=<slug> <request>
|
|
174
|
+
if (change && requestParts[0] === "update") {
|
|
175
|
+
update = true;
|
|
176
|
+
requestParts.shift();
|
|
177
|
+
}
|
|
157
178
|
const request = requestParts.join(" ").trim();
|
|
158
|
-
if (!request) throw new Error("Usage: /dc-plan <request> [--name=slug] [--debug]");
|
|
179
|
+
if (!request) throw new Error("Usage: /dc-plan <request> [--name=slug] [--change=slug] [--update] [--debug]");
|
|
159
180
|
if (name !== undefined && !name.trim()) throw new Error("--name requires a non-empty slug");
|
|
160
|
-
|
|
181
|
+
if (change !== undefined && !change.trim()) throw new Error("--change requires a non-empty slug");
|
|
182
|
+
if (update && !change) throw new Error("--update requires --change=<slug>");
|
|
183
|
+
return { request, name: name?.trim(), change: change?.trim(), update, debug };
|
|
161
184
|
}
|
|
162
185
|
|
|
163
186
|
function messageText(message: unknown): string {
|
|
@@ -194,6 +217,56 @@ function eventSummary(event: DMLEvent, debug: boolean): string {
|
|
|
194
217
|
}
|
|
195
218
|
}
|
|
196
219
|
|
|
220
|
+
async function isMutatingSpecSkill(filePath: string): Promise<boolean> {
|
|
221
|
+
try {
|
|
222
|
+
return /^%\s*Mutating:\s*true\s*$/m.test(await readFile(filePath, "utf8"));
|
|
223
|
+
} catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* After a step that leaves the tree dirty, offer to commit it (or remind the user).
|
|
230
|
+
* A clean tree is what lets the next /dc-apply take a rollback snapshot.
|
|
231
|
+
*/
|
|
232
|
+
export async function offerCommit(
|
|
233
|
+
pi: ExtensionAPI,
|
|
234
|
+
ctx: ExtensionContext,
|
|
235
|
+
action: string,
|
|
236
|
+
change: string,
|
|
237
|
+
): Promise<void> {
|
|
238
|
+
let status;
|
|
239
|
+
try {
|
|
240
|
+
status = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
|
|
241
|
+
} catch {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (status.code !== 0) return; // not a repository: nothing to say
|
|
245
|
+
const files = status.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
246
|
+
if (files.length === 0) return; // clean
|
|
247
|
+
|
|
248
|
+
const message = `${action}: ${change}`;
|
|
249
|
+
const listed = files.slice(0, 12).join("\n");
|
|
250
|
+
const more = files.length > 12 ? `\n… and ${files.length - 12} more` : "";
|
|
251
|
+
const reminder = `${files.length} changed file(s):\n${listed}${more}\n\ngit add -A && git commit -m "${message}"`;
|
|
252
|
+
|
|
253
|
+
if (!ctx.hasUI) {
|
|
254
|
+
ctx.ui.notify(`Uncommitted changes. ${reminder}`, "warning");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (!await ctx.ui.confirm("Commit these changes?", `${reminder}\n\nCommit now?`)) {
|
|
258
|
+
ctx.ui.notify(`Remember to commit before continuing. ${reminder}`, "warning");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
await pi.exec("git", ["add", "-A"], { cwd: ctx.cwd });
|
|
262
|
+
const commit = await pi.exec("git", ["commit", "-m", message], { cwd: ctx.cwd });
|
|
263
|
+
if (commit.code === 0) {
|
|
264
|
+
ctx.ui.notify(`Committed: ${message}`, "info");
|
|
265
|
+
} else {
|
|
266
|
+
ctx.ui.notify(`Commit failed: ${commit.stderr.trim() || "see git output"}`, "error");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
197
270
|
async function listDmlFiles(directory: string, prefix = ""): Promise<string[]> {
|
|
198
271
|
let entries;
|
|
199
272
|
try {
|
|
@@ -232,6 +305,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
232
305
|
let activeDescription: string | undefined;
|
|
233
306
|
let modelToolRegistered = false;
|
|
234
307
|
let diagramToolRegistered = false;
|
|
308
|
+
let specGraphToolRegistered = false;
|
|
235
309
|
let planCommitRegistered = false;
|
|
236
310
|
let planningTransaction: PlanningTransaction | undefined;
|
|
237
311
|
let pendingAgentStep: PendingAgentStep | undefined;
|
|
@@ -260,6 +334,8 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
260
334
|
requiredTools: Type.Array(Type.String()),
|
|
261
335
|
relevantSkills: Type.Array(Type.String()),
|
|
262
336
|
expectedResult: Type.String(),
|
|
337
|
+
satisfies: Type.Optional(Type.Array(Type.String())),
|
|
338
|
+
checks: Type.Optional(Type.Array(Type.String())),
|
|
263
339
|
}), { minItems: 1, maxItems: 12 }),
|
|
264
340
|
finalSynthesis: Type.Optional(Type.String()),
|
|
265
341
|
failureMessage: Type.String(),
|
|
@@ -280,14 +356,18 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
280
356
|
}
|
|
281
357
|
|
|
282
358
|
try {
|
|
283
|
-
const plan = validatePlanSpec(params, transaction.snapshot, transaction.nameOverride
|
|
359
|
+
const plan = validatePlanSpec(params, transaction.snapshot, transaction.nameOverride, {
|
|
360
|
+
requireChecks: Boolean(transaction.change),
|
|
361
|
+
change: transaction.change,
|
|
362
|
+
});
|
|
284
363
|
const preview = [
|
|
285
364
|
plan.spec.title,
|
|
286
365
|
`Objective: ${plan.spec.objective}`,
|
|
366
|
+
transaction.change ? `Change: ${transaction.change}` : "",
|
|
287
367
|
`Steps: ${plan.spec.steps.length}`,
|
|
288
368
|
`Pi tools: ${plan.requiredTools.join(", ") || "none"}`,
|
|
289
|
-
...plan.spec.steps.map((step, index) => `${index + 1}. [${step.executor}] ${step.title}`),
|
|
290
|
-
].join("\n");
|
|
369
|
+
...plan.spec.steps.map((step, index) => `${index + 1}. [${step.executor}] ${step.title}${step.checks.length ? ` (${step.checks.length} checks)` : ""}`),
|
|
370
|
+
].filter(Boolean).join("\n");
|
|
291
371
|
if (!ctx.hasUI || !await ctx.ui.confirm("Create executable DeepClause plan?", preview)) {
|
|
292
372
|
return {
|
|
293
373
|
content: [{ type: "text", text: "Plan creation was not approved." }],
|
|
@@ -296,15 +376,25 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
296
376
|
}
|
|
297
377
|
|
|
298
378
|
const paths = await initializeWorkspace(ctx.cwd);
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
379
|
+
const content = transaction.change
|
|
380
|
+
? assembleTasksDml(plan, transaction.snapshot)
|
|
381
|
+
: assemblePlanDml(plan, transaction.snapshot);
|
|
382
|
+
if (transaction.change) await validateGeneratedTasks(content);
|
|
383
|
+
else await validateGeneratedPlan(content);
|
|
384
|
+
const filePath = transaction.change
|
|
385
|
+
? await writeChangeTasks(paths, normalizePlanSlug(transaction.change), content, Boolean(transaction.update))
|
|
386
|
+
: await writePlanNonDestructively(paths, plan.spec.slug, content);
|
|
302
387
|
transaction.committed = true;
|
|
303
388
|
setPlanCommitActive(false);
|
|
389
|
+
await offerCommit(pi, ctx, "plan", transaction.change ? normalizePlanSlug(transaction.change) : plan.spec.slug);
|
|
304
390
|
const relativePath = path.relative(paths.root, filePath).split(path.sep).join("/");
|
|
305
391
|
const text = [
|
|
306
|
-
|
|
307
|
-
|
|
392
|
+
transaction.change
|
|
393
|
+
? `Created change plan: .pi/deepclause/${relativePath}`
|
|
394
|
+
: `Created executable DML plan: .pi/deepclause/${relativePath}`,
|
|
395
|
+
transaction.change
|
|
396
|
+
? `Next: /dc-check ${normalizePlanSlug(transaction.change)}`
|
|
397
|
+
: `Run it with: /dc-run ${relativePath}`,
|
|
308
398
|
plan.warnings.length ? `Warnings:\n${plan.warnings.join("\n")}` : "",
|
|
309
399
|
].filter(Boolean).join("\n\n");
|
|
310
400
|
return {
|
|
@@ -312,6 +402,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
312
402
|
details: {
|
|
313
403
|
success: true,
|
|
314
404
|
path: relativePath,
|
|
405
|
+
change: transaction.change,
|
|
315
406
|
contextual: plan.spec.steps.some((step) => step.executor === "pi"),
|
|
316
407
|
requiredTools: plan.requiredTools,
|
|
317
408
|
warnings: plan.warnings,
|
|
@@ -659,10 +750,103 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
659
750
|
}
|
|
660
751
|
};
|
|
661
752
|
|
|
753
|
+
const setSpecGraphActive = () => {
|
|
754
|
+
if (!specGraphToolRegistered) {
|
|
755
|
+
pi.registerTool({
|
|
756
|
+
name: DC_SPEC_GRAPH_TOOL,
|
|
757
|
+
label: "Spec Graph",
|
|
758
|
+
description: "Create a Mermaid graph of DeepClause capabilities, requirements, scenarios and changes, write a viewer under .pi/deepclause/diagrams/, and open it.",
|
|
759
|
+
promptSnippet: "Create a capability/change graph from DeepClause spec facts",
|
|
760
|
+
promptGuidelines: [
|
|
761
|
+
"Call dc_spec_graph when the user asks for a graph or visual of capabilities, requirements, changes, or spec coverage.",
|
|
762
|
+
],
|
|
763
|
+
parameters: Type.Object({
|
|
764
|
+
view: Type.Optional(StringEnum(["capabilities", "changes"] as const)),
|
|
765
|
+
}),
|
|
766
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
767
|
+
try {
|
|
768
|
+
const view = params.view ?? "capabilities";
|
|
769
|
+
const mermaid = await runSpecSkill(ctx, "spec_graph", [view]);
|
|
770
|
+
const name = `spec-${view}`;
|
|
771
|
+
const entry: DiagramEntry = {
|
|
772
|
+
name,
|
|
773
|
+
path: `specs (${view})`,
|
|
774
|
+
flow: mermaid,
|
|
775
|
+
seq: "",
|
|
776
|
+
dml: "",
|
|
777
|
+
presentation: mermaid,
|
|
778
|
+
specification: null,
|
|
779
|
+
};
|
|
780
|
+
const build = await buildEntriesViewer({
|
|
781
|
+
cwd: ctx.cwd,
|
|
782
|
+
templateText: await bundledViewerTemplate(),
|
|
783
|
+
vendorAssetPath: viewerVendorAssetPath(),
|
|
784
|
+
entries: [entry],
|
|
785
|
+
});
|
|
786
|
+
const opened = ctx.hasUI
|
|
787
|
+
? await openViewerInBrowser(pi, build.viewerPath, name, "presentation")
|
|
788
|
+
: false;
|
|
789
|
+
const viewer = displayPath(ctx.cwd, build.viewerPath);
|
|
790
|
+
return {
|
|
791
|
+
content: [{ type: "text", text: `Created spec graph (${view}). Viewer: ${viewer}${opened ? " (opened in your browser)" : ""}` }],
|
|
792
|
+
details: { success: true, view, viewer, opened },
|
|
793
|
+
};
|
|
794
|
+
} catch (error) {
|
|
795
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
796
|
+
return { content: [{ type: "text", text: `dc_spec_graph failed: ${message}` }], details: { success: false, error: message } };
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
specGraphToolRegistered = true;
|
|
801
|
+
}
|
|
802
|
+
const activeTools = pi.getActiveTools();
|
|
803
|
+
if (!activeTools.includes(DC_SPEC_GRAPH_TOOL)) {
|
|
804
|
+
pi.setActiveTools([...activeTools, DC_SPEC_GRAPH_TOOL]);
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
const runSpecSkill = async (
|
|
809
|
+
ctx: ExtensionContext,
|
|
810
|
+
skill: string,
|
|
811
|
+
args: string[] = [],
|
|
812
|
+
options: { verifyCommands?: string[]; piAgentStep?: boolean; changeJsonPath?: string } = {},
|
|
813
|
+
): Promise<string> => {
|
|
814
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
815
|
+
const config = await loadConfig(paths.config);
|
|
816
|
+
const filePath = await resolveDmlPath(paths, skill);
|
|
817
|
+
const controller = new AbortController();
|
|
818
|
+
activeController = controller;
|
|
819
|
+
activeDescription = `running ${skill}`;
|
|
820
|
+
try {
|
|
821
|
+
const result = await executeDml(
|
|
822
|
+
filePath,
|
|
823
|
+
args,
|
|
824
|
+
[],
|
|
825
|
+
config,
|
|
826
|
+
pi,
|
|
827
|
+
ctx,
|
|
828
|
+
controller,
|
|
829
|
+
{
|
|
830
|
+
onEvent() {},
|
|
831
|
+
onInput: async () => { throw new Error("spec skills do not request input"); },
|
|
832
|
+
},
|
|
833
|
+
options.piAgentStep ? (request, signal) => runPiAgentStep(request, signal, ctx) : undefined,
|
|
834
|
+
options.verifyCommands ?? [],
|
|
835
|
+
options.changeJsonPath,
|
|
836
|
+
);
|
|
837
|
+
if (result.errors.length) throw new Error(result.errors.join("\n"));
|
|
838
|
+
return result.answer ?? "(no answer)";
|
|
839
|
+
} finally {
|
|
840
|
+
activeController = undefined;
|
|
841
|
+
activeDescription = undefined;
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
|
|
662
845
|
pi.on("session_start", async (_event, ctx) => {
|
|
663
846
|
const config = await loadConfig(getPaths(ctx.cwd).config);
|
|
664
847
|
setModelToolActive(config.modelToolEnabled);
|
|
665
848
|
setDiagramToolActive();
|
|
849
|
+
setSpecGraphActive();
|
|
666
850
|
});
|
|
667
851
|
|
|
668
852
|
pi.on("tool_execution_start", (event) => {
|
|
@@ -748,7 +932,10 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
748
932
|
"it writes the viewer under .pi/deepclause/diagrams/ and opens it.",
|
|
749
933
|
"Commands:",
|
|
750
934
|
" /dc-list",
|
|
751
|
-
" /dc-plan <request> [--name=slug] create
|
|
935
|
+
" /dc-plan <request> [--change=slug] [--update] [--name=slug] create or regenerate a plan",
|
|
936
|
+
" /dc-check <change|spec> validate specs and deltas deterministically",
|
|
937
|
+
" /dc-archive <change> merge a change delta into specs/ and archive it",
|
|
938
|
+
" /dc-apply <change> [--abort] execute tasks.dml; --abort discards an interrupted apply",
|
|
752
939
|
" /dc-run <skill|path> [args] [--context=turn|branch|isolated]",
|
|
753
940
|
" /dc-run <skill|path> --verbose show lifecycle events",
|
|
754
941
|
" /dc-run <skill|path> --debug show full event payloads and SDK diagnostics",
|
|
@@ -770,6 +957,16 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
770
957
|
const parsed = parsePlan(rawArgs);
|
|
771
958
|
if (!ctx.model) throw new Error("Select a pi model before creating a plan");
|
|
772
959
|
const paths = await initializeWorkspace(ctx.cwd);
|
|
960
|
+
if (parsed.change && !parsed.update) {
|
|
961
|
+
const changeSlug = normalizePlanSlug(parsed.change);
|
|
962
|
+
try {
|
|
963
|
+
await access(path.join(paths.changes, changeSlug, "tasks.dml"));
|
|
964
|
+
ctx.ui.notify(`changes/${changeSlug}/tasks.dml already exists. Re-run with --update to regenerate it, or edit tasks.dml directly.`, "error");
|
|
965
|
+
return;
|
|
966
|
+
} catch {
|
|
967
|
+
// no existing plan: proceed
|
|
968
|
+
}
|
|
969
|
+
}
|
|
773
970
|
const promptOptions = ctx.getSystemPromptOptions();
|
|
774
971
|
const snapshot: PlanningSnapshot = {
|
|
775
972
|
model: `${ctx.model.provider}/${ctx.model.id}`,
|
|
@@ -784,12 +981,21 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
784
981
|
planningTransaction = {
|
|
785
982
|
snapshot,
|
|
786
983
|
nameOverride: parsed.name,
|
|
984
|
+
change: parsed.change,
|
|
985
|
+
update: parsed.update,
|
|
787
986
|
committed: false,
|
|
788
987
|
startedAt: Date.now(),
|
|
789
988
|
};
|
|
790
989
|
setPlanCommitActive(true);
|
|
791
|
-
ctx.ui.notify(
|
|
792
|
-
|
|
990
|
+
ctx.ui.notify(
|
|
991
|
+
parsed.change
|
|
992
|
+
? parsed.update
|
|
993
|
+
? `Regenerating the change plan for '${parsed.change}'. Existing artifacts are read first and tasks.dml statuses reset to pending.`
|
|
994
|
+
: `Starting a change planning turn for '${parsed.change}'. Review the delta and plan before it is written.`
|
|
995
|
+
: "Starting a contextual pi planning turn. Review the generated plan before it is written.",
|
|
996
|
+
"info",
|
|
997
|
+
);
|
|
998
|
+
pi.sendUserMessage(buildPlanningPrompt(parsed.request, snapshot, parsed.name, parsed.change, parsed.update));
|
|
793
999
|
} catch (error) {
|
|
794
1000
|
planningTransaction = undefined;
|
|
795
1001
|
setPlanCommitActive(false);
|
|
@@ -854,6 +1060,131 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
854
1060
|
},
|
|
855
1061
|
});
|
|
856
1062
|
|
|
1063
|
+
pi.registerCommand("dc-check", {
|
|
1064
|
+
description: "Validate DeepClause specs and change deltas deterministically (no model calls)",
|
|
1065
|
+
handler: async (_rawArgs, ctx) => {
|
|
1066
|
+
if (activeController || !ctx.isIdle()) {
|
|
1067
|
+
ctx.ui.notify("DeepClause or pi is already active; wait before running /dc-check", "warning");
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
try {
|
|
1071
|
+
const answer = await runSpecSkill(ctx, "spec_validate");
|
|
1072
|
+
publishResult(pi, answer, { skill: "spec_validate" });
|
|
1073
|
+
ctx.ui.notify(answer.startsWith("spec check: OK") ? "Spec check passed" : "Spec check reported errors", answer.startsWith("spec check: OK") ? "info" : "warning");
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1076
|
+
publishResult(pi, `Spec check failed: ${message}`, { error: message });
|
|
1077
|
+
ctx.ui.notify(message, "error");
|
|
1078
|
+
}
|
|
1079
|
+
},
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
pi.registerCommand("dc-archive", {
|
|
1083
|
+
description: "Merge a change delta into specs/ after review, then move the change into changes/archive/",
|
|
1084
|
+
handler: async (rawArgs, ctx) => {
|
|
1085
|
+
if (activeController || !ctx.isIdle()) {
|
|
1086
|
+
ctx.ui.notify("DeepClause or pi is already active; wait before running /dc-archive", "warning");
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const change = rawArgs.trim();
|
|
1090
|
+
if (!change) {
|
|
1091
|
+
ctx.ui.notify("Usage: /dc-archive <change>", "warning");
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
try {
|
|
1095
|
+
const plan = await runSpecSkill(ctx, "spec_merge", [change]);
|
|
1096
|
+
if (!ctx.hasUI || !await ctx.ui.confirm("Archive change into specs?", plan)) {
|
|
1097
|
+
ctx.ui.notify("Archive cancelled", "warning");
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
const applied = await runSpecSkill(ctx, "spec_archive", [change]);
|
|
1101
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
1102
|
+
const from = path.join(paths.changes, change);
|
|
1103
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
1104
|
+
await mkdir(path.join(paths.changes, "archive"), { recursive: true });
|
|
1105
|
+
let target = path.join(paths.changes, "archive", `${stamp}-${change}`);
|
|
1106
|
+
try {
|
|
1107
|
+
await access(target);
|
|
1108
|
+
target = `${target}-2`;
|
|
1109
|
+
} catch {
|
|
1110
|
+
// target is free
|
|
1111
|
+
}
|
|
1112
|
+
await rename(from, target);
|
|
1113
|
+
const archivedTo = path.relative(ctx.cwd, target).split(path.sep).join("/");
|
|
1114
|
+
publishResult(pi, `${applied}\n\n moved to ${archivedTo}`, { skill: "spec_archive", change, archivedTo });
|
|
1115
|
+
ctx.ui.notify(`Archived ${change}`, "info");
|
|
1116
|
+
await offerCommit(pi, ctx, "archive", change);
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1119
|
+
publishResult(pi, `Archive failed: ${message}`, { error: message });
|
|
1120
|
+
ctx.ui.notify(message, "error");
|
|
1121
|
+
}
|
|
1122
|
+
},
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
pi.registerCommand("dc-apply", {
|
|
1126
|
+
description: "Execute a change's tasks.dml with per-task verification and bounded retries",
|
|
1127
|
+
handler: async (rawArgs, ctx) => {
|
|
1128
|
+
if (activeController || !ctx.isIdle()) {
|
|
1129
|
+
ctx.ui.notify("DeepClause or pi is already active; wait before running /dc-apply", "warning");
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const tokens = splitArguments(rawArgs);
|
|
1133
|
+
const abort = tokens.includes("--abort");
|
|
1134
|
+
const change = tokens.filter((token) => token !== "--abort").join(" ").trim();
|
|
1135
|
+
if (!change) {
|
|
1136
|
+
ctx.ui.notify("Usage: /dc-apply <change> [--abort]", "warning");
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
try {
|
|
1140
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
1141
|
+
const changeJson = path.join(paths.changes, change, "change.json");
|
|
1142
|
+
|
|
1143
|
+
if (abort) {
|
|
1144
|
+
const restored = await gitRestore(pi, ctx.cwd, changeJson).catch(() => null);
|
|
1145
|
+
const message = restored
|
|
1146
|
+
? `Discarded the apply and restored the working tree to ${restored}.`
|
|
1147
|
+
: "No recorded apply snapshot to discard.";
|
|
1148
|
+
publishResult(pi, message, { skill: "spec_apply", change, aborted: Boolean(restored) });
|
|
1149
|
+
ctx.ui.notify(message, restored ? "warning" : "info");
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
let started = false;
|
|
1154
|
+
let succeeded = false;
|
|
1155
|
+
try {
|
|
1156
|
+
const plan = await runSpecSkill(ctx, "spec_apply", [change, "plan"]);
|
|
1157
|
+
const commands = [...new Set([...plan.matchAll(/^command:\s*(.+)$/gm)].map((match) => match[1]!.trim()))];
|
|
1158
|
+
const preview = [plan, "", `Approved verification commands: ${commands.join(", ") || "none"}`].join("\n");
|
|
1159
|
+
if (!ctx.hasUI || !await ctx.ui.confirm("Apply change tasks?", preview)) {
|
|
1160
|
+
ctx.ui.notify("Apply cancelled", "warning");
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
started = true;
|
|
1164
|
+
const answer = await runSpecSkill(ctx, "spec_apply", [change, "apply"], { verifyCommands: commands, piAgentStep: true, changeJsonPath: changeJson });
|
|
1165
|
+
succeeded = answer.includes("status: OK");
|
|
1166
|
+
publishResult(pi, answer, { skill: "spec_apply", change });
|
|
1167
|
+
ctx.ui.notify(
|
|
1168
|
+
succeeded ? `Applied ${change}` : `Apply incomplete for ${change}`,
|
|
1169
|
+
succeeded ? "info" : "warning",
|
|
1170
|
+
);
|
|
1171
|
+
} finally {
|
|
1172
|
+
if (started && !succeeded) {
|
|
1173
|
+
ctx.ui.notify(
|
|
1174
|
+
`Apply interrupted; the working tree and task statuses were preserved. Resume with /dc-apply ${change}, or discard with /dc-apply ${change} --abort.`,
|
|
1175
|
+
"warning",
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (succeeded) await offerCommit(pi, ctx, "apply", change);
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1182
|
+
publishResult(pi, `Apply failed: ${message}`, { error: message });
|
|
1183
|
+
ctx.ui.notify(message, "error");
|
|
1184
|
+
}
|
|
1185
|
+
},
|
|
1186
|
+
});
|
|
1187
|
+
|
|
857
1188
|
pi.registerCommand("dc-run", {
|
|
858
1189
|
description: "Run a DML skill with pi's active model",
|
|
859
1190
|
handler: async (rawArgs, ctx) => {
|
|
@@ -868,6 +1199,10 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
|
|
|
868
1199
|
const paths = await initializeWorkspace(ctx.cwd);
|
|
869
1200
|
const config = await loadConfig(paths.config);
|
|
870
1201
|
const filePath = await resolveDmlPath(paths, parsed.target);
|
|
1202
|
+
if (await isMutatingSpecSkill(filePath)) {
|
|
1203
|
+
ctx.ui.notify(`${parsed.target} modifies specs/. Use /dc-archive <change> so you can review the merge first.`, "warning");
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
871
1206
|
const contextualPlan = await isContextualPlan(filePath);
|
|
872
1207
|
if (contextualPlan) {
|
|
873
1208
|
const requiredTools = await readPlanRequiredTools(filePath);
|