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/README.md
CHANGED
|
@@ -131,6 +131,19 @@ The resulting `.dml` file is the plan. Steps use one of two executors:
|
|
|
131
131
|
|
|
132
132
|
For each pi step, only the exact tools named in the committed plan are temporarily active. They must still be installed and active when execution begins; existing tool policies, UI, and approvals remain authoritative. DeepClause control tools cannot be requested recursively. The prior active-tool set is restored after success, failure, or cancellation.
|
|
133
133
|
|
|
134
|
+
## deepclause-pi speckit
|
|
135
|
+
|
|
136
|
+
Spec-driven changes, built on the same runtime: behaviour specs in plain Markdown, reviewable change deltas, and an executable `tasks.dml` that pi runs with deterministic validation, per-task verification, rollback and resume.
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
/dc-plan <request> --change=<slug> propose
|
|
140
|
+
/dc-check <slug> validate (0 tokens)
|
|
141
|
+
/dc-apply <slug> execute, verify, retry, resume
|
|
142
|
+
/dc-archive <slug> merge into specs/
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
See the [deepclause-pi speckit guide](docs/SPECKIT.md) for the getting-started walkthrough and reference.
|
|
146
|
+
|
|
134
147
|
## Diagrams
|
|
135
148
|
|
|
136
149
|
Ask pi for a diagram of any DML file in plain language:
|
package/dist/diagram/viewer.d.ts
CHANGED
|
@@ -25,6 +25,12 @@ export interface BuildViewerOptions {
|
|
|
25
25
|
* sidecars, and write the self-contained viewer plus per-skill Markdown.
|
|
26
26
|
*/
|
|
27
27
|
export declare function buildViewer(options: BuildViewerOptions): Promise<ViewerBuildResult>;
|
|
28
|
+
export declare function buildEntriesViewer(options: {
|
|
29
|
+
cwd: string;
|
|
30
|
+
templateText: string;
|
|
31
|
+
vendorAssetPath: string;
|
|
32
|
+
entries: DiagramEntry[];
|
|
33
|
+
}): Promise<ViewerBuildResult>;
|
|
28
34
|
export declare function writeSidecar(diagramsDir: string, name: string, grade: DiagramGrade, code: string): Promise<string>;
|
|
29
35
|
/** Open the viewer at a specific diagram and view using a fixed, OS-native argv. */
|
|
30
36
|
export declare function openViewerInBrowser(pi: Pick<ExtensionAPI, "exec">, viewerPath: string, name: string, view: string): Promise<boolean>;
|
package/dist/diagram/viewer.js
CHANGED
|
@@ -71,6 +71,11 @@ export async function buildViewer(options) {
|
|
|
71
71
|
const viewerPath = await renderViewer(diagrams, entries, options.templateText, vendorFile);
|
|
72
72
|
return { viewerPath, diagramsDir: diagrams, entries };
|
|
73
73
|
}
|
|
74
|
+
export async function buildEntriesViewer(options) {
|
|
75
|
+
const { diagrams, vendorFile } = await ensureDiagramDir(options.cwd, options.vendorAssetPath);
|
|
76
|
+
const viewerPath = await renderViewer(diagrams, options.entries, options.templateText, vendorFile);
|
|
77
|
+
return { viewerPath, diagramsDir: diagrams, entries: options.entries };
|
|
78
|
+
}
|
|
74
79
|
export async function writeSidecar(diagramsDir, name, grade, code) {
|
|
75
80
|
const file = path.join(diagramsDir, `${name}.${grade}.mmd`);
|
|
76
81
|
await writeFile(file, code.endsWith("\n") ? code : `${code}\n`, "utf8");
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type ContextMode } from "./config.js";
|
|
3
3
|
export interface ParsedRun {
|
|
4
4
|
target: string;
|
|
@@ -10,10 +10,17 @@ export interface ParsedRun {
|
|
|
10
10
|
interface ParsedPlan {
|
|
11
11
|
request: string;
|
|
12
12
|
name?: string;
|
|
13
|
+
change?: string;
|
|
14
|
+
update?: boolean;
|
|
13
15
|
debug: boolean;
|
|
14
16
|
}
|
|
15
17
|
export declare function splitArguments(input: string): string[];
|
|
16
18
|
export declare function parseRun(input: string): ParsedRun;
|
|
17
19
|
export declare function parsePlan(input: string): ParsedPlan;
|
|
20
|
+
/**
|
|
21
|
+
* After a step that leaves the tree dirty, offer to commit it (or remind the user).
|
|
22
|
+
* A clean tree is what lets the next /dc-apply take a rollback snapshot.
|
|
23
|
+
*/
|
|
24
|
+
export declare function offerCommit(pi: ExtensionAPI, ctx: ExtensionContext, action: string, change: string): Promise<void>;
|
|
18
25
|
export default function deepClauseExtension(pi: ExtensionAPI): void;
|
|
19
26
|
export {};
|
package/dist/index.js
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 { StringEnum } from "@earendil-works/pi-ai";
|
|
@@ -8,15 +8,16 @@ import { loadConfig, 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) {
|
|
@@ -100,25 +101,42 @@ export function parseRun(input) {
|
|
|
100
101
|
export function parsePlan(input) {
|
|
101
102
|
const tokens = splitArguments(input);
|
|
102
103
|
let name;
|
|
104
|
+
let change;
|
|
105
|
+
let update = false;
|
|
103
106
|
let debug = false;
|
|
104
107
|
const requestParts = [];
|
|
105
108
|
for (let index = 0; index < tokens.length; index++) {
|
|
106
109
|
const token = tokens[index];
|
|
107
110
|
if (token === "--debug" || token === "-d")
|
|
108
111
|
debug = true;
|
|
112
|
+
else if (token === "--update")
|
|
113
|
+
update = true;
|
|
109
114
|
else if (token.startsWith("--name="))
|
|
110
115
|
name = token.slice("--name=".length);
|
|
111
116
|
else if (token === "--name")
|
|
112
117
|
name = tokens[++index];
|
|
118
|
+
else if (token.startsWith("--change="))
|
|
119
|
+
change = token.slice("--change=".length);
|
|
120
|
+
else if (token === "--change")
|
|
121
|
+
change = tokens[++index];
|
|
113
122
|
else
|
|
114
123
|
requestParts.push(token);
|
|
115
124
|
}
|
|
125
|
+
// allow the leading "update" keyword form: /dc-plan update --change=<slug> <request>
|
|
126
|
+
if (change && requestParts[0] === "update") {
|
|
127
|
+
update = true;
|
|
128
|
+
requestParts.shift();
|
|
129
|
+
}
|
|
116
130
|
const request = requestParts.join(" ").trim();
|
|
117
131
|
if (!request)
|
|
118
|
-
throw new Error("Usage: /dc-plan <request> [--name=slug] [--debug]");
|
|
132
|
+
throw new Error("Usage: /dc-plan <request> [--name=slug] [--change=slug] [--update] [--debug]");
|
|
119
133
|
if (name !== undefined && !name.trim())
|
|
120
134
|
throw new Error("--name requires a non-empty slug");
|
|
121
|
-
|
|
135
|
+
if (change !== undefined && !change.trim())
|
|
136
|
+
throw new Error("--change requires a non-empty slug");
|
|
137
|
+
if (update && !change)
|
|
138
|
+
throw new Error("--update requires --change=<slug>");
|
|
139
|
+
return { request, name: name?.trim(), change: change?.trim(), update, debug };
|
|
122
140
|
}
|
|
123
141
|
function messageText(message) {
|
|
124
142
|
if (!message || typeof message !== "object")
|
|
@@ -155,6 +173,52 @@ function eventSummary(event, debug) {
|
|
|
155
173
|
case "memory_compaction": return `compaction ${event.compactionAction ?? "event"}`;
|
|
156
174
|
}
|
|
157
175
|
}
|
|
176
|
+
async function isMutatingSpecSkill(filePath) {
|
|
177
|
+
try {
|
|
178
|
+
return /^%\s*Mutating:\s*true\s*$/m.test(await readFile(filePath, "utf8"));
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* After a step that leaves the tree dirty, offer to commit it (or remind the user).
|
|
186
|
+
* A clean tree is what lets the next /dc-apply take a rollback snapshot.
|
|
187
|
+
*/
|
|
188
|
+
export async function offerCommit(pi, ctx, action, change) {
|
|
189
|
+
let status;
|
|
190
|
+
try {
|
|
191
|
+
status = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (status.code !== 0)
|
|
197
|
+
return; // not a repository: nothing to say
|
|
198
|
+
const files = status.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
199
|
+
if (files.length === 0)
|
|
200
|
+
return; // clean
|
|
201
|
+
const message = `${action}: ${change}`;
|
|
202
|
+
const listed = files.slice(0, 12).join("\n");
|
|
203
|
+
const more = files.length > 12 ? `\n… and ${files.length - 12} more` : "";
|
|
204
|
+
const reminder = `${files.length} changed file(s):\n${listed}${more}\n\ngit add -A && git commit -m "${message}"`;
|
|
205
|
+
if (!ctx.hasUI) {
|
|
206
|
+
ctx.ui.notify(`Uncommitted changes. ${reminder}`, "warning");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!await ctx.ui.confirm("Commit these changes?", `${reminder}\n\nCommit now?`)) {
|
|
210
|
+
ctx.ui.notify(`Remember to commit before continuing. ${reminder}`, "warning");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
await pi.exec("git", ["add", "-A"], { cwd: ctx.cwd });
|
|
214
|
+
const commit = await pi.exec("git", ["commit", "-m", message], { cwd: ctx.cwd });
|
|
215
|
+
if (commit.code === 0) {
|
|
216
|
+
ctx.ui.notify(`Committed: ${message}`, "info");
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
ctx.ui.notify(`Commit failed: ${commit.stderr.trim() || "see git output"}`, "error");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
158
222
|
async function listDmlFiles(directory, prefix = "") {
|
|
159
223
|
let entries;
|
|
160
224
|
try {
|
|
@@ -192,6 +256,7 @@ export default function deepClauseExtension(pi) {
|
|
|
192
256
|
let activeDescription;
|
|
193
257
|
let modelToolRegistered = false;
|
|
194
258
|
let diagramToolRegistered = false;
|
|
259
|
+
let specGraphToolRegistered = false;
|
|
195
260
|
let planCommitRegistered = false;
|
|
196
261
|
let planningTransaction;
|
|
197
262
|
let pendingAgentStep;
|
|
@@ -219,6 +284,8 @@ export default function deepClauseExtension(pi) {
|
|
|
219
284
|
requiredTools: Type.Array(Type.String()),
|
|
220
285
|
relevantSkills: Type.Array(Type.String()),
|
|
221
286
|
expectedResult: Type.String(),
|
|
287
|
+
satisfies: Type.Optional(Type.Array(Type.String())),
|
|
288
|
+
checks: Type.Optional(Type.Array(Type.String())),
|
|
222
289
|
}), { minItems: 1, maxItems: 12 }),
|
|
223
290
|
finalSynthesis: Type.Optional(Type.String()),
|
|
224
291
|
failureMessage: Type.String(),
|
|
@@ -238,14 +305,18 @@ export default function deepClauseExtension(pi) {
|
|
|
238
305
|
};
|
|
239
306
|
}
|
|
240
307
|
try {
|
|
241
|
-
const plan = validatePlanSpec(params, transaction.snapshot, transaction.nameOverride
|
|
308
|
+
const plan = validatePlanSpec(params, transaction.snapshot, transaction.nameOverride, {
|
|
309
|
+
requireChecks: Boolean(transaction.change),
|
|
310
|
+
change: transaction.change,
|
|
311
|
+
});
|
|
242
312
|
const preview = [
|
|
243
313
|
plan.spec.title,
|
|
244
314
|
`Objective: ${plan.spec.objective}`,
|
|
315
|
+
transaction.change ? `Change: ${transaction.change}` : "",
|
|
245
316
|
`Steps: ${plan.spec.steps.length}`,
|
|
246
317
|
`Pi tools: ${plan.requiredTools.join(", ") || "none"}`,
|
|
247
|
-
...plan.spec.steps.map((step, index) => `${index + 1}. [${step.executor}] ${step.title}`),
|
|
248
|
-
].join("\n");
|
|
318
|
+
...plan.spec.steps.map((step, index) => `${index + 1}. [${step.executor}] ${step.title}${step.checks.length ? ` (${step.checks.length} checks)` : ""}`),
|
|
319
|
+
].filter(Boolean).join("\n");
|
|
249
320
|
if (!ctx.hasUI || !await ctx.ui.confirm("Create executable DeepClause plan?", preview)) {
|
|
250
321
|
return {
|
|
251
322
|
content: [{ type: "text", text: "Plan creation was not approved." }],
|
|
@@ -253,15 +324,27 @@ export default function deepClauseExtension(pi) {
|
|
|
253
324
|
};
|
|
254
325
|
}
|
|
255
326
|
const paths = await initializeWorkspace(ctx.cwd);
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
327
|
+
const content = transaction.change
|
|
328
|
+
? assembleTasksDml(plan, transaction.snapshot)
|
|
329
|
+
: assemblePlanDml(plan, transaction.snapshot);
|
|
330
|
+
if (transaction.change)
|
|
331
|
+
await validateGeneratedTasks(content);
|
|
332
|
+
else
|
|
333
|
+
await validateGeneratedPlan(content);
|
|
334
|
+
const filePath = transaction.change
|
|
335
|
+
? await writeChangeTasks(paths, normalizePlanSlug(transaction.change), content, Boolean(transaction.update))
|
|
336
|
+
: await writePlanNonDestructively(paths, plan.spec.slug, content);
|
|
259
337
|
transaction.committed = true;
|
|
260
338
|
setPlanCommitActive(false);
|
|
339
|
+
await offerCommit(pi, ctx, "plan", transaction.change ? normalizePlanSlug(transaction.change) : plan.spec.slug);
|
|
261
340
|
const relativePath = path.relative(paths.root, filePath).split(path.sep).join("/");
|
|
262
341
|
const text = [
|
|
263
|
-
|
|
264
|
-
|
|
342
|
+
transaction.change
|
|
343
|
+
? `Created change plan: .pi/deepclause/${relativePath}`
|
|
344
|
+
: `Created executable DML plan: .pi/deepclause/${relativePath}`,
|
|
345
|
+
transaction.change
|
|
346
|
+
? `Next: /dc-check ${normalizePlanSlug(transaction.change)}`
|
|
347
|
+
: `Run it with: /dc-run ${relativePath}`,
|
|
265
348
|
plan.warnings.length ? `Warnings:\n${plan.warnings.join("\n")}` : "",
|
|
266
349
|
].filter(Boolean).join("\n\n");
|
|
267
350
|
return {
|
|
@@ -269,6 +352,7 @@ export default function deepClauseExtension(pi) {
|
|
|
269
352
|
details: {
|
|
270
353
|
success: true,
|
|
271
354
|
path: relativePath,
|
|
355
|
+
change: transaction.change,
|
|
272
356
|
contextual: plan.spec.steps.some((step) => step.executor === "pi"),
|
|
273
357
|
requiredTools: plan.requiredTools,
|
|
274
358
|
warnings: plan.warnings,
|
|
@@ -604,10 +688,87 @@ export default function deepClauseExtension(pi) {
|
|
|
604
688
|
pi.setActiveTools([...activeTools, DC_DIAGRAM_TOOL]);
|
|
605
689
|
}
|
|
606
690
|
};
|
|
691
|
+
const setSpecGraphActive = () => {
|
|
692
|
+
if (!specGraphToolRegistered) {
|
|
693
|
+
pi.registerTool({
|
|
694
|
+
name: DC_SPEC_GRAPH_TOOL,
|
|
695
|
+
label: "Spec Graph",
|
|
696
|
+
description: "Create a Mermaid graph of DeepClause capabilities, requirements, scenarios and changes, write a viewer under .pi/deepclause/diagrams/, and open it.",
|
|
697
|
+
promptSnippet: "Create a capability/change graph from DeepClause spec facts",
|
|
698
|
+
promptGuidelines: [
|
|
699
|
+
"Call dc_spec_graph when the user asks for a graph or visual of capabilities, requirements, changes, or spec coverage.",
|
|
700
|
+
],
|
|
701
|
+
parameters: Type.Object({
|
|
702
|
+
view: Type.Optional(StringEnum(["capabilities", "changes"])),
|
|
703
|
+
}),
|
|
704
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
705
|
+
try {
|
|
706
|
+
const view = params.view ?? "capabilities";
|
|
707
|
+
const mermaid = await runSpecSkill(ctx, "spec_graph", [view]);
|
|
708
|
+
const name = `spec-${view}`;
|
|
709
|
+
const entry = {
|
|
710
|
+
name,
|
|
711
|
+
path: `specs (${view})`,
|
|
712
|
+
flow: mermaid,
|
|
713
|
+
seq: "",
|
|
714
|
+
dml: "",
|
|
715
|
+
presentation: mermaid,
|
|
716
|
+
specification: null,
|
|
717
|
+
};
|
|
718
|
+
const build = await buildEntriesViewer({
|
|
719
|
+
cwd: ctx.cwd,
|
|
720
|
+
templateText: await bundledViewerTemplate(),
|
|
721
|
+
vendorAssetPath: viewerVendorAssetPath(),
|
|
722
|
+
entries: [entry],
|
|
723
|
+
});
|
|
724
|
+
const opened = ctx.hasUI
|
|
725
|
+
? await openViewerInBrowser(pi, build.viewerPath, name, "presentation")
|
|
726
|
+
: false;
|
|
727
|
+
const viewer = displayPath(ctx.cwd, build.viewerPath);
|
|
728
|
+
return {
|
|
729
|
+
content: [{ type: "text", text: `Created spec graph (${view}). Viewer: ${viewer}${opened ? " (opened in your browser)" : ""}` }],
|
|
730
|
+
details: { success: true, view, viewer, opened },
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
735
|
+
return { content: [{ type: "text", text: `dc_spec_graph failed: ${message}` }], details: { success: false, error: message } };
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
specGraphToolRegistered = true;
|
|
740
|
+
}
|
|
741
|
+
const activeTools = pi.getActiveTools();
|
|
742
|
+
if (!activeTools.includes(DC_SPEC_GRAPH_TOOL)) {
|
|
743
|
+
pi.setActiveTools([...activeTools, DC_SPEC_GRAPH_TOOL]);
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
const runSpecSkill = async (ctx, skill, args = [], options = {}) => {
|
|
747
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
748
|
+
const config = await loadConfig(paths.config);
|
|
749
|
+
const filePath = await resolveDmlPath(paths, skill);
|
|
750
|
+
const controller = new AbortController();
|
|
751
|
+
activeController = controller;
|
|
752
|
+
activeDescription = `running ${skill}`;
|
|
753
|
+
try {
|
|
754
|
+
const result = await executeDml(filePath, args, [], config, pi, ctx, controller, {
|
|
755
|
+
onEvent() { },
|
|
756
|
+
onInput: async () => { throw new Error("spec skills do not request input"); },
|
|
757
|
+
}, options.piAgentStep ? (request, signal) => runPiAgentStep(request, signal, ctx) : undefined, options.verifyCommands ?? [], options.changeJsonPath);
|
|
758
|
+
if (result.errors.length)
|
|
759
|
+
throw new Error(result.errors.join("\n"));
|
|
760
|
+
return result.answer ?? "(no answer)";
|
|
761
|
+
}
|
|
762
|
+
finally {
|
|
763
|
+
activeController = undefined;
|
|
764
|
+
activeDescription = undefined;
|
|
765
|
+
}
|
|
766
|
+
};
|
|
607
767
|
pi.on("session_start", async (_event, ctx) => {
|
|
608
768
|
const config = await loadConfig(getPaths(ctx.cwd).config);
|
|
609
769
|
setModelToolActive(config.modelToolEnabled);
|
|
610
770
|
setDiagramToolActive();
|
|
771
|
+
setSpecGraphActive();
|
|
611
772
|
});
|
|
612
773
|
pi.on("tool_execution_start", (event) => {
|
|
613
774
|
if (pendingAgentStep && !pendingAgentStep.toolsUsed.includes(event.toolName)) {
|
|
@@ -689,7 +850,10 @@ export default function deepClauseExtension(pi) {
|
|
|
689
850
|
"it writes the viewer under .pi/deepclause/diagrams/ and opens it.",
|
|
690
851
|
"Commands:",
|
|
691
852
|
" /dc-list",
|
|
692
|
-
" /dc-plan <request> [--name=slug] create
|
|
853
|
+
" /dc-plan <request> [--change=slug] [--update] [--name=slug] create or regenerate a plan",
|
|
854
|
+
" /dc-check <change|spec> validate specs and deltas deterministically",
|
|
855
|
+
" /dc-archive <change> merge a change delta into specs/ and archive it",
|
|
856
|
+
" /dc-apply <change> [--abort] execute tasks.dml; --abort discards an interrupted apply",
|
|
693
857
|
" /dc-run <skill|path> [args] [--context=turn|branch|isolated]",
|
|
694
858
|
" /dc-run <skill|path> --verbose show lifecycle events",
|
|
695
859
|
" /dc-run <skill|path> --debug show full event payloads and SDK diagnostics",
|
|
@@ -711,6 +875,17 @@ export default function deepClauseExtension(pi) {
|
|
|
711
875
|
if (!ctx.model)
|
|
712
876
|
throw new Error("Select a pi model before creating a plan");
|
|
713
877
|
const paths = await initializeWorkspace(ctx.cwd);
|
|
878
|
+
if (parsed.change && !parsed.update) {
|
|
879
|
+
const changeSlug = normalizePlanSlug(parsed.change);
|
|
880
|
+
try {
|
|
881
|
+
await access(path.join(paths.changes, changeSlug, "tasks.dml"));
|
|
882
|
+
ctx.ui.notify(`changes/${changeSlug}/tasks.dml already exists. Re-run with --update to regenerate it, or edit tasks.dml directly.`, "error");
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
catch {
|
|
886
|
+
// no existing plan: proceed
|
|
887
|
+
}
|
|
888
|
+
}
|
|
714
889
|
const promptOptions = ctx.getSystemPromptOptions();
|
|
715
890
|
const snapshot = {
|
|
716
891
|
model: `${ctx.model.provider}/${ctx.model.id}`,
|
|
@@ -725,12 +900,18 @@ export default function deepClauseExtension(pi) {
|
|
|
725
900
|
planningTransaction = {
|
|
726
901
|
snapshot,
|
|
727
902
|
nameOverride: parsed.name,
|
|
903
|
+
change: parsed.change,
|
|
904
|
+
update: parsed.update,
|
|
728
905
|
committed: false,
|
|
729
906
|
startedAt: Date.now(),
|
|
730
907
|
};
|
|
731
908
|
setPlanCommitActive(true);
|
|
732
|
-
ctx.ui.notify(
|
|
733
|
-
|
|
909
|
+
ctx.ui.notify(parsed.change
|
|
910
|
+
? parsed.update
|
|
911
|
+
? `Regenerating the change plan for '${parsed.change}'. Existing artifacts are read first and tasks.dml statuses reset to pending.`
|
|
912
|
+
: `Starting a change planning turn for '${parsed.change}'. Review the delta and plan before it is written.`
|
|
913
|
+
: "Starting a contextual pi planning turn. Review the generated plan before it is written.", "info");
|
|
914
|
+
pi.sendUserMessage(buildPlanningPrompt(parsed.request, snapshot, parsed.name, parsed.change, parsed.update));
|
|
734
915
|
}
|
|
735
916
|
catch (error) {
|
|
736
917
|
planningTransaction = undefined;
|
|
@@ -794,6 +975,126 @@ export default function deepClauseExtension(pi) {
|
|
|
794
975
|
ctx.ui.notify("Cancelling DeepClause execution", "warning");
|
|
795
976
|
},
|
|
796
977
|
});
|
|
978
|
+
pi.registerCommand("dc-check", {
|
|
979
|
+
description: "Validate DeepClause specs and change deltas deterministically (no model calls)",
|
|
980
|
+
handler: async (_rawArgs, ctx) => {
|
|
981
|
+
if (activeController || !ctx.isIdle()) {
|
|
982
|
+
ctx.ui.notify("DeepClause or pi is already active; wait before running /dc-check", "warning");
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
try {
|
|
986
|
+
const answer = await runSpecSkill(ctx, "spec_validate");
|
|
987
|
+
publishResult(pi, answer, { skill: "spec_validate" });
|
|
988
|
+
ctx.ui.notify(answer.startsWith("spec check: OK") ? "Spec check passed" : "Spec check reported errors", answer.startsWith("spec check: OK") ? "info" : "warning");
|
|
989
|
+
}
|
|
990
|
+
catch (error) {
|
|
991
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
992
|
+
publishResult(pi, `Spec check failed: ${message}`, { error: message });
|
|
993
|
+
ctx.ui.notify(message, "error");
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
});
|
|
997
|
+
pi.registerCommand("dc-archive", {
|
|
998
|
+
description: "Merge a change delta into specs/ after review, then move the change into changes/archive/",
|
|
999
|
+
handler: async (rawArgs, ctx) => {
|
|
1000
|
+
if (activeController || !ctx.isIdle()) {
|
|
1001
|
+
ctx.ui.notify("DeepClause or pi is already active; wait before running /dc-archive", "warning");
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const change = rawArgs.trim();
|
|
1005
|
+
if (!change) {
|
|
1006
|
+
ctx.ui.notify("Usage: /dc-archive <change>", "warning");
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
try {
|
|
1010
|
+
const plan = await runSpecSkill(ctx, "spec_merge", [change]);
|
|
1011
|
+
if (!ctx.hasUI || !await ctx.ui.confirm("Archive change into specs?", plan)) {
|
|
1012
|
+
ctx.ui.notify("Archive cancelled", "warning");
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const applied = await runSpecSkill(ctx, "spec_archive", [change]);
|
|
1016
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
1017
|
+
const from = path.join(paths.changes, change);
|
|
1018
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
1019
|
+
await mkdir(path.join(paths.changes, "archive"), { recursive: true });
|
|
1020
|
+
let target = path.join(paths.changes, "archive", `${stamp}-${change}`);
|
|
1021
|
+
try {
|
|
1022
|
+
await access(target);
|
|
1023
|
+
target = `${target}-2`;
|
|
1024
|
+
}
|
|
1025
|
+
catch {
|
|
1026
|
+
// target is free
|
|
1027
|
+
}
|
|
1028
|
+
await rename(from, target);
|
|
1029
|
+
const archivedTo = path.relative(ctx.cwd, target).split(path.sep).join("/");
|
|
1030
|
+
publishResult(pi, `${applied}\n\n moved to ${archivedTo}`, { skill: "spec_archive", change, archivedTo });
|
|
1031
|
+
ctx.ui.notify(`Archived ${change}`, "info");
|
|
1032
|
+
await offerCommit(pi, ctx, "archive", change);
|
|
1033
|
+
}
|
|
1034
|
+
catch (error) {
|
|
1035
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1036
|
+
publishResult(pi, `Archive failed: ${message}`, { error: message });
|
|
1037
|
+
ctx.ui.notify(message, "error");
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
});
|
|
1041
|
+
pi.registerCommand("dc-apply", {
|
|
1042
|
+
description: "Execute a change's tasks.dml with per-task verification and bounded retries",
|
|
1043
|
+
handler: async (rawArgs, ctx) => {
|
|
1044
|
+
if (activeController || !ctx.isIdle()) {
|
|
1045
|
+
ctx.ui.notify("DeepClause or pi is already active; wait before running /dc-apply", "warning");
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
const tokens = splitArguments(rawArgs);
|
|
1049
|
+
const abort = tokens.includes("--abort");
|
|
1050
|
+
const change = tokens.filter((token) => token !== "--abort").join(" ").trim();
|
|
1051
|
+
if (!change) {
|
|
1052
|
+
ctx.ui.notify("Usage: /dc-apply <change> [--abort]", "warning");
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
try {
|
|
1056
|
+
const paths = await initializeWorkspace(ctx.cwd);
|
|
1057
|
+
const changeJson = path.join(paths.changes, change, "change.json");
|
|
1058
|
+
if (abort) {
|
|
1059
|
+
const restored = await gitRestore(pi, ctx.cwd, changeJson).catch(() => null);
|
|
1060
|
+
const message = restored
|
|
1061
|
+
? `Discarded the apply and restored the working tree to ${restored}.`
|
|
1062
|
+
: "No recorded apply snapshot to discard.";
|
|
1063
|
+
publishResult(pi, message, { skill: "spec_apply", change, aborted: Boolean(restored) });
|
|
1064
|
+
ctx.ui.notify(message, restored ? "warning" : "info");
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
let started = false;
|
|
1068
|
+
let succeeded = false;
|
|
1069
|
+
try {
|
|
1070
|
+
const plan = await runSpecSkill(ctx, "spec_apply", [change, "plan"]);
|
|
1071
|
+
const commands = [...new Set([...plan.matchAll(/^command:\s*(.+)$/gm)].map((match) => match[1].trim()))];
|
|
1072
|
+
const preview = [plan, "", `Approved verification commands: ${commands.join(", ") || "none"}`].join("\n");
|
|
1073
|
+
if (!ctx.hasUI || !await ctx.ui.confirm("Apply change tasks?", preview)) {
|
|
1074
|
+
ctx.ui.notify("Apply cancelled", "warning");
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
started = true;
|
|
1078
|
+
const answer = await runSpecSkill(ctx, "spec_apply", [change, "apply"], { verifyCommands: commands, piAgentStep: true, changeJsonPath: changeJson });
|
|
1079
|
+
succeeded = answer.includes("status: OK");
|
|
1080
|
+
publishResult(pi, answer, { skill: "spec_apply", change });
|
|
1081
|
+
ctx.ui.notify(succeeded ? `Applied ${change}` : `Apply incomplete for ${change}`, succeeded ? "info" : "warning");
|
|
1082
|
+
}
|
|
1083
|
+
finally {
|
|
1084
|
+
if (started && !succeeded) {
|
|
1085
|
+
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");
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (succeeded)
|
|
1089
|
+
await offerCommit(pi, ctx, "apply", change);
|
|
1090
|
+
}
|
|
1091
|
+
catch (error) {
|
|
1092
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1093
|
+
publishResult(pi, `Apply failed: ${message}`, { error: message });
|
|
1094
|
+
ctx.ui.notify(message, "error");
|
|
1095
|
+
}
|
|
1096
|
+
},
|
|
1097
|
+
});
|
|
797
1098
|
pi.registerCommand("dc-run", {
|
|
798
1099
|
description: "Run a DML skill with pi's active model",
|
|
799
1100
|
handler: async (rawArgs, ctx) => {
|
|
@@ -806,6 +1107,10 @@ export default function deepClauseExtension(pi) {
|
|
|
806
1107
|
const paths = await initializeWorkspace(ctx.cwd);
|
|
807
1108
|
const config = await loadConfig(paths.config);
|
|
808
1109
|
const filePath = await resolveDmlPath(paths, parsed.target);
|
|
1110
|
+
if (await isMutatingSpecSkill(filePath)) {
|
|
1111
|
+
ctx.ui.notify(`${parsed.target} modifies specs/. Use /dc-archive <change> so you can review the merge first.`, "warning");
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
809
1114
|
const contextualPlan = await isContextualPlan(filePath);
|
|
810
1115
|
if (contextualPlan) {
|
|
811
1116
|
const requiredTools = await readPlanRequiredTools(filePath);
|
package/dist/planner.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface PlanStepSpec {
|
|
|
10
10
|
requiredTools: string[];
|
|
11
11
|
relevantSkills: string[];
|
|
12
12
|
expectedResult: string;
|
|
13
|
+
satisfies: string[];
|
|
14
|
+
checks: string[];
|
|
13
15
|
}
|
|
14
16
|
export interface PlanSpec {
|
|
15
17
|
slug: string;
|
|
@@ -19,6 +21,11 @@ export interface PlanSpec {
|
|
|
19
21
|
steps: PlanStepSpec[];
|
|
20
22
|
finalSynthesis?: string;
|
|
21
23
|
failureMessage: string;
|
|
24
|
+
change?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ValidatePlanOptions {
|
|
27
|
+
requireChecks?: boolean;
|
|
28
|
+
change?: string;
|
|
22
29
|
}
|
|
23
30
|
export interface PlanningSnapshot {
|
|
24
31
|
model: string;
|
|
@@ -36,10 +43,28 @@ export interface ValidatedPlan {
|
|
|
36
43
|
warnings: string[];
|
|
37
44
|
}
|
|
38
45
|
export declare function normalizePlanSlug(value: string): string;
|
|
39
|
-
export declare function validatePlanSpec(value: unknown, snapshot: PlanningSnapshot, nameOverride?: string): ValidatedPlan;
|
|
46
|
+
export declare function validatePlanSpec(value: unknown, snapshot: PlanningSnapshot, nameOverride?: string, options?: ValidatePlanOptions): ValidatedPlan;
|
|
47
|
+
export interface ParsedCheck {
|
|
48
|
+
kind: "cmd" | "exists" | "model";
|
|
49
|
+
value: string;
|
|
50
|
+
}
|
|
51
|
+
/** Parse an encoded verification check: cmd:<command>, exists:<path>, model:<question>. */
|
|
52
|
+
export declare function parseCheck(encoded: string): ParsedCheck;
|
|
53
|
+
/**
|
|
54
|
+
* Assemble the `tasks.dml` data artifact for a change: plan_task/2 definitions plus
|
|
55
|
+
* the managed plan_task_status/2 block. Verified by /dc-check and executed by /dc-apply.
|
|
56
|
+
*/
|
|
57
|
+
export declare function assembleTasksDml(plan: ValidatedPlan, snapshot: PlanningSnapshot): string;
|
|
58
|
+
/** Write changes/<slug>/tasks.dml, refusing to clobber an existing plan unless overwrite=true. */
|
|
59
|
+
export declare function writeChangeTasks(paths: DeepClausePaths, slug: string, text: string, overwrite?: boolean): Promise<string>;
|
|
40
60
|
export declare function assemblePlanDml(plan: ValidatedPlan, snapshot: PlanningSnapshot): string;
|
|
41
61
|
export declare function validateGeneratedPlan(dml: string): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Data-only artifact (tasks.dml): no agent_main of its own, so validation appends a
|
|
64
|
+
* trivial entry point to parse the facts without changing what is written.
|
|
65
|
+
*/
|
|
66
|
+
export declare function validateGeneratedTasks(dml: string): Promise<void>;
|
|
42
67
|
export declare function writePlanNonDestructively(paths: DeepClausePaths, slug: string, dml: string): Promise<string>;
|
|
43
68
|
export declare function isContextualPlan(filePath: string): Promise<boolean>;
|
|
44
69
|
export declare function readPlanRequiredTools(filePath: string): Promise<string[]>;
|
|
45
|
-
export declare function buildPlanningPrompt(request: string, snapshot: PlanningSnapshot, nameOverride?: string): string;
|
|
70
|
+
export declare function buildPlanningPrompt(request: string, snapshot: PlanningSnapshot, nameOverride?: string, change?: string, update?: boolean): string;
|