deepclause-pi 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +60 -0
- package/dist/diagram/viewer.d.ts +6 -0
- package/dist/diagram/viewer.js +5 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +423 -22
- package/dist/planner.d.ts +27 -2
- package/dist/planner.js +109 -4
- package/dist/runtime.d.ts +17 -1
- package/dist/runtime.js +139 -4
- package/dist/workspace.d.ts +3 -0
- package/dist/workspace.js +20 -0
- package/docs/SPECKIT.md +232 -0
- package/docs/SPEC_LAYER_PROPOSAL.md +1893 -0
- package/package.json +2 -2
- package/src/assets/AGENTS.md +66 -0
- package/src/assets/apply.dml +188 -0
- package/src/assets/spec_apply.dml +20 -0
- package/src/assets/spec_archive.dml +11 -0
- package/src/assets/spec_coverage.dml +26 -0
- package/src/assets/spec_graph.dml +12 -0
- package/src/assets/spec_merge.dml +10 -0
- package/src/assets/spec_query.dml +10 -0
- package/src/assets/spec_scaffold.dml +10 -0
- package/src/assets/spec_status.dml +7 -0
- package/src/assets/spec_validate.dml +9 -0
- package/src/assets/specs.dml +995 -0
- package/src/config.ts +91 -0
- package/src/diagram/viewer.ts +11 -0
- package/src/index.ts +454 -20
- package/src/planner.ts +123 -3
- package/src/runtime.ts +147 -4
- package/src/workspace.ts +24 -0
package/src/planner.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface PlanStepSpec {
|
|
|
16
16
|
requiredTools: string[];
|
|
17
17
|
relevantSkills: string[];
|
|
18
18
|
expectedResult: string;
|
|
19
|
+
satisfies: string[];
|
|
20
|
+
checks: string[];
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export interface PlanSpec {
|
|
@@ -26,6 +28,12 @@ export interface PlanSpec {
|
|
|
26
28
|
steps: PlanStepSpec[];
|
|
27
29
|
finalSynthesis?: string;
|
|
28
30
|
failureMessage: string;
|
|
31
|
+
change?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ValidatePlanOptions {
|
|
35
|
+
requireChecks?: boolean;
|
|
36
|
+
change?: string;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
export interface PlanningSnapshot {
|
|
@@ -70,7 +78,12 @@ export function normalizePlanSlug(value: string): string {
|
|
|
70
78
|
return slug;
|
|
71
79
|
}
|
|
72
80
|
|
|
73
|
-
export function validatePlanSpec(
|
|
81
|
+
export function validatePlanSpec(
|
|
82
|
+
value: unknown,
|
|
83
|
+
snapshot: PlanningSnapshot,
|
|
84
|
+
nameOverride?: string,
|
|
85
|
+
options: ValidatePlanOptions = {},
|
|
86
|
+
): ValidatedPlan {
|
|
74
87
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Plan specification must be an object");
|
|
75
88
|
const raw = value as Record<string, unknown>;
|
|
76
89
|
if (!Array.isArray(raw.steps) || raw.steps.length < 1 || raw.steps.length > 12) {
|
|
@@ -86,13 +99,19 @@ export function validatePlanSpec(value: unknown, snapshot: PlanningSnapshot, nam
|
|
|
86
99
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`steps[${index}] must be an object`);
|
|
87
100
|
const step = entry as Record<string, unknown>;
|
|
88
101
|
const id = requireText(step.id ?? `step_${index + 1}`, `steps[${index}].id`, 80);
|
|
89
|
-
if (!/^[a-zA-
|
|
102
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(id)) throw new Error(`steps[${index}].id must be a simple identifier (letters, digits, dot, dash, underscore)`);
|
|
90
103
|
if (ids.has(id)) throw new Error(`Duplicate plan step id: ${id}`);
|
|
91
104
|
ids.add(id);
|
|
92
105
|
const executor = step.executor;
|
|
93
106
|
if (executor !== "pi" && executor !== "dml") throw new Error(`steps[${index}].executor must be pi or dml`);
|
|
94
107
|
const requiredTools = stringArray(step.requiredTools, `steps[${index}].requiredTools`, 16);
|
|
95
108
|
const relevantSkills = stringArray(step.relevantSkills, `steps[${index}].relevantSkills`, 16);
|
|
109
|
+
const satisfies = stringArray(step.satisfies, `steps[${index}].satisfies`, 32);
|
|
110
|
+
const checks = stringArray(step.checks, `steps[${index}].checks`, 16);
|
|
111
|
+
for (const check of checks) parseCheck(check);
|
|
112
|
+
if (options.requireChecks && checks.length === 0) {
|
|
113
|
+
throw new Error(`Plan step ${id} must declare at least one verification check (cmd:..., exists:... or model:...)`);
|
|
114
|
+
}
|
|
96
115
|
|
|
97
116
|
if (executor === "dml" && requiredTools.length > 0) {
|
|
98
117
|
throw new Error(`DML step ${id} cannot request pi tools; use executor=pi`);
|
|
@@ -114,6 +133,8 @@ export function validatePlanSpec(value: unknown, snapshot: PlanningSnapshot, nam
|
|
|
114
133
|
requiredTools: [...new Set(requiredTools)],
|
|
115
134
|
relevantSkills: [...new Set(relevantSkills)],
|
|
116
135
|
expectedResult: requireText(step.expectedResult, `steps[${index}].expectedResult`, 1_000),
|
|
136
|
+
satisfies: [...new Set(satisfies)],
|
|
137
|
+
checks: [...new Set(checks)],
|
|
117
138
|
};
|
|
118
139
|
});
|
|
119
140
|
|
|
@@ -127,6 +148,7 @@ export function validatePlanSpec(value: unknown, snapshot: PlanningSnapshot, nam
|
|
|
127
148
|
? requireText(raw.finalSynthesis, "finalSynthesis", 2_000)
|
|
128
149
|
: undefined,
|
|
129
150
|
failureMessage: requireText(raw.failureMessage, "failureMessage", 1_000),
|
|
151
|
+
change: options.change ? normalizePlanSlug(options.change) : undefined,
|
|
130
152
|
};
|
|
131
153
|
|
|
132
154
|
return {
|
|
@@ -148,6 +170,75 @@ function commentText(value: string): string {
|
|
|
148
170
|
return value.replace(/[\r\n]+/g, " ").replace(/%/g, "percent").trim();
|
|
149
171
|
}
|
|
150
172
|
|
|
173
|
+
export interface ParsedCheck {
|
|
174
|
+
kind: "cmd" | "exists" | "model";
|
|
175
|
+
value: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Parse an encoded verification check: cmd:<command>, exists:<path>, model:<question>. */
|
|
179
|
+
export function parseCheck(encoded: string): ParsedCheck {
|
|
180
|
+
const separator = encoded.indexOf(":");
|
|
181
|
+
if (separator <= 0) throw new Error(`Check must be cmd:..., exists:... or model:...; got '${encoded}'`);
|
|
182
|
+
const kind = encoded.slice(0, separator).trim();
|
|
183
|
+
const value = encoded.slice(separator + 1).trim();
|
|
184
|
+
if (kind !== "cmd" && kind !== "exists" && kind !== "model") throw new Error(`Unknown check kind '${kind}' in '${encoded}'`);
|
|
185
|
+
if (!value) throw new Error(`Check '${encoded}' has an empty value`);
|
|
186
|
+
return { kind, value };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Assemble the `tasks.dml` data artifact for a change: plan_task/2 definitions plus
|
|
191
|
+
* the managed plan_task_status/2 block. Verified by /dc-check and executed by /dc-apply.
|
|
192
|
+
*/
|
|
193
|
+
export function assembleTasksDml(plan: ValidatedPlan, snapshot: PlanningSnapshot): string {
|
|
194
|
+
const { spec } = plan;
|
|
195
|
+
const taskBlocks = spec.steps.map((step) => {
|
|
196
|
+
const checks = step.checks.map((encoded) => {
|
|
197
|
+
const { kind, value } = parseCheck(encoded);
|
|
198
|
+
return `${kind}(${dmlString(value)})`;
|
|
199
|
+
});
|
|
200
|
+
return [
|
|
201
|
+
`plan_task(${dmlString(step.id)}, task{`,
|
|
202
|
+
` executor: ${step.executor},`,
|
|
203
|
+
` do: ${dmlString(step.instruction)},`,
|
|
204
|
+
` tools: ${dmlStringList(step.requiredTools)},`,
|
|
205
|
+
` expected: ${dmlString(step.expectedResult)},`,
|
|
206
|
+
` satisfies: ${dmlStringList(step.satisfies)},`,
|
|
207
|
+
` checks: [${checks.join(", ")}]`,
|
|
208
|
+
`}).`,
|
|
209
|
+
].join("\n");
|
|
210
|
+
});
|
|
211
|
+
const statusLines = spec.steps.map((step) => `plan_task_status(${dmlString(step.id)}, pending).`);
|
|
212
|
+
const metadata = [
|
|
213
|
+
"% tasks.dml — generated by /dc-plan. Edit conservatively.",
|
|
214
|
+
`% Change: ${commentText(spec.change ?? spec.slug)}`,
|
|
215
|
+
`% Title: ${commentText(spec.title)}`,
|
|
216
|
+
`% Planning model: ${commentText(snapshot.model)}`,
|
|
217
|
+
`% Required pi tools: ${plan.requiredTools.join(", ") || "none"}`,
|
|
218
|
+
].join("\n");
|
|
219
|
+
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`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Write changes/<slug>/tasks.dml, refusing to clobber an existing plan unless overwrite=true. */
|
|
223
|
+
export async function writeChangeTasks(paths: DeepClausePaths, slug: string, text: string, overwrite = false): Promise<string> {
|
|
224
|
+
const dir = path.join(paths.changes, slug);
|
|
225
|
+
await mkdir(dir, { recursive: true });
|
|
226
|
+
const filePath = path.join(dir, "tasks.dml");
|
|
227
|
+
if (overwrite) {
|
|
228
|
+
await writeFile(filePath, text, { encoding: "utf8" });
|
|
229
|
+
return filePath;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
await writeFile(filePath, text, { encoding: "utf8", flag: "wx" });
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
235
|
+
throw new Error(`changes/${slug}/tasks.dml already exists; re-run with --update to regenerate it`);
|
|
236
|
+
}
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
return filePath;
|
|
240
|
+
}
|
|
241
|
+
|
|
151
242
|
export function assemblePlanDml(plan: ValidatedPlan, snapshot: PlanningSnapshot): string {
|
|
152
243
|
const { spec } = plan;
|
|
153
244
|
const resultVariables: string[] = [];
|
|
@@ -209,6 +300,16 @@ export async function validateGeneratedPlan(dml: string): Promise<void> {
|
|
|
209
300
|
if (!validation.valid) throw new Error(`Generated DML failed validation: ${validation.errors.join("; ")}`);
|
|
210
301
|
}
|
|
211
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Data-only artifact (tasks.dml): no agent_main of its own, so validation appends a
|
|
305
|
+
* trivial entry point to parse the facts without changing what is written.
|
|
306
|
+
*/
|
|
307
|
+
export async function validateGeneratedTasks(dml: string): Promise<void> {
|
|
308
|
+
if (dml.includes(".deepclause/")) throw new Error("Generated tasks may not reference .deepclause/");
|
|
309
|
+
const validation = await validateWithProlog(`${dml}\nagent_main :- true.\n`);
|
|
310
|
+
if (!validation.valid) throw new Error(`Generated tasks.dml failed validation: ${validation.errors.join("; ")}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
212
313
|
export async function writePlanNonDestructively(paths: DeepClausePaths, slug: string, dml: string): Promise<string> {
|
|
213
314
|
await mkdir(paths.plans, { recursive: true });
|
|
214
315
|
for (let suffix = 1; suffix <= 100; suffix++) {
|
|
@@ -235,7 +336,7 @@ export async function readPlanRequiredTools(filePath: string): Promise<string[]>
|
|
|
235
336
|
return [...new Set(match[1]!.split(",").map((name) => name.trim()).filter(Boolean))];
|
|
236
337
|
}
|
|
237
338
|
|
|
238
|
-
export function buildPlanningPrompt(request: string, snapshot: PlanningSnapshot, nameOverride?: string): string {
|
|
339
|
+
export function buildPlanningPrompt(request: string, snapshot: PlanningSnapshot, nameOverride?: string, change?: string, update = false): string {
|
|
239
340
|
const tools = snapshot.allTools.map((tool) => ({
|
|
240
341
|
name: tool.name,
|
|
241
342
|
active: snapshot.activeTools.includes(tool.name),
|
|
@@ -244,6 +345,24 @@ export function buildPlanningPrompt(request: string, snapshot: PlanningSnapshot,
|
|
|
244
345
|
guidelines: tool.promptGuidelines ?? [],
|
|
245
346
|
source: tool.sourceInfo,
|
|
246
347
|
}));
|
|
348
|
+
const changeInstructions = change
|
|
349
|
+
? [
|
|
350
|
+
update
|
|
351
|
+
? `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.`
|
|
352
|
+
: `This plan is for the change '${change}'. Before committing, create .pi/deepclause/changes/${change}/ with normal file tools:`,
|
|
353
|
+
...(update
|
|
354
|
+
? []
|
|
355
|
+
: [
|
|
356
|
+
"- proposal.md — why / what / capabilities / impact.",
|
|
357
|
+
"- specs/<capability>.spec.md — delta(s) using ## ADDED|MODIFIED|REMOVED Requirements.",
|
|
358
|
+
"- design.md — optional approach and trade-offs.",
|
|
359
|
+
]),
|
|
360
|
+
"Specs describe behaviour only: no commands, file paths, library choices or implementation steps.",
|
|
361
|
+
"Use exactly three hashes for ### Requirement and four for #### Scenario; every requirement needs at least one scenario.",
|
|
362
|
+
"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>.",
|
|
363
|
+
"The committed tasks.dml replaces any previous one; every plan_task_status entry resets to pending.",
|
|
364
|
+
]
|
|
365
|
+
: [];
|
|
247
366
|
return [
|
|
248
367
|
"Create an executable DeepClause plan for the request below.",
|
|
249
368
|
"You are in a normal pi turn: inspect the workspace and use currently active tools when that materially improves the plan.",
|
|
@@ -253,6 +372,7 @@ export function buildPlanningPrompt(request: string, snapshot: PlanningSnapshot,
|
|
|
253
372
|
"Choose executor='dml' for contained reasoning that needs no pi tool; requiredTools must then be empty.",
|
|
254
373
|
"Use only exact active tool names. Never request dc_run, dc_plan_commit, or pi_agent_step.",
|
|
255
374
|
"Keep steps bounded, concrete, ordered, and independently observable. Prefer 3-8 steps.",
|
|
375
|
+
...changeInstructions,
|
|
256
376
|
nameOverride ? `The user requested the plan filename slug: ${nameOverride}` : "Choose a concise lowercase slug.",
|
|
257
377
|
`User request:\n${request}`,
|
|
258
378
|
`Current model: ${snapshot.model}; thinking level: ${snapshot.thinkingLevel}`,
|
package/src/runtime.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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
|
-
import { createDeepClause } from "deepclause-sdk";
|
|
3
|
+
import { createDeepClause, createJevJudgeBackend, createLLMJudgeBackend } from "deepclause-sdk";
|
|
4
4
|
import type {
|
|
5
5
|
DMLEvent,
|
|
6
|
+
JudgeBackend,
|
|
6
7
|
LLMBackend,
|
|
7
8
|
LLMBackendMessage,
|
|
8
9
|
LLMUsage,
|
|
@@ -16,6 +17,66 @@ import { PI_AGENT_STEP_TOOL } from "./planner.js";
|
|
|
16
17
|
|
|
17
18
|
export const PI_WORKSPACE_LIST_TOOL = "pi_workspace_list";
|
|
18
19
|
export const PI_BASH_TOOL = "pi_bash";
|
|
20
|
+
export const DC_VERIFY_RUN_TOOL = "dc_verify_run";
|
|
21
|
+
export const DC_APPLY_SNAPSHOT_TOOL = "dc_apply_snapshot";
|
|
22
|
+
export const DC_APPLY_ACCEPT_TOOL = "dc_apply_accept";
|
|
23
|
+
export const DC_APPLY_RESTORE_TOOL = "dc_apply_restore";
|
|
24
|
+
|
|
25
|
+
async function readChangeJson(changeJsonPath: string): Promise<Record<string, unknown>> {
|
|
26
|
+
try {
|
|
27
|
+
const parsed: unknown = JSON.parse(await readFile(changeJsonPath, "utf8"));
|
|
28
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : {};
|
|
29
|
+
} catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function patchChangeJson(changeJsonPath: string, patch: Record<string, unknown>): Promise<void> {
|
|
35
|
+
let existing: Record<string, unknown> = {};
|
|
36
|
+
try {
|
|
37
|
+
const parsed: unknown = JSON.parse(await readFile(changeJsonPath, "utf8"));
|
|
38
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed as Record<string, unknown>;
|
|
39
|
+
} catch {
|
|
40
|
+
// start a fresh manifest
|
|
41
|
+
}
|
|
42
|
+
await writeFile(changeJsonPath, `${JSON.stringify({ ...existing, ...patch }, null, 2)}\n`, "utf8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Record a git snapshot ref, refusing a dirty working tree. If an apply is already
|
|
47
|
+
* in progress for this change, the existing ref is returned so the run resumes
|
|
48
|
+
* instead of starting over or rejecting the (intentionally) dirty tree.
|
|
49
|
+
*/
|
|
50
|
+
export async function gitSnapshot(pi: Pick<ExtensionAPI, "exec">, cwd: string, changeJsonPath: string): Promise<string> {
|
|
51
|
+
const existing = await readChangeJson(changeJsonPath);
|
|
52
|
+
const resumable = typeof existing.snapshot === "string" && existing.snapshot && existing.applyState === "in_progress";
|
|
53
|
+
if (resumable) return existing.snapshot as string;
|
|
54
|
+
|
|
55
|
+
const status = await pi.exec("git", ["status", "--porcelain"], { cwd });
|
|
56
|
+
if (status.code !== 0) throw new Error("git is unavailable or this is not a repository");
|
|
57
|
+
if (status.stdout.trim()) throw new Error("working tree is dirty; commit or stash before applying, or resume with /dc-apply (which preserves an interrupted apply)");
|
|
58
|
+
const head = await pi.exec("git", ["rev-parse", "HEAD"], { cwd });
|
|
59
|
+
if (head.code !== 0) throw new Error("could not read git HEAD");
|
|
60
|
+
const ref = head.stdout.trim();
|
|
61
|
+
await patchChangeJson(changeJsonPath, { snapshot: ref, applyState: "in_progress" });
|
|
62
|
+
return ref;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Restore the recorded snapshot (hard reset plus clean of untracked files). */
|
|
66
|
+
export async function gitRestore(pi: Pick<ExtensionAPI, "exec">, cwd: string, changeJsonPath: string): Promise<string | null> {
|
|
67
|
+
const existing = await readChangeJson(changeJsonPath);
|
|
68
|
+
const ref = typeof existing.snapshot === "string" && existing.snapshot ? existing.snapshot : null;
|
|
69
|
+
if (!ref) return null;
|
|
70
|
+
await pi.exec("git", ["reset", "--hard", ref], { cwd });
|
|
71
|
+
await pi.exec("git", ["clean", "-fd"], { cwd });
|
|
72
|
+
await patchChangeJson(changeJsonPath, { snapshot: null, applyState: "aborted" });
|
|
73
|
+
return ref;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Mark the apply accepted and clear the snapshot. */
|
|
77
|
+
export async function gitAccept(changeJsonPath: string): Promise<void> {
|
|
78
|
+
await patchChangeJson(changeJsonPath, { snapshot: null, applyState: "done" });
|
|
79
|
+
}
|
|
19
80
|
|
|
20
81
|
export type BashApproval = (command: string, signal: AbortSignal) => Promise<boolean>;
|
|
21
82
|
|
|
@@ -267,22 +328,50 @@ export async function executeDml(
|
|
|
267
328
|
filePath: string,
|
|
268
329
|
args: string[],
|
|
269
330
|
initialMessages: MemoryMessage[],
|
|
270
|
-
config: DeepClauseConfig,
|
|
331
|
+
config: DeepClauseConfig & { judgeBackend?: string },
|
|
271
332
|
pi: ExtensionAPI,
|
|
272
333
|
ctx: ExtensionContext,
|
|
273
334
|
controller: AbortController,
|
|
274
335
|
callbacks: ExecutionCallbacks,
|
|
275
336
|
runPiAgentStep?: (request: PiAgentStepRequest, signal: AbortSignal) => Promise<PiAgentStepResult>,
|
|
337
|
+
verifyCommands: string[] = [],
|
|
338
|
+
applyChangeJsonPath?: string,
|
|
276
339
|
): Promise<ExecutionResult> {
|
|
277
340
|
const model = ctx.model;
|
|
278
341
|
if (!model) throw new Error("Select a pi model before running DeepClause");
|
|
279
342
|
const backend = createPiBackend(ctx, config.maxTokens, callbacks.onDiagnostic ?? (() => {}));
|
|
343
|
+
|
|
344
|
+
// Judgment backends. `llm` reuses pi's active model and credentials; `jev`
|
|
345
|
+
// is opt-in and only registered when enabled and its key is present.
|
|
346
|
+
const judgeBackends: Record<string, JudgeBackend> = {
|
|
347
|
+
llm: createLLMJudgeBackend({ llmBackend: backend, model: model.id }),
|
|
348
|
+
};
|
|
349
|
+
const jev = config.judgment.jev;
|
|
350
|
+
if (jev.enabled) {
|
|
351
|
+
const apiKey = process.env[jev.apiKeyEnv];
|
|
352
|
+
if (apiKey) {
|
|
353
|
+
judgeBackends.jev = createJevJudgeBackend({ apiKey, model: jev.model });
|
|
354
|
+
} else {
|
|
355
|
+
callbacks.onDiagnostic?.(
|
|
356
|
+
`judgment backend 'jev' is enabled but ${jev.apiKeyEnv} is not set; falling back to 'llm'`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const requestedJudge = config.judgeBackend ?? config.judgment.default;
|
|
361
|
+
if (!judgeBackends[requestedJudge]) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Judgment backend '${requestedJudge}' is not available. Enable it in .pi/deepclause/config.json or use --judge=llm.`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
280
367
|
const sdk = await createDeepClause({
|
|
281
368
|
model: model.id,
|
|
282
369
|
maxTokens: config.maxTokens,
|
|
283
370
|
streaming: true,
|
|
284
371
|
debug: config.verbose,
|
|
285
372
|
llmBackend: backend,
|
|
373
|
+
judgeBackends,
|
|
374
|
+
defaultJudge: requestedJudge,
|
|
286
375
|
});
|
|
287
376
|
registerPiRuntimeTools(
|
|
288
377
|
sdk,
|
|
@@ -325,9 +414,62 @@ export async function executeDml(
|
|
|
325
414
|
},
|
|
326
415
|
});
|
|
327
416
|
}
|
|
417
|
+
if (verifyCommands.length > 0) {
|
|
418
|
+
sdk.registerTool(DC_VERIFY_RUN_TOOL, {
|
|
419
|
+
description: "Run one of the change's pre-approved verification commands in the workspace and return its exit code, stdout and stderr.",
|
|
420
|
+
parameters: {
|
|
421
|
+
type: "object",
|
|
422
|
+
properties: {
|
|
423
|
+
command: { type: "string", description: "Exact approved command string" },
|
|
424
|
+
},
|
|
425
|
+
required: ["command"],
|
|
426
|
+
},
|
|
427
|
+
execute: async (args) => {
|
|
428
|
+
const command = typeof args.command === "string" ? args.command.trim() : "";
|
|
429
|
+
if (!verifyCommands.includes(command)) {
|
|
430
|
+
throw new Error(`verification command was not approved for this run: ${command}`);
|
|
431
|
+
}
|
|
432
|
+
const result = await pi.exec("bash", ["-lc", command], {
|
|
433
|
+
cwd: ctx.cwd,
|
|
434
|
+
signal: controller.signal,
|
|
435
|
+
timeout: 120_000,
|
|
436
|
+
});
|
|
437
|
+
return { command, stdout: result.stdout, stderr: result.stderr, exitCode: result.code, killed: result.killed };
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (applyChangeJsonPath) {
|
|
443
|
+
const changeJsonPath = applyChangeJsonPath;
|
|
444
|
+
sdk.registerTool(DC_APPLY_SNAPSHOT_TOOL, {
|
|
445
|
+
description: "Record a git snapshot of the workspace before applying tasks. Refuses a dirty working tree.",
|
|
446
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
447
|
+
execute: async () => gitSnapshot(pi, ctx.cwd, changeJsonPath),
|
|
448
|
+
});
|
|
449
|
+
sdk.registerTool(DC_APPLY_ACCEPT_TOOL, {
|
|
450
|
+
description: "Accept the apply so the working tree is not restored.",
|
|
451
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
452
|
+
execute: async () => {
|
|
453
|
+
await gitAccept(changeJsonPath);
|
|
454
|
+
return "accepted";
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
sdk.registerTool(DC_APPLY_RESTORE_TOOL, {
|
|
458
|
+
description: "Restore the recorded snapshot, discarding task changes.",
|
|
459
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
460
|
+
execute: async () => (await gitRestore(pi, ctx.cwd, changeJsonPath)) ?? "none",
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
328
464
|
sdk.setToolPolicy({
|
|
329
465
|
mode: "whitelist",
|
|
330
|
-
tools: [
|
|
466
|
+
tools: [
|
|
467
|
+
PI_WORKSPACE_LIST_TOOL,
|
|
468
|
+
PI_BASH_TOOL,
|
|
469
|
+
...(runPiAgentStep ? [PI_AGENT_STEP_TOOL] : []),
|
|
470
|
+
...(verifyCommands.length > 0 ? [DC_VERIFY_RUN_TOOL] : []),
|
|
471
|
+
...(applyChangeJsonPath ? [DC_APPLY_SNAPSHOT_TOOL, DC_APPLY_ACCEPT_TOOL, DC_APPLY_RESTORE_TOOL] : []),
|
|
472
|
+
],
|
|
331
473
|
});
|
|
332
474
|
|
|
333
475
|
const result: ExecutionResult = {
|
|
@@ -343,6 +485,7 @@ export async function executeDml(
|
|
|
343
485
|
gasLimit: config.gasLimit,
|
|
344
486
|
signal: controller.signal,
|
|
345
487
|
initialMessages,
|
|
488
|
+
judgeBackend: config.judgeBackend,
|
|
346
489
|
onUserInput: (prompt) => callbacks.onInput(prompt, controller.signal),
|
|
347
490
|
})) {
|
|
348
491
|
callbacks.onEvent(event);
|
package/src/workspace.ts
CHANGED
|
@@ -7,6 +7,9 @@ export interface DeepClausePaths {
|
|
|
7
7
|
root: string;
|
|
8
8
|
skills: string;
|
|
9
9
|
plans: string;
|
|
10
|
+
specs: string;
|
|
11
|
+
changes: string;
|
|
12
|
+
lib: string;
|
|
10
13
|
config: string;
|
|
11
14
|
agents: string;
|
|
12
15
|
reference: string;
|
|
@@ -57,6 +60,9 @@ export function getPaths(cwd: string): DeepClausePaths {
|
|
|
57
60
|
root,
|
|
58
61
|
skills: path.join(root, "skills"),
|
|
59
62
|
plans: path.join(root, "plans"),
|
|
63
|
+
specs: path.join(root, "specs"),
|
|
64
|
+
changes: path.join(root, "changes"),
|
|
65
|
+
lib: path.join(root, "lib"),
|
|
60
66
|
config: path.join(root, "config.json"),
|
|
61
67
|
agents: path.join(root, "AGENTS.md"),
|
|
62
68
|
reference: path.join(root, "DML_REFERENCE.md"),
|
|
@@ -81,6 +87,10 @@ async function bundledAuthoringGuide(): Promise<string> {
|
|
|
81
87
|
return readFile(fileURLToPath(new URL("./assets/AGENTS.md", import.meta.url)), "utf8");
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
async function bundledAsset(name: string): Promise<string> {
|
|
91
|
+
return readFile(fileURLToPath(new URL(`./assets/${name}`, import.meta.url)), "utf8");
|
|
92
|
+
}
|
|
93
|
+
|
|
84
94
|
async function bundledDeepResearch(): Promise<string> {
|
|
85
95
|
return readFile(fileURLToPath(new URL("./assets/deep_research.dml", import.meta.url)), "utf8");
|
|
86
96
|
}
|
|
@@ -90,6 +100,9 @@ export async function initializeWorkspace(cwd: string): Promise<DeepClausePaths>
|
|
|
90
100
|
await Promise.all([
|
|
91
101
|
mkdir(paths.skills, { recursive: true }),
|
|
92
102
|
mkdir(paths.plans, { recursive: true }),
|
|
103
|
+
mkdir(paths.specs, { recursive: true }),
|
|
104
|
+
mkdir(paths.changes, { recursive: true }),
|
|
105
|
+
mkdir(paths.lib, { recursive: true }),
|
|
93
106
|
]);
|
|
94
107
|
await Promise.all([
|
|
95
108
|
writeIfMissing(paths.config, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`),
|
|
@@ -97,6 +110,17 @@ export async function initializeWorkspace(cwd: string): Promise<DeepClausePaths>
|
|
|
97
110
|
writeIfMissing(paths.reference, await bundledReference()),
|
|
98
111
|
writeIfMissing(path.join(paths.skills, "example.dml"), EXAMPLE_DML),
|
|
99
112
|
writeIfMissing(path.join(paths.skills, "deep_research.dml"), await bundledDeepResearch()),
|
|
113
|
+
writeIfMissing(path.join(paths.lib, "specs.dml"), await bundledAsset("specs.dml")),
|
|
114
|
+
writeIfMissing(path.join(paths.lib, "apply.dml"), await bundledAsset("apply.dml")),
|
|
115
|
+
writeIfMissing(path.join(paths.skills, "spec_validate.dml"), await bundledAsset("spec_validate.dml")),
|
|
116
|
+
writeIfMissing(path.join(paths.skills, "spec_status.dml"), await bundledAsset("spec_status.dml")),
|
|
117
|
+
writeIfMissing(path.join(paths.skills, "spec_query.dml"), await bundledAsset("spec_query.dml")),
|
|
118
|
+
writeIfMissing(path.join(paths.skills, "spec_graph.dml"), await bundledAsset("spec_graph.dml")),
|
|
119
|
+
writeIfMissing(path.join(paths.skills, "spec_merge.dml"), await bundledAsset("spec_merge.dml")),
|
|
120
|
+
writeIfMissing(path.join(paths.skills, "spec_archive.dml"), await bundledAsset("spec_archive.dml")),
|
|
121
|
+
writeIfMissing(path.join(paths.skills, "spec_coverage.dml"), await bundledAsset("spec_coverage.dml")),
|
|
122
|
+
writeIfMissing(path.join(paths.skills, "spec_scaffold.dml"), await bundledAsset("spec_scaffold.dml")),
|
|
123
|
+
writeIfMissing(path.join(paths.skills, "spec_apply.dml"), await bundledAsset("spec_apply.dml")),
|
|
100
124
|
]);
|
|
101
125
|
return paths;
|
|
102
126
|
}
|