deepclause-pi 0.1.5 → 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 +36 -0
- package/dist/diagram/extract.d.ts +5 -0
- package/dist/diagram/extract.js +701 -0
- package/dist/diagram/grade.d.ts +41 -0
- package/dist/diagram/grade.js +70 -0
- package/dist/diagram/validate.d.ts +36 -0
- package/dist/diagram/validate.js +148 -0
- package/dist/diagram/viewer.d.ts +36 -0
- package/dist/diagram/viewer.js +99 -0
- package/dist/diagram/workspace.d.ts +24 -0
- package/dist/diagram/workspace.js +106 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +462 -17
- package/dist/model.d.ts +16 -0
- package/dist/model.js +28 -0
- 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/DIAGRAM_INTEGRATION_PROPOSAL.md +154 -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 +82 -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/assets/vendor/mermaid.min.js +3636 -0
- package/src/assets/viewer.template.html +319 -0
- package/src/diagram/extract.ts +721 -0
- package/src/diagram/grade.ts +104 -0
- package/src/diagram/validate.ts +188 -0
- package/src/diagram/viewer.ts +144 -0
- package/src/diagram/workspace.ts +109 -0
- package/src/index.ts +507 -16
- package/src/model.ts +46 -0
- package/src/planner.ts +123 -3
- package/src/runtime.ts +117 -2
- package/src/workspace.ts +24 -0
package/dist/model.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot text completion through pi's active model and credentials. Used by
|
|
3
|
+
* diagram grading; it never requests API keys or mutates provider state.
|
|
4
|
+
*/
|
|
5
|
+
export async function completeTextWithPiModel(ctx, options) {
|
|
6
|
+
const model = ctx.model;
|
|
7
|
+
if (!model)
|
|
8
|
+
throw new Error("Select a pi model before generating a diagram grade");
|
|
9
|
+
if (!ctx.modelRegistry.hasConfiguredAuth(model)) {
|
|
10
|
+
throw new Error(`Pi has no configured authentication for ${model.provider}/${model.id}`);
|
|
11
|
+
}
|
|
12
|
+
const response = await ctx.modelRegistry.complete(model, {
|
|
13
|
+
systemPrompt: options.systemPrompt,
|
|
14
|
+
messages: [{ role: "user", content: options.prompt, timestamp: Date.now() }],
|
|
15
|
+
}, {
|
|
16
|
+
signal: options.signal,
|
|
17
|
+
maxTokens: options.maxTokens,
|
|
18
|
+
cacheRetention: "none",
|
|
19
|
+
});
|
|
20
|
+
if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
21
|
+
throw new Error(response.errorMessage || `Pi model request ${response.stopReason}`);
|
|
22
|
+
}
|
|
23
|
+
const text = response.content
|
|
24
|
+
.filter((content) => content.type === "text")
|
|
25
|
+
.map((content) => content.text)
|
|
26
|
+
.join("");
|
|
27
|
+
return { text, usage: response.usage };
|
|
28
|
+
}
|
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;
|
package/dist/planner.js
CHANGED
|
@@ -31,7 +31,7 @@ export function normalizePlanSlug(value) {
|
|
|
31
31
|
throw new Error("Plan slug must contain a letter or digit");
|
|
32
32
|
return slug;
|
|
33
33
|
}
|
|
34
|
-
export function validatePlanSpec(value, snapshot, nameOverride) {
|
|
34
|
+
export function validatePlanSpec(value, snapshot, nameOverride, options = {}) {
|
|
35
35
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
36
36
|
throw new Error("Plan specification must be an object");
|
|
37
37
|
const raw = value;
|
|
@@ -48,8 +48,8 @@ export function validatePlanSpec(value, snapshot, nameOverride) {
|
|
|
48
48
|
throw new Error(`steps[${index}] must be an object`);
|
|
49
49
|
const step = entry;
|
|
50
50
|
const id = requireText(step.id ?? `step_${index + 1}`, `steps[${index}].id`, 80);
|
|
51
|
-
if (!/^[a-zA-
|
|
52
|
-
throw new Error(`steps[${index}].id must be a simple identifier`);
|
|
51
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(id))
|
|
52
|
+
throw new Error(`steps[${index}].id must be a simple identifier (letters, digits, dot, dash, underscore)`);
|
|
53
53
|
if (ids.has(id))
|
|
54
54
|
throw new Error(`Duplicate plan step id: ${id}`);
|
|
55
55
|
ids.add(id);
|
|
@@ -58,6 +58,13 @@ export function validatePlanSpec(value, snapshot, nameOverride) {
|
|
|
58
58
|
throw new Error(`steps[${index}].executor must be pi or dml`);
|
|
59
59
|
const requiredTools = stringArray(step.requiredTools, `steps[${index}].requiredTools`, 16);
|
|
60
60
|
const relevantSkills = stringArray(step.relevantSkills, `steps[${index}].relevantSkills`, 16);
|
|
61
|
+
const satisfies = stringArray(step.satisfies, `steps[${index}].satisfies`, 32);
|
|
62
|
+
const checks = stringArray(step.checks, `steps[${index}].checks`, 16);
|
|
63
|
+
for (const check of checks)
|
|
64
|
+
parseCheck(check);
|
|
65
|
+
if (options.requireChecks && checks.length === 0) {
|
|
66
|
+
throw new Error(`Plan step ${id} must declare at least one verification check (cmd:..., exists:... or model:...)`);
|
|
67
|
+
}
|
|
61
68
|
if (executor === "dml" && requiredTools.length > 0) {
|
|
62
69
|
throw new Error(`DML step ${id} cannot request pi tools; use executor=pi`);
|
|
63
70
|
}
|
|
@@ -81,6 +88,8 @@ export function validatePlanSpec(value, snapshot, nameOverride) {
|
|
|
81
88
|
requiredTools: [...new Set(requiredTools)],
|
|
82
89
|
relevantSkills: [...new Set(relevantSkills)],
|
|
83
90
|
expectedResult: requireText(step.expectedResult, `steps[${index}].expectedResult`, 1_000),
|
|
91
|
+
satisfies: [...new Set(satisfies)],
|
|
92
|
+
checks: [...new Set(checks)],
|
|
84
93
|
};
|
|
85
94
|
});
|
|
86
95
|
const spec = {
|
|
@@ -93,6 +102,7 @@ export function validatePlanSpec(value, snapshot, nameOverride) {
|
|
|
93
102
|
? requireText(raw.finalSynthesis, "finalSynthesis", 2_000)
|
|
94
103
|
: undefined,
|
|
95
104
|
failureMessage: requireText(raw.failureMessage, "failureMessage", 1_000),
|
|
105
|
+
change: options.change ? normalizePlanSlug(options.change) : undefined,
|
|
96
106
|
};
|
|
97
107
|
return {
|
|
98
108
|
spec,
|
|
@@ -109,6 +119,71 @@ function dmlStringList(values) {
|
|
|
109
119
|
function commentText(value) {
|
|
110
120
|
return value.replace(/[\r\n]+/g, " ").replace(/%/g, "percent").trim();
|
|
111
121
|
}
|
|
122
|
+
/** Parse an encoded verification check: cmd:<command>, exists:<path>, model:<question>. */
|
|
123
|
+
export function parseCheck(encoded) {
|
|
124
|
+
const separator = encoded.indexOf(":");
|
|
125
|
+
if (separator <= 0)
|
|
126
|
+
throw new Error(`Check must be cmd:..., exists:... or model:...; got '${encoded}'`);
|
|
127
|
+
const kind = encoded.slice(0, separator).trim();
|
|
128
|
+
const value = encoded.slice(separator + 1).trim();
|
|
129
|
+
if (kind !== "cmd" && kind !== "exists" && kind !== "model")
|
|
130
|
+
throw new Error(`Unknown check kind '${kind}' in '${encoded}'`);
|
|
131
|
+
if (!value)
|
|
132
|
+
throw new Error(`Check '${encoded}' has an empty value`);
|
|
133
|
+
return { kind, value };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Assemble the `tasks.dml` data artifact for a change: plan_task/2 definitions plus
|
|
137
|
+
* the managed plan_task_status/2 block. Verified by /dc-check and executed by /dc-apply.
|
|
138
|
+
*/
|
|
139
|
+
export function assembleTasksDml(plan, snapshot) {
|
|
140
|
+
const { spec } = plan;
|
|
141
|
+
const taskBlocks = spec.steps.map((step) => {
|
|
142
|
+
const checks = step.checks.map((encoded) => {
|
|
143
|
+
const { kind, value } = parseCheck(encoded);
|
|
144
|
+
return `${kind}(${dmlString(value)})`;
|
|
145
|
+
});
|
|
146
|
+
return [
|
|
147
|
+
`plan_task(${dmlString(step.id)}, task{`,
|
|
148
|
+
` executor: ${step.executor},`,
|
|
149
|
+
` do: ${dmlString(step.instruction)},`,
|
|
150
|
+
` tools: ${dmlStringList(step.requiredTools)},`,
|
|
151
|
+
` expected: ${dmlString(step.expectedResult)},`,
|
|
152
|
+
` satisfies: ${dmlStringList(step.satisfies)},`,
|
|
153
|
+
` checks: [${checks.join(", ")}]`,
|
|
154
|
+
`}).`,
|
|
155
|
+
].join("\n");
|
|
156
|
+
});
|
|
157
|
+
const statusLines = spec.steps.map((step) => `plan_task_status(${dmlString(step.id)}, pending).`);
|
|
158
|
+
const metadata = [
|
|
159
|
+
"% tasks.dml — generated by /dc-plan. Edit conservatively.",
|
|
160
|
+
`% Change: ${commentText(spec.change ?? spec.slug)}`,
|
|
161
|
+
`% Title: ${commentText(spec.title)}`,
|
|
162
|
+
`% Planning model: ${commentText(snapshot.model)}`,
|
|
163
|
+
`% Required pi tools: ${plan.requiredTools.join(", ") || "none"}`,
|
|
164
|
+
].join("\n");
|
|
165
|
+
return `${metadata}\n\n${taskBlocks.join("\n\n")}\n\n% --- execution state (managed by apply.dml; do not edit by hand) ---\n${statusLines.join("\n")}\n`;
|
|
166
|
+
}
|
|
167
|
+
/** Write changes/<slug>/tasks.dml, refusing to clobber an existing plan unless overwrite=true. */
|
|
168
|
+
export async function writeChangeTasks(paths, slug, text, overwrite = false) {
|
|
169
|
+
const dir = path.join(paths.changes, slug);
|
|
170
|
+
await mkdir(dir, { recursive: true });
|
|
171
|
+
const filePath = path.join(dir, "tasks.dml");
|
|
172
|
+
if (overwrite) {
|
|
173
|
+
await writeFile(filePath, text, { encoding: "utf8" });
|
|
174
|
+
return filePath;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
await writeFile(filePath, text, { encoding: "utf8", flag: "wx" });
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error.code === "EEXIST") {
|
|
181
|
+
throw new Error(`changes/${slug}/tasks.dml already exists; re-run with --update to regenerate it`);
|
|
182
|
+
}
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
return filePath;
|
|
186
|
+
}
|
|
112
187
|
export function assemblePlanDml(plan, snapshot) {
|
|
113
188
|
const { spec } = plan;
|
|
114
189
|
const resultVariables = [];
|
|
@@ -167,6 +242,17 @@ export async function validateGeneratedPlan(dml) {
|
|
|
167
242
|
if (!validation.valid)
|
|
168
243
|
throw new Error(`Generated DML failed validation: ${validation.errors.join("; ")}`);
|
|
169
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Data-only artifact (tasks.dml): no agent_main of its own, so validation appends a
|
|
247
|
+
* trivial entry point to parse the facts without changing what is written.
|
|
248
|
+
*/
|
|
249
|
+
export async function validateGeneratedTasks(dml) {
|
|
250
|
+
if (dml.includes(".deepclause/"))
|
|
251
|
+
throw new Error("Generated tasks may not reference .deepclause/");
|
|
252
|
+
const validation = await validateWithProlog(`${dml}\nagent_main :- true.\n`);
|
|
253
|
+
if (!validation.valid)
|
|
254
|
+
throw new Error(`Generated tasks.dml failed validation: ${validation.errors.join("; ")}`);
|
|
255
|
+
}
|
|
170
256
|
export async function writePlanNonDestructively(paths, slug, dml) {
|
|
171
257
|
await mkdir(paths.plans, { recursive: true });
|
|
172
258
|
for (let suffix = 1; suffix <= 100; suffix++) {
|
|
@@ -193,7 +279,7 @@ export async function readPlanRequiredTools(filePath) {
|
|
|
193
279
|
return [];
|
|
194
280
|
return [...new Set(match[1].split(",").map((name) => name.trim()).filter(Boolean))];
|
|
195
281
|
}
|
|
196
|
-
export function buildPlanningPrompt(request, snapshot, nameOverride) {
|
|
282
|
+
export function buildPlanningPrompt(request, snapshot, nameOverride, change, update = false) {
|
|
197
283
|
const tools = snapshot.allTools.map((tool) => ({
|
|
198
284
|
name: tool.name,
|
|
199
285
|
active: snapshot.activeTools.includes(tool.name),
|
|
@@ -202,6 +288,24 @@ export function buildPlanningPrompt(request, snapshot, nameOverride) {
|
|
|
202
288
|
guidelines: tool.promptGuidelines ?? [],
|
|
203
289
|
source: tool.sourceInfo,
|
|
204
290
|
}));
|
|
291
|
+
const changeInstructions = change
|
|
292
|
+
? [
|
|
293
|
+
update
|
|
294
|
+
? `This regenerates the change '${change}'. Read the existing .pi/deepclause/changes/${change}/proposal.md, specs/**, design.md and tasks.dml first, then revise them as needed.`
|
|
295
|
+
: `This plan is for the change '${change}'. Before committing, create .pi/deepclause/changes/${change}/ with normal file tools:`,
|
|
296
|
+
...(update
|
|
297
|
+
? []
|
|
298
|
+
: [
|
|
299
|
+
"- proposal.md — why / what / capabilities / impact.",
|
|
300
|
+
"- specs/<capability>.spec.md — delta(s) using ## ADDED|MODIFIED|REMOVED Requirements.",
|
|
301
|
+
"- design.md — optional approach and trade-offs.",
|
|
302
|
+
]),
|
|
303
|
+
"Specs describe behaviour only: no commands, file paths, library choices or implementation steps.",
|
|
304
|
+
"Use exactly three hashes for ### Requirement and four for #### Scenario; every requirement needs at least one scenario.",
|
|
305
|
+
"Each committed step must list the scenario ids it satisfies (capability#scenario-slug) and at least one check encoded as cmd:<command>, exists:<path> or model:<question>.",
|
|
306
|
+
"The committed tasks.dml replaces any previous one; every plan_task_status entry resets to pending.",
|
|
307
|
+
]
|
|
308
|
+
: [];
|
|
205
309
|
return [
|
|
206
310
|
"Create an executable DeepClause plan for the request below.",
|
|
207
311
|
"You are in a normal pi turn: inspect the workspace and use currently active tools when that materially improves the plan.",
|
|
@@ -211,6 +315,7 @@ export function buildPlanningPrompt(request, snapshot, nameOverride) {
|
|
|
211
315
|
"Choose executor='dml' for contained reasoning that needs no pi tool; requiredTools must then be empty.",
|
|
212
316
|
"Use only exact active tool names. Never request dc_run, dc_plan_commit, or pi_agent_step.",
|
|
213
317
|
"Keep steps bounded, concrete, ordered, and independently observable. Prefer 3-8 steps.",
|
|
318
|
+
...changeInstructions,
|
|
214
319
|
nameOverride ? `The user requested the plan filename slug: ${nameOverride}` : "Choose a concise lowercase slug.",
|
|
215
320
|
`User request:\n${request}`,
|
|
216
321
|
`Current model: ${snapshot.model}; thinking level: ${snapshot.thinkingLevel}`,
|
package/dist/runtime.d.ts
CHANGED
|
@@ -3,6 +3,20 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
3
3
|
import type { DeepClauseConfig } from "./config.js";
|
|
4
4
|
export declare const PI_WORKSPACE_LIST_TOOL = "pi_workspace_list";
|
|
5
5
|
export declare const PI_BASH_TOOL = "pi_bash";
|
|
6
|
+
export declare const DC_VERIFY_RUN_TOOL = "dc_verify_run";
|
|
7
|
+
export declare const DC_APPLY_SNAPSHOT_TOOL = "dc_apply_snapshot";
|
|
8
|
+
export declare const DC_APPLY_ACCEPT_TOOL = "dc_apply_accept";
|
|
9
|
+
export declare const DC_APPLY_RESTORE_TOOL = "dc_apply_restore";
|
|
10
|
+
/**
|
|
11
|
+
* Record a git snapshot ref, refusing a dirty working tree. If an apply is already
|
|
12
|
+
* in progress for this change, the existing ref is returned so the run resumes
|
|
13
|
+
* instead of starting over or rejecting the (intentionally) dirty tree.
|
|
14
|
+
*/
|
|
15
|
+
export declare function gitSnapshot(pi: Pick<ExtensionAPI, "exec">, cwd: string, changeJsonPath: string): Promise<string>;
|
|
16
|
+
/** Restore the recorded snapshot (hard reset plus clean of untracked files). */
|
|
17
|
+
export declare function gitRestore(pi: Pick<ExtensionAPI, "exec">, cwd: string, changeJsonPath: string): Promise<string | null>;
|
|
18
|
+
/** Mark the apply accepted and clear the snapshot. */
|
|
19
|
+
export declare function gitAccept(changeJsonPath: string): Promise<void>;
|
|
6
20
|
export type BashApproval = (command: string, signal: AbortSignal) => Promise<boolean>;
|
|
7
21
|
export declare function registerPiRuntimeTools(sdk: DeepClauseSDK, pi: Pick<ExtensionAPI, "exec">, cwd: string, signal: AbortSignal, approveBash?: BashApproval): void;
|
|
8
22
|
export interface ExecutionCallbacks {
|
|
@@ -27,4 +41,4 @@ export interface ExecutionResult {
|
|
|
27
41
|
errors: string[];
|
|
28
42
|
usage: LLMUsage;
|
|
29
43
|
}
|
|
30
|
-
export declare function executeDml(filePath: string, args: string[], initialMessages: MemoryMessage[], config: DeepClauseConfig, pi: ExtensionAPI, ctx: ExtensionContext, controller: AbortController, callbacks: ExecutionCallbacks, runPiAgentStep?: (request: PiAgentStepRequest, signal: AbortSignal) => Promise<PiAgentStepResult
|
|
44
|
+
export declare function executeDml(filePath: string, args: string[], initialMessages: MemoryMessage[], config: DeepClauseConfig, pi: ExtensionAPI, ctx: ExtensionContext, controller: AbortController, callbacks: ExecutionCallbacks, runPiAgentStep?: (request: PiAgentStepRequest, signal: AbortSignal) => Promise<PiAgentStepResult>, verifyCommands?: string[], applyChangeJsonPath?: string): Promise<ExecutionResult>;
|
package/dist/runtime.js
CHANGED
|
@@ -1,9 +1,71 @@
|
|
|
1
|
-
import { realpath, readFile } from "node:fs/promises";
|
|
1
|
+
import { realpath, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createDeepClause } from "deepclause-sdk";
|
|
4
4
|
import { PI_AGENT_STEP_TOOL } from "./planner.js";
|
|
5
5
|
export const PI_WORKSPACE_LIST_TOOL = "pi_workspace_list";
|
|
6
6
|
export const PI_BASH_TOOL = "pi_bash";
|
|
7
|
+
export const DC_VERIFY_RUN_TOOL = "dc_verify_run";
|
|
8
|
+
export const DC_APPLY_SNAPSHOT_TOOL = "dc_apply_snapshot";
|
|
9
|
+
export const DC_APPLY_ACCEPT_TOOL = "dc_apply_accept";
|
|
10
|
+
export const DC_APPLY_RESTORE_TOOL = "dc_apply_restore";
|
|
11
|
+
async function readChangeJson(changeJsonPath) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(await readFile(changeJsonPath, "utf8"));
|
|
14
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function patchChangeJson(changeJsonPath, patch) {
|
|
21
|
+
let existing = {};
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(await readFile(changeJsonPath, "utf8"));
|
|
24
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
25
|
+
existing = parsed;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// start a fresh manifest
|
|
29
|
+
}
|
|
30
|
+
await writeFile(changeJsonPath, `${JSON.stringify({ ...existing, ...patch }, null, 2)}\n`, "utf8");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Record a git snapshot ref, refusing a dirty working tree. If an apply is already
|
|
34
|
+
* in progress for this change, the existing ref is returned so the run resumes
|
|
35
|
+
* instead of starting over or rejecting the (intentionally) dirty tree.
|
|
36
|
+
*/
|
|
37
|
+
export async function gitSnapshot(pi, cwd, changeJsonPath) {
|
|
38
|
+
const existing = await readChangeJson(changeJsonPath);
|
|
39
|
+
const resumable = typeof existing.snapshot === "string" && existing.snapshot && existing.applyState === "in_progress";
|
|
40
|
+
if (resumable)
|
|
41
|
+
return existing.snapshot;
|
|
42
|
+
const status = await pi.exec("git", ["status", "--porcelain"], { cwd });
|
|
43
|
+
if (status.code !== 0)
|
|
44
|
+
throw new Error("git is unavailable or this is not a repository");
|
|
45
|
+
if (status.stdout.trim())
|
|
46
|
+
throw new Error("working tree is dirty; commit or stash before applying, or resume with /dc-apply (which preserves an interrupted apply)");
|
|
47
|
+
const head = await pi.exec("git", ["rev-parse", "HEAD"], { cwd });
|
|
48
|
+
if (head.code !== 0)
|
|
49
|
+
throw new Error("could not read git HEAD");
|
|
50
|
+
const ref = head.stdout.trim();
|
|
51
|
+
await patchChangeJson(changeJsonPath, { snapshot: ref, applyState: "in_progress" });
|
|
52
|
+
return ref;
|
|
53
|
+
}
|
|
54
|
+
/** Restore the recorded snapshot (hard reset plus clean of untracked files). */
|
|
55
|
+
export async function gitRestore(pi, cwd, changeJsonPath) {
|
|
56
|
+
const existing = await readChangeJson(changeJsonPath);
|
|
57
|
+
const ref = typeof existing.snapshot === "string" && existing.snapshot ? existing.snapshot : null;
|
|
58
|
+
if (!ref)
|
|
59
|
+
return null;
|
|
60
|
+
await pi.exec("git", ["reset", "--hard", ref], { cwd });
|
|
61
|
+
await pi.exec("git", ["clean", "-fd"], { cwd });
|
|
62
|
+
await patchChangeJson(changeJsonPath, { snapshot: null, applyState: "aborted" });
|
|
63
|
+
return ref;
|
|
64
|
+
}
|
|
65
|
+
/** Mark the apply accepted and clear the snapshot. */
|
|
66
|
+
export async function gitAccept(changeJsonPath) {
|
|
67
|
+
await patchChangeJson(changeJsonPath, { snapshot: null, applyState: "done" });
|
|
68
|
+
}
|
|
7
69
|
function isInside(parent, child) {
|
|
8
70
|
const relative = path.relative(parent, child);
|
|
9
71
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
@@ -198,7 +260,7 @@ function createPiBackend(ctx, maxTokens, onDiagnostic) {
|
|
|
198
260
|
},
|
|
199
261
|
};
|
|
200
262
|
}
|
|
201
|
-
export async function executeDml(filePath, args, initialMessages, config, pi, ctx, controller, callbacks, runPiAgentStep) {
|
|
263
|
+
export async function executeDml(filePath, args, initialMessages, config, pi, ctx, controller, callbacks, runPiAgentStep, verifyCommands = [], applyChangeJsonPath) {
|
|
202
264
|
const model = ctx.model;
|
|
203
265
|
if (!model)
|
|
204
266
|
throw new Error("Select a pi model before running DeepClause");
|
|
@@ -242,9 +304,60 @@ export async function executeDml(filePath, args, initialMessages, config, pi, ct
|
|
|
242
304
|
},
|
|
243
305
|
});
|
|
244
306
|
}
|
|
307
|
+
if (verifyCommands.length > 0) {
|
|
308
|
+
sdk.registerTool(DC_VERIFY_RUN_TOOL, {
|
|
309
|
+
description: "Run one of the change's pre-approved verification commands in the workspace and return its exit code, stdout and stderr.",
|
|
310
|
+
parameters: {
|
|
311
|
+
type: "object",
|
|
312
|
+
properties: {
|
|
313
|
+
command: { type: "string", description: "Exact approved command string" },
|
|
314
|
+
},
|
|
315
|
+
required: ["command"],
|
|
316
|
+
},
|
|
317
|
+
execute: async (args) => {
|
|
318
|
+
const command = typeof args.command === "string" ? args.command.trim() : "";
|
|
319
|
+
if (!verifyCommands.includes(command)) {
|
|
320
|
+
throw new Error(`verification command was not approved for this run: ${command}`);
|
|
321
|
+
}
|
|
322
|
+
const result = await pi.exec("bash", ["-lc", command], {
|
|
323
|
+
cwd: ctx.cwd,
|
|
324
|
+
signal: controller.signal,
|
|
325
|
+
timeout: 120_000,
|
|
326
|
+
});
|
|
327
|
+
return { command, stdout: result.stdout, stderr: result.stderr, exitCode: result.code, killed: result.killed };
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (applyChangeJsonPath) {
|
|
332
|
+
const changeJsonPath = applyChangeJsonPath;
|
|
333
|
+
sdk.registerTool(DC_APPLY_SNAPSHOT_TOOL, {
|
|
334
|
+
description: "Record a git snapshot of the workspace before applying tasks. Refuses a dirty working tree.",
|
|
335
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
336
|
+
execute: async () => gitSnapshot(pi, ctx.cwd, changeJsonPath),
|
|
337
|
+
});
|
|
338
|
+
sdk.registerTool(DC_APPLY_ACCEPT_TOOL, {
|
|
339
|
+
description: "Accept the apply so the working tree is not restored.",
|
|
340
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
341
|
+
execute: async () => {
|
|
342
|
+
await gitAccept(changeJsonPath);
|
|
343
|
+
return "accepted";
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
sdk.registerTool(DC_APPLY_RESTORE_TOOL, {
|
|
347
|
+
description: "Restore the recorded snapshot, discarding task changes.",
|
|
348
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
349
|
+
execute: async () => (await gitRestore(pi, ctx.cwd, changeJsonPath)) ?? "none",
|
|
350
|
+
});
|
|
351
|
+
}
|
|
245
352
|
sdk.setToolPolicy({
|
|
246
353
|
mode: "whitelist",
|
|
247
|
-
tools: [
|
|
354
|
+
tools: [
|
|
355
|
+
PI_WORKSPACE_LIST_TOOL,
|
|
356
|
+
PI_BASH_TOOL,
|
|
357
|
+
...(runPiAgentStep ? [PI_AGENT_STEP_TOOL] : []),
|
|
358
|
+
...(verifyCommands.length > 0 ? [DC_VERIFY_RUN_TOOL] : []),
|
|
359
|
+
...(applyChangeJsonPath ? [DC_APPLY_SNAPSHOT_TOOL, DC_APPLY_ACCEPT_TOOL, DC_APPLY_RESTORE_TOOL] : []),
|
|
360
|
+
],
|
|
248
361
|
});
|
|
249
362
|
const result = {
|
|
250
363
|
errors: [],
|
package/dist/workspace.d.ts
CHANGED
package/dist/workspace.js
CHANGED
|
@@ -46,6 +46,9 @@ export function getPaths(cwd) {
|
|
|
46
46
|
root,
|
|
47
47
|
skills: path.join(root, "skills"),
|
|
48
48
|
plans: path.join(root, "plans"),
|
|
49
|
+
specs: path.join(root, "specs"),
|
|
50
|
+
changes: path.join(root, "changes"),
|
|
51
|
+
lib: path.join(root, "lib"),
|
|
49
52
|
config: path.join(root, "config.json"),
|
|
50
53
|
agents: path.join(root, "AGENTS.md"),
|
|
51
54
|
reference: path.join(root, "DML_REFERENCE.md"),
|
|
@@ -68,6 +71,9 @@ async function bundledReference() {
|
|
|
68
71
|
async function bundledAuthoringGuide() {
|
|
69
72
|
return readFile(fileURLToPath(new URL("./assets/AGENTS.md", import.meta.url)), "utf8");
|
|
70
73
|
}
|
|
74
|
+
async function bundledAsset(name) {
|
|
75
|
+
return readFile(fileURLToPath(new URL(`./assets/${name}`, import.meta.url)), "utf8");
|
|
76
|
+
}
|
|
71
77
|
async function bundledDeepResearch() {
|
|
72
78
|
return readFile(fileURLToPath(new URL("./assets/deep_research.dml", import.meta.url)), "utf8");
|
|
73
79
|
}
|
|
@@ -76,6 +82,9 @@ export async function initializeWorkspace(cwd) {
|
|
|
76
82
|
await Promise.all([
|
|
77
83
|
mkdir(paths.skills, { recursive: true }),
|
|
78
84
|
mkdir(paths.plans, { recursive: true }),
|
|
85
|
+
mkdir(paths.specs, { recursive: true }),
|
|
86
|
+
mkdir(paths.changes, { recursive: true }),
|
|
87
|
+
mkdir(paths.lib, { recursive: true }),
|
|
79
88
|
]);
|
|
80
89
|
await Promise.all([
|
|
81
90
|
writeIfMissing(paths.config, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`),
|
|
@@ -83,6 +92,17 @@ export async function initializeWorkspace(cwd) {
|
|
|
83
92
|
writeIfMissing(paths.reference, await bundledReference()),
|
|
84
93
|
writeIfMissing(path.join(paths.skills, "example.dml"), EXAMPLE_DML),
|
|
85
94
|
writeIfMissing(path.join(paths.skills, "deep_research.dml"), await bundledDeepResearch()),
|
|
95
|
+
writeIfMissing(path.join(paths.lib, "specs.dml"), await bundledAsset("specs.dml")),
|
|
96
|
+
writeIfMissing(path.join(paths.lib, "apply.dml"), await bundledAsset("apply.dml")),
|
|
97
|
+
writeIfMissing(path.join(paths.skills, "spec_validate.dml"), await bundledAsset("spec_validate.dml")),
|
|
98
|
+
writeIfMissing(path.join(paths.skills, "spec_status.dml"), await bundledAsset("spec_status.dml")),
|
|
99
|
+
writeIfMissing(path.join(paths.skills, "spec_query.dml"), await bundledAsset("spec_query.dml")),
|
|
100
|
+
writeIfMissing(path.join(paths.skills, "spec_graph.dml"), await bundledAsset("spec_graph.dml")),
|
|
101
|
+
writeIfMissing(path.join(paths.skills, "spec_merge.dml"), await bundledAsset("spec_merge.dml")),
|
|
102
|
+
writeIfMissing(path.join(paths.skills, "spec_archive.dml"), await bundledAsset("spec_archive.dml")),
|
|
103
|
+
writeIfMissing(path.join(paths.skills, "spec_coverage.dml"), await bundledAsset("spec_coverage.dml")),
|
|
104
|
+
writeIfMissing(path.join(paths.skills, "spec_scaffold.dml"), await bundledAsset("spec_scaffold.dml")),
|
|
105
|
+
writeIfMissing(path.join(paths.skills, "spec_apply.dml"), await bundledAsset("spec_apply.dml")),
|
|
86
106
|
]);
|
|
87
107
|
return paths;
|
|
88
108
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# DML diagrams in `deepclause-pi` — minimal design
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
> The user asks pi to turn a DML file into a diagram — **presentation grade** or
|
|
6
|
+
> **specification grade**. When it's done, a viewer opens.
|
|
7
|
+
|
|
8
|
+
That's the whole feature. No command suite, no manual build/edit/check/clean
|
|
9
|
+
ritual. Pi does the work; the user just asks.
|
|
10
|
+
|
|
11
|
+
## How the user experiences it
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
User: “Make me a presentation-grade diagram of
|
|
15
|
+
skills/deep_research.dml”
|
|
16
|
+
|
|
17
|
+
pi: (calls the dc_diagram tool)
|
|
18
|
+
Creating a presentation-grade diagram for deep_research.dml…
|
|
19
|
+
(viewer opens in the browser at the Presentation view)
|
|
20
|
+
|
|
21
|
+
User: “Now a specification-grade one for the same file”
|
|
22
|
+
|
|
23
|
+
pi: (calls dc_diagram with grade=specification)
|
|
24
|
+
(viewer opens at the Specification view)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The DML file can live **anywhere** — a relative path, an absolute path, or a
|
|
28
|
+
path outside `.pi/deepclause/`. Pi points at it; the tool reads it.
|
|
29
|
+
|
|
30
|
+
Grade is inferred from the request, or passed explicitly:
|
|
31
|
+
`presentation` / `presentation-grade` → Presentation; `specification` /
|
|
32
|
+
`spec` / `detailed` / `technical` → Specification. If the user says "both",
|
|
33
|
+
produce both views.
|
|
34
|
+
|
|
35
|
+
## What the extension exposes
|
|
36
|
+
|
|
37
|
+
Exactly **one model-callable tool**:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
dc_diagram
|
|
41
|
+
dml : string // path to any .dml file (relative or absolute)
|
|
42
|
+
grade : "presentation" | "specification" | "both" // default: presentation
|
|
43
|
+
view : "flow" | "sequence" // optional, default: flow
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- Registered on startup (it is a safe, read-and-generate operation — it does not
|
|
47
|
+
execute the DML, unlike `dc_run`, so it does not need the `/dc-tool` opt-in).
|
|
48
|
+
- `promptGuidelines` tell pi to call it whenever the user asks for a diagram /
|
|
49
|
+
flowchart / visual of a DML file, to infer the grade from the wording, and not
|
|
50
|
+
to hand-write Mermaid itself.
|
|
51
|
+
- A short line in `AUTHORING_INSTRUCTION` / `.pi/deepclause/AGENTS.md` mentions
|
|
52
|
+
the tool and where diagrams live.
|
|
53
|
+
|
|
54
|
+
Optional, only if we want manual triggering: a single `/dc-diagram <path>
|
|
55
|
+
[--grade=...]` command that does exactly the same thing. Not required.
|
|
56
|
+
|
|
57
|
+
## What the tool does (one call, end to end)
|
|
58
|
+
|
|
59
|
+
1. Resolve the input path: relative to the workspace or absolute; require an
|
|
60
|
+
existing `.dml` file (`realpath` + extension check). No `.pi/deepclause/`
|
|
61
|
+
restriction on input.
|
|
62
|
+
2. Read the DML and compute a deterministic Mermaid **seed** in-process
|
|
63
|
+
(`renderDml` / `renderSequence`, ported from `dml_flow.mjs`). Always valid.
|
|
64
|
+
3. If a grade is requested, ask the active pi model to rewrite the seed in that
|
|
65
|
+
grade (reusing pi's model backend — no `pi_bash`, no API keys), then validate
|
|
66
|
+
and retry up to ~3 rounds:
|
|
67
|
+
- **Presentation**: ~8–12 nodes, plain language, headline numbers, 1–2
|
|
68
|
+
callouts, no function names or framework jargon.
|
|
69
|
+
- **Specification**: keep function names, `task`/`tool` roles,
|
|
70
|
+
post-conditions, seed data; precise for an engineer.
|
|
71
|
+
- Validation: structural checks always; real Mermaid parser via headless
|
|
72
|
+
Chrome only if Chrome is available. A broken result never reaches the
|
|
73
|
+
viewer.
|
|
74
|
+
4. Write the sidecar(s) under the active workspace's
|
|
75
|
+
`.pi/deepclause/diagrams/`: `<name>.presentation.mmd` and/or
|
|
76
|
+
`<name>.specification.mmd`.
|
|
77
|
+
5. (Re)build `viewer.html` (embedded manifest + vendored Mermaid).
|
|
78
|
+
6. **Open the viewer** at that diagram and view:
|
|
79
|
+
`xdg-open .pi/deepclause/diagrams/viewer.html?view=presentation#<name>`
|
|
80
|
+
(fixed argv via `pi.exec`, TUI/interactive only; otherwise just return the
|
|
81
|
+
path).
|
|
82
|
+
7. Return a one-line result: file, grade, and viewer path.
|
|
83
|
+
|
|
84
|
+
Progress and cancellation reuse the existing `/dc-run` UI plumbing
|
|
85
|
+
(`setWidget`/`setStatus`, a local `AbortController` wired to the active
|
|
86
|
+
execution so `/dc-cancel` still works).
|
|
87
|
+
|
|
88
|
+
## Names and collisions
|
|
89
|
+
|
|
90
|
+
The viewer is keyed by the DML **file name** (without `.dml`). If two DML files
|
|
91
|
+
with the same base name are diagrammed, disambiguate with a short path hash
|
|
92
|
+
(e.g. `deep_research-3f9a`) so sidecars and viewer entries never collide. The
|
|
93
|
+
full source path is always shown in the viewer.
|
|
94
|
+
|
|
95
|
+
## Artifacts
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
.pi/deepclause/diagrams/ # in the active workspace, regardless
|
|
99
|
+
├── viewer.html # of where the DML source lives
|
|
100
|
+
├── index.json # manifest (also handy for tests/scripts)
|
|
101
|
+
├── <name>.md # Mermaid fences for editors
|
|
102
|
+
├── <name>.presentation.mmd # presentation-grade sidecar
|
|
103
|
+
└── <name>.specification.mmd # specification-grade sidecar
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- Created lazily on first use; `initializeWorkspace()` stays untouched.
|
|
107
|
+
- Output always stays under `.pi/deepclause/`; no `.deepclause/`, no `docs/`.
|
|
108
|
+
- Regenerating a grade overwrites only that sidecar (the user asked for it);
|
|
109
|
+
the other grade and other diagrams are preserved.
|
|
110
|
+
|
|
111
|
+
## Viewer
|
|
112
|
+
|
|
113
|
+
Reuse the handbook `viewer.template.html` nearly verbatim, with the sidebar
|
|
114
|
+
list, theme picker, split pane (editable Mermaid + read-only DML), SVG/PNG
|
|
115
|
+
export and hash deep-links. The view dropdown becomes:
|
|
116
|
+
|
|
117
|
+
`Presentation → Specification → Flow → Sequence`
|
|
118
|
+
|
|
119
|
+
Fallback order for the default view: Presentation → Specification → Flow.
|
|
120
|
+
|
|
121
|
+
## Code changes
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
src/diagram/extract.ts # TS port of dml_flow.mjs (renderDml, renderSequence)
|
|
125
|
+
src/diagram/grade.ts # model rewrite + validation/retry loop
|
|
126
|
+
src/diagram/viewer.ts # write sidecars, build viewer.html, open browser
|
|
127
|
+
src/assets/viewer.template.html # adapted; Presentation/Specification labels
|
|
128
|
+
src/assets/vendor/mermaid.min.js # vendored, written on first build
|
|
129
|
+
src/runtime.ts # export a small completeWithPiModel() helper
|
|
130
|
+
src/index.ts # register dc_diagram + authoring note
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Policy
|
|
134
|
+
|
|
135
|
+
- No compiler, no `.deepclause/`, no DML execution.
|
|
136
|
+
- Input may be any readable `.dml` path; **output** is confined to the active
|
|
137
|
+
workspace's `.pi/deepclause/diagrams/`.
|
|
138
|
+
- Does not register runtime tools and does not widen `pi_bash`/approval scope.
|
|
139
|
+
- Browser opening uses a fixed command, not a shell string.
|
|
140
|
+
|
|
141
|
+
## Validation
|
|
142
|
+
|
|
143
|
+
- Asking pi for a diagram in natural language results in a `dc_diagram` call and
|
|
144
|
+
an opened viewer.
|
|
145
|
+
- A DML file outside `.pi/deepclause/` (including an absolute path) works.
|
|
146
|
+
- Presentation vs specification wording selects the right grade; "both"
|
|
147
|
+
produces both.
|
|
148
|
+
- Chrome present → real-parser gate; Chrome absent → structural gate, viewer
|
|
149
|
+
still opens.
|
|
150
|
+
- Invalid/partial model output is retried and never written as a final sidecar.
|
|
151
|
+
- Missing file / non-`.dml` input is rejected.
|
|
152
|
+
- Same-name DML files do not collide in the viewer.
|
|
153
|
+
- No approval prompts are raised.
|
|
154
|
+
- No files appear outside `.pi/deepclause/diagrams/`.
|