deepclause-pi 0.1.3

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/dist/index.js ADDED
@@ -0,0 +1,809 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { StringEnum } from "@earendil-works/pi-ai";
4
+ import { Type } from "typebox";
5
+ import { buildInitialMessages } from "./context.js";
6
+ import { loadConfig, setModelToolEnabled } from "./config.js";
7
+ import { executeDml } from "./runtime.js";
8
+ import { getPaths, initializeWorkspace, resolveDmlPath } from "./workspace.js";
9
+ import { assemblePlanDml, buildPlanningPrompt, DC_PLAN_COMMIT_TOOL, isContextualPlan, PI_AGENT_STEP_TOOL, readPlanRequiredTools, validateGeneratedPlan, validatePlanSpec, writePlanNonDestructively, } from "./planner.js";
10
+ const DC_RUN_TOOL = "dc_run";
11
+ 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. 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/.`;
12
+ const STATUS_KEY = "deepclause";
13
+ const WIDGET_KEY = "deepclause-stream";
14
+ export function splitArguments(input) {
15
+ const result = [];
16
+ let current = "";
17
+ let quote;
18
+ let escaped = false;
19
+ for (const char of input.trim()) {
20
+ if (escaped) {
21
+ current += char;
22
+ escaped = false;
23
+ }
24
+ else if (char === "\\" && quote !== "'") {
25
+ escaped = true;
26
+ }
27
+ else if (quote) {
28
+ if (char === quote)
29
+ quote = undefined;
30
+ else
31
+ current += char;
32
+ }
33
+ else if (char === "'" || char === '"') {
34
+ quote = char;
35
+ }
36
+ else if (/\s/.test(char)) {
37
+ if (current) {
38
+ result.push(current);
39
+ current = "";
40
+ }
41
+ }
42
+ else {
43
+ current += char;
44
+ }
45
+ }
46
+ if (escaped)
47
+ current += "\\";
48
+ if (quote)
49
+ throw new Error("Unterminated quoted argument");
50
+ if (current)
51
+ result.push(current);
52
+ return result;
53
+ }
54
+ export function parseRun(input) {
55
+ const tokens = splitArguments(input);
56
+ const target = tokens.shift();
57
+ if (!target)
58
+ throw new Error("Usage: /dc-run <skill|path> [args] [--context=MODE] [--verbose|--debug]");
59
+ let contextMode;
60
+ let verbose = false;
61
+ let debug = false;
62
+ const args = [];
63
+ for (let index = 0; index < tokens.length; index++) {
64
+ const token = tokens[index];
65
+ const contextValue = token.startsWith("--context=") ? token.slice("--context=".length) : undefined;
66
+ if (token === "--verbose" || token === "-v") {
67
+ verbose = true;
68
+ }
69
+ else if (token === "--debug" || token === "-d") {
70
+ verbose = true;
71
+ debug = true;
72
+ }
73
+ else if (contextValue !== undefined) {
74
+ if (contextValue !== "turn" && contextValue !== "branch" && contextValue !== "isolated") {
75
+ throw new Error("--context must be turn, branch, or isolated");
76
+ }
77
+ contextMode = contextValue;
78
+ }
79
+ else if (token === "--context") {
80
+ const value = tokens[++index];
81
+ if (value !== "turn" && value !== "branch" && value !== "isolated") {
82
+ throw new Error("--context must be turn, branch, or isolated");
83
+ }
84
+ contextMode = value;
85
+ }
86
+ else {
87
+ args.push(token);
88
+ }
89
+ }
90
+ return { target, args, contextMode, verbose, debug };
91
+ }
92
+ export function parsePlan(input) {
93
+ const tokens = splitArguments(input);
94
+ let name;
95
+ let debug = false;
96
+ const requestParts = [];
97
+ for (let index = 0; index < tokens.length; index++) {
98
+ const token = tokens[index];
99
+ if (token === "--debug" || token === "-d")
100
+ debug = true;
101
+ else if (token.startsWith("--name="))
102
+ name = token.slice("--name=".length);
103
+ else if (token === "--name")
104
+ name = tokens[++index];
105
+ else
106
+ requestParts.push(token);
107
+ }
108
+ const request = requestParts.join(" ").trim();
109
+ if (!request)
110
+ throw new Error("Usage: /dc-plan <request> [--name=slug] [--debug]");
111
+ if (name !== undefined && !name.trim())
112
+ throw new Error("--name requires a non-empty slug");
113
+ return { request, name: name?.trim(), debug };
114
+ }
115
+ function messageText(message) {
116
+ if (!message || typeof message !== "object")
117
+ return "";
118
+ const content = message.content;
119
+ if (typeof content === "string")
120
+ return content;
121
+ if (!Array.isArray(content))
122
+ return "";
123
+ return content
124
+ .filter((part) => Boolean(part) && typeof part === "object")
125
+ .filter((part) => part.type === "text" && typeof part.text === "string")
126
+ .map((part) => part.text)
127
+ .join("\n")
128
+ .trim();
129
+ }
130
+ function elapsedSeconds(startedAt) {
131
+ return `${((Date.now() - startedAt) / 1000).toFixed(1)}s`;
132
+ }
133
+ function eventSummary(event, debug) {
134
+ if (debug)
135
+ return JSON.stringify(event);
136
+ switch (event.type) {
137
+ case "output": return `output: ${event.content ?? ""}`;
138
+ case "log": return `log: ${event.content ?? ""}`;
139
+ case "answer": return "answer received";
140
+ case "finished": return "runtime finished";
141
+ case "error": return `error: ${event.content ?? "unknown error"}`;
142
+ case "input_required": return `input required: ${event.prompt ?? ""}`;
143
+ case "stream": return event.done ? "model stream completed" : "model stream update";
144
+ case "tool_call": return `tool ${event.toolState ?? "call"}: ${event.toolName ?? "unknown"}`;
145
+ case "usage": return `usage: ${event.usage?.inputTokens ?? 0} in / ${event.usage?.outputTokens ?? 0} out`;
146
+ case "task_activity": return `task ${event.taskState ?? "active"}: ${event.taskDescription ?? event.taskId ?? "task"}`;
147
+ case "memory_compaction": return `compaction ${event.compactionAction ?? "event"}`;
148
+ }
149
+ }
150
+ async function listDmlFiles(directory, prefix = "") {
151
+ let entries;
152
+ try {
153
+ entries = await readdir(directory, { withFileTypes: true });
154
+ }
155
+ catch (error) {
156
+ if (error.code === "ENOENT")
157
+ return [];
158
+ throw error;
159
+ }
160
+ const files = [];
161
+ for (const entry of entries) {
162
+ const relative = path.posix.join(prefix, entry.name);
163
+ if (entry.isDirectory())
164
+ files.push(...await listDmlFiles(path.join(directory, entry.name), relative));
165
+ else if (entry.isFile() && entry.name.endsWith(".dml"))
166
+ files.push(relative);
167
+ }
168
+ return files.sort();
169
+ }
170
+ function modelLabel(ctx) {
171
+ return ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "none selected";
172
+ }
173
+ function publishResult(pi, content, details) {
174
+ pi.sendMessage({ customType: "deepclause-result", content, display: true, details });
175
+ }
176
+ export default function deepClauseExtension(pi) {
177
+ let activeController;
178
+ let activeDescription;
179
+ let modelToolRegistered = false;
180
+ let planCommitRegistered = false;
181
+ let planningTransaction;
182
+ let pendingAgentStep;
183
+ const setPlanCommitActive = (enabled) => {
184
+ if (enabled && !planCommitRegistered) {
185
+ pi.registerTool({
186
+ name: DC_PLAN_COMMIT_TOOL,
187
+ label: "Commit DeepClause Plan",
188
+ description: "Commit a structured executable DeepClause plan after inspecting the current pi context, skills, workspace, and active tools. Available only during /dc-plan.",
189
+ promptSnippet: "Commit the structured DML plan requested by /dc-plan",
190
+ promptGuidelines: [
191
+ "Call dc_plan_commit exactly once after gathering enough context to create a concrete executable plan.",
192
+ "Use only exact active pi tool names, and choose dml steps when no pi capability is required.",
193
+ ],
194
+ parameters: Type.Object({
195
+ slug: Type.String(),
196
+ title: Type.String(),
197
+ objective: Type.String(),
198
+ assumptions: Type.Array(Type.String()),
199
+ steps: Type.Array(Type.Object({
200
+ id: Type.String(),
201
+ title: Type.String(),
202
+ instruction: Type.String(),
203
+ executor: StringEnum(["pi", "dml"]),
204
+ requiredTools: Type.Array(Type.String()),
205
+ relevantSkills: Type.Array(Type.String()),
206
+ expectedResult: Type.String(),
207
+ }), { minItems: 1, maxItems: 12 }),
208
+ finalSynthesis: Type.Optional(Type.String()),
209
+ failureMessage: Type.String(),
210
+ }),
211
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
212
+ const transaction = planningTransaction;
213
+ if (!transaction) {
214
+ return {
215
+ content: [{ type: "text", text: "No /dc-plan transaction is active." }],
216
+ details: { success: false, error: "no_planning_transaction" },
217
+ };
218
+ }
219
+ if (transaction.committed) {
220
+ return {
221
+ content: [{ type: "text", text: "This planning transaction has already committed a plan." }],
222
+ details: { success: false, error: "plan_already_committed" },
223
+ };
224
+ }
225
+ try {
226
+ const plan = validatePlanSpec(params, transaction.snapshot, transaction.nameOverride);
227
+ const preview = [
228
+ plan.spec.title,
229
+ `Objective: ${plan.spec.objective}`,
230
+ `Steps: ${plan.spec.steps.length}`,
231
+ `Pi tools: ${plan.requiredTools.join(", ") || "none"}`,
232
+ ...plan.spec.steps.map((step, index) => `${index + 1}. [${step.executor}] ${step.title}`),
233
+ ].join("\n");
234
+ if (!ctx.hasUI || !await ctx.ui.confirm("Create executable DeepClause plan?", preview)) {
235
+ return {
236
+ content: [{ type: "text", text: "Plan creation was not approved." }],
237
+ details: { success: false, error: "plan_not_approved" },
238
+ };
239
+ }
240
+ const paths = await initializeWorkspace(ctx.cwd);
241
+ const dml = assemblePlanDml(plan, transaction.snapshot);
242
+ await validateGeneratedPlan(dml);
243
+ const filePath = await writePlanNonDestructively(paths, plan.spec.slug, dml);
244
+ transaction.committed = true;
245
+ setPlanCommitActive(false);
246
+ const relativePath = path.relative(paths.root, filePath).split(path.sep).join("/");
247
+ const text = [
248
+ `Created executable DML plan: .pi/deepclause/${relativePath}`,
249
+ `Run it with: /dc-run ${relativePath}`,
250
+ plan.warnings.length ? `Warnings:\n${plan.warnings.join("\n")}` : "",
251
+ ].filter(Boolean).join("\n\n");
252
+ return {
253
+ content: [{ type: "text", text }],
254
+ details: {
255
+ success: true,
256
+ path: relativePath,
257
+ contextual: plan.spec.steps.some((step) => step.executor === "pi"),
258
+ requiredTools: plan.requiredTools,
259
+ warnings: plan.warnings,
260
+ },
261
+ };
262
+ }
263
+ catch (error) {
264
+ const message = error instanceof Error ? error.message : String(error);
265
+ return {
266
+ content: [{ type: "text", text: `Plan commit failed: ${message}` }],
267
+ details: { success: false, error: message },
268
+ };
269
+ }
270
+ },
271
+ });
272
+ planCommitRegistered = true;
273
+ }
274
+ const activeTools = pi.getActiveTools();
275
+ const isActive = activeTools.includes(DC_PLAN_COMMIT_TOOL);
276
+ if (enabled !== isActive) {
277
+ pi.setActiveTools(enabled
278
+ ? [...new Set([...activeTools, DC_PLAN_COMMIT_TOOL])]
279
+ : activeTools.filter((name) => name !== DC_PLAN_COMMIT_TOOL));
280
+ }
281
+ };
282
+ const runPiAgentStep = async (request, signal, ctx) => {
283
+ if (pendingAgentStep)
284
+ throw new Error("Another delegated pi plan step is active");
285
+ if (!request.instruction.trim())
286
+ throw new Error("pi_agent_step requires a non-empty instruction");
287
+ const requestedTools = [...new Set(request.tools)];
288
+ const recursiveTools = new Set([DC_RUN_TOOL, DC_PLAN_COMMIT_TOOL, PI_AGENT_STEP_TOOL]);
289
+ if (requestedTools.some((name) => recursiveTools.has(name))) {
290
+ throw new Error("A contextual plan cannot request DeepClause control tools");
291
+ }
292
+ const knownTools = new Set(pi.getAllTools().map((tool) => tool.name));
293
+ const previousTools = pi.getActiveTools();
294
+ const activeTools = new Set(previousTools);
295
+ for (const toolName of requestedTools) {
296
+ if (!knownTools.has(toolName))
297
+ throw new Error(`Required pi tool is no longer installed: ${toolName}`);
298
+ if (!activeTools.has(toolName))
299
+ throw new Error(`Required pi tool is not active: ${toolName}`);
300
+ }
301
+ if (!ctx.isIdle())
302
+ throw new Error("Pi must be idle before a contextual plan step starts");
303
+ const correlationId = `dc-step-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
304
+ pi.setActiveTools(requestedTools);
305
+ return new Promise((resolve, reject) => {
306
+ const onAbort = () => {
307
+ ctx.abort();
308
+ if (pendingAgentStep) {
309
+ pendingAgentStep.cleanup();
310
+ pendingAgentStep = undefined;
311
+ }
312
+ reject(signal.reason instanceof Error ? signal.reason : new Error("Contextual pi plan step cancelled"));
313
+ };
314
+ const cleanup = () => {
315
+ signal.removeEventListener("abort", onAbort);
316
+ pi.setActiveTools(previousTools);
317
+ };
318
+ pendingAgentStep = {
319
+ previousTools,
320
+ toolsUsed: [],
321
+ errors: [],
322
+ cleanup,
323
+ resolve,
324
+ reject,
325
+ };
326
+ if (signal.aborted) {
327
+ onAbort();
328
+ return;
329
+ }
330
+ signal.addEventListener("abort", onAbort, { once: true });
331
+ try {
332
+ pi.sendUserMessage([
333
+ `[DeepClause contextual plan step]`,
334
+ `Internal correlation: ${correlationId}`,
335
+ `Instruction: ${request.instruction}`,
336
+ `Expected result: ${request.expected}`,
337
+ `Relevant skills: ${request.skills.join(", ") || "none specified"}`,
338
+ `Active tools for this step: ${requestedTools.join(", ") || "none"}`,
339
+ "Execute this bounded step using the current pi context and skills. Do not invoke DeepClause planning or execution controls. Finish with a concise summary of actions, concrete results, validation, and remaining errors.",
340
+ ].join("\n\n"));
341
+ }
342
+ catch (error) {
343
+ pendingAgentStep = undefined;
344
+ cleanup();
345
+ reject(error instanceof Error ? error : new Error(String(error)));
346
+ }
347
+ });
348
+ };
349
+ const setModelToolActive = (enabled) => {
350
+ if (enabled && !modelToolRegistered) {
351
+ pi.registerTool({
352
+ name: DC_RUN_TOOL,
353
+ label: "Run DeepClause Skill",
354
+ description: "Execute an existing DML program under .pi/deepclause/ with pi's active model and return its final answer, errors, and usage.",
355
+ promptSnippet: "Run an existing DeepClause DML skill from .pi/deepclause/",
356
+ promptGuidelines: [
357
+ "Use dc_run only for existing DML programs when their deterministic logic, constraints, or specialized orchestration is useful; do not use dc_run to compile natural language or create a skill.",
358
+ "Do not call dc_run while another DeepClause execution is active, and do not claim success unless dc_run returns an answer without errors.",
359
+ ],
360
+ parameters: Type.Object({
361
+ skill: Type.String({ description: "Skill name such as example, or a DML path relative to .pi/deepclause/." }),
362
+ args: Type.Optional(Type.Array(Type.String(), { description: "Positional arguments passed to agent_main/N." })),
363
+ context: Type.Optional(StringEnum(["turn", "branch", "isolated"], {
364
+ description: "Optional session-context override for this execution.",
365
+ })),
366
+ }),
367
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
368
+ if (activeController) {
369
+ return {
370
+ content: [{ type: "text", text: "DeepClause execution rejected: another execution is already active." }],
371
+ details: { success: false, error: "execution_already_active" },
372
+ };
373
+ }
374
+ const paths = await initializeWorkspace(ctx.cwd);
375
+ const config = await loadConfig(paths.config);
376
+ if (!config.modelToolEnabled || !pi.getActiveTools().includes(DC_RUN_TOOL)) {
377
+ return {
378
+ content: [{ type: "text", text: "The dc_run tool is disabled. The user can enable it with /dc-tool enable." }],
379
+ details: { success: false, error: "tool_disabled" },
380
+ };
381
+ }
382
+ const mode = params.context ?? config.contextMode;
383
+ const filePath = await resolveDmlPath(paths, params.skill);
384
+ if (await isContextualPlan(filePath)) {
385
+ return {
386
+ content: [{ type: "text", text: "Contextual DML plans must be started by the user with /dc-run; they cannot start a nested pi agent turn from dc_run." }],
387
+ details: { success: false, error: "interactive_plan_requires_user_run" },
388
+ };
389
+ }
390
+ const skillName = path.relative(paths.root, filePath);
391
+ const initialMessages = buildInitialMessages(ctx.sessionManager.getBranch(), mode, config.branchMessageLimit);
392
+ const controller = new AbortController();
393
+ const cancel = () => controller.abort(signal?.reason ?? new Error("dc_run cancelled"));
394
+ if (signal?.aborted)
395
+ cancel();
396
+ else
397
+ signal?.addEventListener("abort", cancel, { once: true });
398
+ activeController = controller;
399
+ activeDescription = `model tool running ${skillName}`;
400
+ const startedAt = Date.now();
401
+ const progress = [];
402
+ try {
403
+ const result = await executeDml(filePath, params.args ?? [], initialMessages, config, pi, ctx, controller, {
404
+ onEvent: (event) => {
405
+ if (event.type === "output" && event.content)
406
+ progress.push(event.content);
407
+ else if (event.type === "task_activity")
408
+ progress.push(`Task ${event.taskState ?? "active"}: ${event.taskDescription ?? event.taskId ?? "task"}`);
409
+ else if (event.type === "tool_call")
410
+ progress.push(`Tool ${event.toolState ?? "call"}: ${event.toolName ?? "unknown"}`);
411
+ else if (event.type === "input_required")
412
+ progress.push(`Waiting for user input: ${event.prompt ?? ""}`);
413
+ else
414
+ return;
415
+ onUpdate?.({
416
+ content: [{ type: "text", text: progress.slice(-4).join("\n") }],
417
+ details: { skill: skillName, contextMode: mode, elapsedMs: Date.now() - startedAt },
418
+ });
419
+ },
420
+ onDiagnostic: () => { },
421
+ onInput: async (prompt, inputSignal) => {
422
+ if (!ctx.hasUI)
423
+ throw new Error("dc_run cannot request user input without interactive UI");
424
+ const answer = await ctx.ui.input("DeepClause input", prompt, { signal: inputSignal });
425
+ if (answer === undefined)
426
+ throw new Error("Input cancelled");
427
+ return answer;
428
+ },
429
+ });
430
+ const success = result.errors.length === 0 && result.answer !== undefined;
431
+ const text = result.answer ?? (result.errors.length > 0
432
+ ? `DeepClause execution failed:\n${result.errors.join("\n")}`
433
+ : "DeepClause execution finished without an answer.");
434
+ return {
435
+ content: [{ type: "text", text }],
436
+ details: {
437
+ success,
438
+ skill: skillName,
439
+ contextMode: mode,
440
+ elapsedMs: Date.now() - startedAt,
441
+ answer: result.answer,
442
+ errors: result.errors,
443
+ usage: result.usage,
444
+ },
445
+ };
446
+ }
447
+ catch (error) {
448
+ const message = error instanceof Error ? error.message : String(error);
449
+ return {
450
+ content: [{ type: "text", text: `DeepClause execution failed: ${message}` }],
451
+ details: { success: false, skill: skillName, contextMode: mode, error: message },
452
+ };
453
+ }
454
+ finally {
455
+ signal?.removeEventListener("abort", cancel);
456
+ activeController = undefined;
457
+ activeDescription = undefined;
458
+ }
459
+ },
460
+ });
461
+ modelToolRegistered = true;
462
+ }
463
+ const activeTools = pi.getActiveTools();
464
+ const isActive = activeTools.includes(DC_RUN_TOOL);
465
+ if (enabled !== isActive) {
466
+ pi.setActiveTools(enabled
467
+ ? [...new Set([...activeTools, DC_RUN_TOOL])]
468
+ : activeTools.filter((name) => name !== DC_RUN_TOOL));
469
+ }
470
+ };
471
+ pi.on("session_start", async (_event, ctx) => {
472
+ const config = await loadConfig(getPaths(ctx.cwd).config);
473
+ setModelToolActive(config.modelToolEnabled);
474
+ });
475
+ pi.on("tool_execution_start", (event) => {
476
+ if (pendingAgentStep && !pendingAgentStep.toolsUsed.includes(event.toolName)) {
477
+ pendingAgentStep.toolsUsed.push(event.toolName);
478
+ }
479
+ });
480
+ pi.on("tool_execution_end", (event) => {
481
+ if (pendingAgentStep && event.isError)
482
+ pendingAgentStep.errors.push(`${event.toolName} failed`);
483
+ });
484
+ pi.on("agent_end", (event) => {
485
+ if (!pendingAgentStep)
486
+ return;
487
+ for (let index = event.messages.length - 1; index >= 0; index--) {
488
+ const text = messageText(event.messages[index]);
489
+ if (text) {
490
+ pendingAgentStep.summary = text;
491
+ break;
492
+ }
493
+ }
494
+ });
495
+ pi.on("agent_settled", () => {
496
+ if (pendingAgentStep) {
497
+ const pending = pendingAgentStep;
498
+ pendingAgentStep = undefined;
499
+ pending.cleanup();
500
+ const summary = pending.summary?.trim() ?? "";
501
+ pending.resolve({
502
+ // Tool failures are normal, recoverable events in a pi agent loop. The
503
+ // delegated step succeeds when pi settles with a textual summary; the
504
+ // collected failures remain available as diagnostics.
505
+ success: summary.length > 0,
506
+ summary: summary || "Pi completed the delegated turn without a textual summary.",
507
+ toolsUsed: pending.toolsUsed,
508
+ errors: pending.errors,
509
+ });
510
+ return;
511
+ }
512
+ if (planningTransaction) {
513
+ const committed = planningTransaction.committed;
514
+ planningTransaction = undefined;
515
+ setPlanCommitActive(false);
516
+ if (!committed)
517
+ console.error("[deepclause] /dc-plan turn settled without committing a plan");
518
+ }
519
+ });
520
+ pi.on("before_agent_start", (event) => ({
521
+ systemPrompt: `${event.systemPrompt}\n\n${AUTHORING_INSTRUCTION}`,
522
+ }));
523
+ pi.on("session_shutdown", () => {
524
+ activeController?.abort(new Error("Pi session changed or closed"));
525
+ if (pendingAgentStep) {
526
+ const pending = pendingAgentStep;
527
+ pendingAgentStep = undefined;
528
+ pending.cleanup();
529
+ pending.reject(new Error("Pi session changed or closed"));
530
+ }
531
+ planningTransaction = undefined;
532
+ setPlanCommitActive(false);
533
+ activeController = undefined;
534
+ activeDescription = undefined;
535
+ });
536
+ pi.registerCommand("dc", {
537
+ description: "Show DeepClause runtime help and status",
538
+ handler: async (_args, ctx) => {
539
+ const paths = await initializeWorkspace(ctx.cwd);
540
+ const config = await loadConfig(paths.config);
541
+ const message = [
542
+ "DeepClause pi runtime",
543
+ `Model: ${modelLabel(ctx)}`,
544
+ `Status: ${activeDescription ?? "idle"}`,
545
+ `Root: ${path.relative(ctx.cwd, paths.root)}`,
546
+ `Skills: ${path.relative(ctx.cwd, paths.skills)}`,
547
+ `Plans: ${path.relative(ctx.cwd, paths.plans)}`,
548
+ `Context: ${config.contextMode} (verbose default: ${config.verbose})`,
549
+ `Model tool (${DC_RUN_TOOL}): ${config.modelToolEnabled && pi.getActiveTools().includes(DC_RUN_TOOL) ? "enabled" : "disabled"}`,
550
+ "Commands:",
551
+ " /dc-list",
552
+ " /dc-plan <request> [--name=slug] create an executable contextual DML plan",
553
+ " /dc-run <skill|path> [args] [--context=turn|branch|isolated]",
554
+ " /dc-run <skill|path> --verbose show lifecycle events",
555
+ " /dc-run <skill|path> --debug show full event payloads and SDK diagnostics",
556
+ " /dc-tool enable|disable|status control the model-callable dc_run tool",
557
+ " /dc-cancel",
558
+ ].join("\n");
559
+ ctx.ui.notify(message, "info");
560
+ },
561
+ });
562
+ pi.registerCommand("dc-plan", {
563
+ description: "Create an executable DML plan using pi's current context, skills, and active tools",
564
+ handler: async (rawArgs, ctx) => {
565
+ if (activeController || pendingAgentStep || planningTransaction || !ctx.isIdle()) {
566
+ ctx.ui.notify("DeepClause or pi is already active; wait before starting /dc-plan", "warning");
567
+ return;
568
+ }
569
+ try {
570
+ const parsed = parsePlan(rawArgs);
571
+ if (!ctx.model)
572
+ throw new Error("Select a pi model before creating a plan");
573
+ const paths = await initializeWorkspace(ctx.cwd);
574
+ const promptOptions = ctx.getSystemPromptOptions();
575
+ const snapshot = {
576
+ model: `${ctx.model.provider}/${ctx.model.id}`,
577
+ thinkingLevel: String(ctx.thinkingLevel ?? pi.getThinkingLevel()),
578
+ activeTools: pi.getActiveTools().filter((name) => name !== DC_PLAN_COMMIT_TOOL),
579
+ allTools: pi.getAllTools().filter((tool) => tool.name !== DC_PLAN_COMMIT_TOOL && tool.name !== PI_AGENT_STEP_TOOL),
580
+ skillNames: (promptOptions.skills ?? []).map((skill) => skill.name),
581
+ contextFiles: (promptOptions.contextFiles ?? []).map((file) => file.path),
582
+ existingSkills: await listDmlFiles(paths.skills),
583
+ existingPlans: await listDmlFiles(paths.plans),
584
+ };
585
+ planningTransaction = {
586
+ snapshot,
587
+ nameOverride: parsed.name,
588
+ committed: false,
589
+ startedAt: Date.now(),
590
+ };
591
+ setPlanCommitActive(true);
592
+ ctx.ui.notify("Starting a contextual pi planning turn. Review the generated plan before it is written.", "info");
593
+ pi.sendUserMessage(buildPlanningPrompt(parsed.request, snapshot, parsed.name));
594
+ }
595
+ catch (error) {
596
+ planningTransaction = undefined;
597
+ setPlanCommitActive(false);
598
+ const message = error instanceof Error ? error.message : String(error);
599
+ if (rawArgs.includes("--debug") || rawArgs.includes("-d"))
600
+ console.error(`[deepclause:plan] ${message}`);
601
+ ctx.ui.notify(message, "error");
602
+ }
603
+ },
604
+ });
605
+ pi.registerCommand("dc-tool", {
606
+ description: "Enable, disable, or inspect the model-callable dc_run tool",
607
+ handler: async (rawArgs, ctx) => {
608
+ const action = rawArgs.trim().toLowerCase() || "status";
609
+ if (action !== "enable" && action !== "on" && action !== "disable" && action !== "off" && action !== "status") {
610
+ ctx.ui.notify("Usage: /dc-tool enable|disable|status", "warning");
611
+ return;
612
+ }
613
+ const paths = await initializeWorkspace(ctx.cwd);
614
+ if (action === "enable" || action === "on") {
615
+ await setModelToolEnabled(paths.config, true);
616
+ setModelToolActive(true);
617
+ ctx.ui.notify("dc_run is enabled for this workspace and is now callable by the model", "warning");
618
+ }
619
+ else if (action === "disable" || action === "off") {
620
+ await setModelToolEnabled(paths.config, false);
621
+ setModelToolActive(false);
622
+ ctx.ui.notify("dc_run is disabled for this workspace", "info");
623
+ }
624
+ else {
625
+ const config = await loadConfig(paths.config);
626
+ const active = config.modelToolEnabled && pi.getActiveTools().includes(DC_RUN_TOOL);
627
+ ctx.ui.notify(`dc_run model tool: ${active ? "enabled" : "disabled"}`, "info");
628
+ }
629
+ },
630
+ });
631
+ pi.registerCommand("dc-list", {
632
+ description: "List DeepClause DML skills and generated plans",
633
+ handler: async (_args, ctx) => {
634
+ const paths = getPaths(ctx.cwd);
635
+ const [skills, plans] = await Promise.all([listDmlFiles(paths.skills), listDmlFiles(paths.plans)]);
636
+ const message = [
637
+ "Skills:",
638
+ ...(skills.length ? skills.map((file) => ` ${file}`) : [" none"]),
639
+ "",
640
+ "Plans:",
641
+ ...(plans.length ? plans.map((file) => ` ${file}`) : [" none"]),
642
+ ].join("\n");
643
+ ctx.ui.notify(message, "info");
644
+ },
645
+ });
646
+ pi.registerCommand("dc-cancel", {
647
+ description: "Cancel the active DeepClause execution",
648
+ handler: async (_args, ctx) => {
649
+ if (!activeController) {
650
+ ctx.ui.notify("No DeepClause execution is active", "info");
651
+ return;
652
+ }
653
+ activeController.abort(new Error("Cancelled by user"));
654
+ ctx.ui.notify("Cancelling DeepClause execution", "warning");
655
+ },
656
+ });
657
+ pi.registerCommand("dc-run", {
658
+ description: "Run a DML skill with pi's active model",
659
+ handler: async (rawArgs, ctx) => {
660
+ if (activeController) {
661
+ ctx.ui.notify("A DeepClause execution is already active", "warning");
662
+ return;
663
+ }
664
+ try {
665
+ const parsed = parseRun(rawArgs);
666
+ const paths = await initializeWorkspace(ctx.cwd);
667
+ const config = await loadConfig(paths.config);
668
+ const filePath = await resolveDmlPath(paths, parsed.target);
669
+ const contextualPlan = await isContextualPlan(filePath);
670
+ if (contextualPlan) {
671
+ const requiredTools = await readPlanRequiredTools(filePath);
672
+ const knownTools = new Set(pi.getAllTools().map((tool) => tool.name));
673
+ const activeTools = new Set(pi.getActiveTools());
674
+ const missingTools = requiredTools.filter((name) => !knownTools.has(name));
675
+ const inactiveTools = requiredTools.filter((name) => knownTools.has(name) && !activeTools.has(name));
676
+ if (missingTools.length)
677
+ throw new Error(`Contextual plan requires missing pi tools: ${missingTools.join(", ")}`);
678
+ if (inactiveTools.length)
679
+ throw new Error(`Contextual plan requires inactive pi tools: ${inactiveTools.join(", ")}`);
680
+ if (!ctx.hasUI || !await ctx.ui.confirm("Run contextual DeepClause plan?", [
681
+ "This plan may delegate bounded steps to pi using the current session context, skills, and explicitly named active tools.",
682
+ `Preflight tools: ${requiredTools.join(", ") || "none declared"}`,
683
+ "Tool-specific approvals still apply.",
684
+ ].join("\n\n"))) {
685
+ ctx.ui.notify("Contextual plan execution was not approved", "warning");
686
+ return;
687
+ }
688
+ }
689
+ const mode = parsed.contextMode ?? config.contextMode;
690
+ const initialMessages = buildInitialMessages(ctx.sessionManager.getBranch(), mode, config.branchMessageLimit);
691
+ const controller = new AbortController();
692
+ activeController = controller;
693
+ const outputLines = [];
694
+ const recentEvents = [];
695
+ const events = [];
696
+ const startedAt = Date.now();
697
+ const skillName = path.relative(paths.root, filePath);
698
+ const verbose = parsed.verbose || config.verbose;
699
+ let phase = "starting runtime";
700
+ let inputTokens = 0;
701
+ let outputTokens = 0;
702
+ activeDescription = `running ${skillName} (${elapsedSeconds(startedAt)})`;
703
+ const renderExecution = () => {
704
+ const header = `DeepClause RUNNING ${skillName} ${elapsedSeconds(startedAt)}`;
705
+ const metadata = `${modelLabel(ctx)} | context=${mode} | ${parsed.debug ? "debug" : verbose ? "verbose" : "normal"}`;
706
+ const lines = [header, metadata, `Phase: ${phase}`, `Usage: ${inputTokens} input / ${outputTokens} output tokens`];
707
+ if (outputLines.length > 0)
708
+ lines.push("", "Output:", ...outputLines.slice(-5));
709
+ if (verbose && recentEvents.length > 0)
710
+ lines.push("", "Recent events:", ...recentEvents.slice(parsed.debug ? -8 : -5));
711
+ ctx.ui.setWidget(WIDGET_KEY, lines, { placement: "aboveEditor" });
712
+ ctx.ui.setStatus(STATUS_KEY, `${skillName}: ${phase} (${elapsedSeconds(startedAt)})`);
713
+ activeDescription = `running ${skillName}: ${phase} (${elapsedSeconds(startedAt)})`;
714
+ };
715
+ renderExecution();
716
+ const statusTimer = setInterval(renderExecution, 250);
717
+ const onEvent = (event) => {
718
+ const summary = eventSummary(event, parsed.debug);
719
+ if (parsed.debug)
720
+ console.error(`[deepclause:event] ${summary}`);
721
+ recentEvents.push(summary.length > 300 ? `${summary.slice(0, 297)}...` : summary);
722
+ if (event.type === "task_activity") {
723
+ const label = event.taskDescription ?? event.taskId ?? "task";
724
+ phase = `${event.taskState ?? "running"} task: ${label.slice(0, 100)}`;
725
+ }
726
+ else if (event.type === "stream" && event.content) {
727
+ phase = "receiving model response";
728
+ outputLines.push(...event.content.split("\n").filter(Boolean));
729
+ }
730
+ else if (event.type === "output" && event.content) {
731
+ phase = event.content;
732
+ outputLines.push(event.content);
733
+ }
734
+ else if (event.type === "tool_call") {
735
+ phase = `tool ${event.toolState ?? "call"}: ${event.toolName ?? "unknown"}`;
736
+ }
737
+ else if (event.type === "input_required") {
738
+ phase = `waiting for input: ${event.prompt ?? ""}`;
739
+ }
740
+ else if (event.type === "usage" && event.usage) {
741
+ inputTokens += event.usage.inputTokens;
742
+ outputTokens += event.usage.outputTokens;
743
+ }
744
+ else if (event.type === "answer") {
745
+ phase = "answer received";
746
+ }
747
+ else if (event.type === "finished") {
748
+ phase = "finishing";
749
+ }
750
+ else if (event.type === "error" && event.content) {
751
+ phase = `error: ${event.content}`;
752
+ ctx.ui.notify(event.content, "error");
753
+ }
754
+ if (parsed.debug || event.type === "task_activity" || event.type === "tool_call" || event.type === "error") {
755
+ events.push(parsed.debug ? { ...event } : { type: event.type, state: event.taskState ?? event.toolState, name: event.taskDescription ?? event.toolName, error: event.content });
756
+ }
757
+ renderExecution();
758
+ };
759
+ const onDiagnostic = (message, details) => {
760
+ const rendered = details === undefined ? message : `${message}: ${JSON.stringify(details)}`;
761
+ recentEvents.push(rendered.length > 300 ? `${rendered.slice(0, 297)}...` : rendered);
762
+ if (parsed.debug)
763
+ console.error(`[deepclause] ${rendered}`);
764
+ renderExecution();
765
+ };
766
+ try {
767
+ const result = await executeDml(filePath, parsed.args, initialMessages, { ...config, verbose: config.verbose || parsed.debug }, pi, ctx, controller, {
768
+ onEvent,
769
+ onDiagnostic,
770
+ onInput: async (prompt, signal) => {
771
+ const answer = await ctx.ui.input("DeepClause input", prompt, { signal });
772
+ if (answer === undefined)
773
+ throw new Error("Input cancelled");
774
+ return answer;
775
+ },
776
+ }, contextualPlan ? (request, signal) => runPiAgentStep(request, signal, ctx) : undefined);
777
+ const answer = result.answer ?? (result.errors.length ? result.errors.join("\n") : "DML execution finished without an answer.");
778
+ publishResult(pi, answer, {
779
+ skill: skillName,
780
+ contextMode: mode,
781
+ model: modelLabel(ctx),
782
+ elapsedMs: Date.now() - startedAt,
783
+ verbosity: parsed.debug ? "debug" : verbose ? "verbose" : "normal",
784
+ usage: result.usage,
785
+ events,
786
+ });
787
+ if (result.errors.length === 0)
788
+ ctx.ui.notify(`DeepClause execution complete in ${elapsedSeconds(startedAt)}`, "info");
789
+ }
790
+ finally {
791
+ clearInterval(statusTimer);
792
+ }
793
+ }
794
+ catch (error) {
795
+ const message = error instanceof Error ? error.message : String(error);
796
+ if (rawArgs.includes("--debug") || rawArgs.includes("-d"))
797
+ console.error(`[deepclause] execution failed: ${message}`);
798
+ publishResult(pi, `DeepClause execution failed: ${message}`, { error: message });
799
+ ctx.ui.notify(message, activeController?.signal.aborted ? "warning" : "error");
800
+ }
801
+ finally {
802
+ activeController = undefined;
803
+ activeDescription = undefined;
804
+ ctx.ui.setStatus(STATUS_KEY, undefined);
805
+ ctx.ui.setWidget(WIDGET_KEY, undefined);
806
+ }
807
+ },
808
+ });
809
+ }