deepclause-pi 0.4.0 → 0.5.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/src/index.ts CHANGED
@@ -322,6 +322,24 @@ function publishResult(pi: ExtensionAPI, content: string, details: Record<string
322
322
  pi.sendMessage({ customType: "deepclause-result", content, display: true, details });
323
323
  }
324
324
 
325
+ /**
326
+ * Ask the user a DeepClause question.
327
+ *
328
+ * Pi's interactive text-input dialog renders only its title: the placeholder
329
+ * argument is ignored by `ExtensionInputComponent`. Passing the question as the
330
+ * placeholder (as the extension used to) made it invisible, so put the whole
331
+ * question in the title and label which run is asking.
332
+ */
333
+ export function requestDeepClauseInput(
334
+ ctx: Pick<ExtensionContext, "ui">,
335
+ label: string,
336
+ prompt: string,
337
+ signal: AbortSignal,
338
+ ): Promise<string | undefined> {
339
+ const heading = label ? `DeepClause input — ${label}` : "DeepClause input";
340
+ return ctx.ui.input(`${heading}\n\n${prompt}`, undefined, { signal });
341
+ }
342
+
325
343
  export default function deepClauseExtension(pi: ExtensionAPI) {
326
344
  let activeController: AbortController | undefined;
327
345
  let activeDescription: string | undefined;
@@ -332,6 +350,27 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
332
350
  let planningTransaction: PlanningTransaction | undefined;
333
351
  let pendingAgentStep: PendingAgentStep | undefined;
334
352
 
353
+ /**
354
+ * Claim the single DeepClause execution slot synchronously, before any await.
355
+ * Without this, parallel `dc_run` calls all pass the `activeController` check
356
+ * while the first one is still awaiting its setup, then run concurrently and
357
+ * fight over the one-slot pi input dialog (leaving earlier runs hung).
358
+ */
359
+ const claimExecution = (description: string): AbortController | undefined => {
360
+ if (activeController) return undefined;
361
+ const controller = new AbortController();
362
+ activeController = controller;
363
+ activeDescription = description;
364
+ return controller;
365
+ };
366
+
367
+ const releaseExecution = (controller: AbortController): void => {
368
+ if (activeController === controller) {
369
+ activeController = undefined;
370
+ activeDescription = undefined;
371
+ }
372
+ };
373
+
335
374
  const setPlanCommitActive = (enabled: boolean) => {
336
375
  if (enabled && !planCommitRegistered) {
337
376
  pi.registerTool({
@@ -528,6 +567,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
528
567
  promptGuidelines: [
529
568
  "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.",
530
569
  "Do not call dc_run while another DeepClause execution is active, and do not claim success unless dc_run returns an answer without errors.",
570
+ "If dc_run reports execution_already_active, wait for the active run to finish and then retry this skill instead of reporting failure.",
531
571
  ],
532
572
  parameters: Type.Object({
533
573
  skill: Type.String({ description: "Skill name such as example, or a DML path relative to .pi/deepclause/." }),
@@ -537,46 +577,45 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
537
577
  })),
538
578
  }),
539
579
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
540
- if (activeController) {
580
+ const controller = claimExecution("dc_run");
581
+ if (!controller) {
541
582
  return {
542
- content: [{ type: "text", text: "DeepClause execution rejected: another execution is already active." }],
583
+ content: [{ type: "text", text: "DeepClause execution rejected: another execution is active. Wait for it to finish, then call dc_run again for this skill." }],
543
584
  details: { success: false, error: "execution_already_active" },
544
585
  };
545
586
  }
546
-
547
- const paths = await initializeWorkspace(ctx.cwd);
548
- const config = await loadConfig(paths.config);
549
- if (!config.modelToolEnabled || !pi.getActiveTools().includes(DC_RUN_TOOL)) {
550
- return {
551
- content: [{ type: "text", text: "The dc_run tool is disabled. The user can enable it with /dc-tool enable." }],
552
- details: { success: false, error: "tool_disabled" },
553
- };
554
- }
555
-
556
- const mode = params.context ?? config.contextMode;
557
- const filePath = await resolveDmlPath(paths, params.skill);
558
- if (await isContextualPlan(filePath)) {
559
- return {
560
- 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." }],
561
- details: { success: false, error: "interactive_plan_requires_user_run" },
562
- };
563
- }
564
- const skillName = path.relative(paths.root, filePath);
565
- const initialMessages = buildInitialMessages(
566
- ctx.sessionManager.getBranch(),
567
- mode,
568
- config.branchMessageLimit,
569
- );
570
- const controller = new AbortController();
571
587
  const cancel = () => controller.abort(signal?.reason ?? new Error("dc_run cancelled"));
572
588
  if (signal?.aborted) cancel();
573
589
  else signal?.addEventListener("abort", cancel, { once: true });
574
- activeController = controller;
575
- activeDescription = `model tool running ${skillName}`;
576
- const startedAt = Date.now();
577
- const progress: string[] = [];
578
590
 
579
591
  try {
592
+ const paths = await initializeWorkspace(ctx.cwd);
593
+ const config = await loadConfig(paths.config);
594
+ if (!config.modelToolEnabled || !pi.getActiveTools().includes(DC_RUN_TOOL)) {
595
+ return {
596
+ content: [{ type: "text", text: "The dc_run tool is disabled. The user can enable it with /dc-tool enable." }],
597
+ details: { success: false, error: "tool_disabled" },
598
+ };
599
+ }
600
+
601
+ const mode = params.context ?? config.contextMode;
602
+ const filePath = await resolveDmlPath(paths, params.skill);
603
+ if (await isContextualPlan(filePath)) {
604
+ return {
605
+ 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." }],
606
+ details: { success: false, error: "interactive_plan_requires_user_run" },
607
+ };
608
+ }
609
+ const skillName = path.relative(paths.root, filePath);
610
+ activeDescription = `model tool running ${skillName}`;
611
+ const initialMessages = buildInitialMessages(
612
+ ctx.sessionManager.getBranch(),
613
+ mode,
614
+ config.branchMessageLimit,
615
+ );
616
+ const startedAt = Date.now();
617
+ const progress: string[] = [];
618
+
580
619
  const result = await executeDml(
581
620
  filePath,
582
621
  params.args ?? [],
@@ -600,7 +639,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
600
639
  onDiagnostic: () => {},
601
640
  onInput: async (prompt, inputSignal) => {
602
641
  if (!ctx.hasUI) throw new Error("dc_run cannot request user input without interactive UI");
603
- const answer = await ctx.ui.input("DeepClause input", prompt, { signal: inputSignal });
642
+ const answer = await requestDeepClauseInput(ctx, skillName, prompt, inputSignal);
604
643
  if (answer === undefined) throw new Error("Input cancelled");
605
644
  return answer;
606
645
  },
@@ -626,12 +665,11 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
626
665
  const message = error instanceof Error ? error.message : String(error);
627
666
  return {
628
667
  content: [{ type: "text", text: `DeepClause execution failed: ${message}` }],
629
- details: { success: false, skill: skillName, contextMode: mode, error: message },
668
+ details: { success: false, error: message },
630
669
  };
631
670
  } finally {
632
671
  signal?.removeEventListener("abort", cancel);
633
- activeController = undefined;
634
- activeDescription = undefined;
672
+ releaseExecution(controller);
635
673
  }
636
674
  },
637
675
  });
@@ -652,11 +690,11 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
652
690
  pi.registerTool({
653
691
  name: DC_DIAGRAM_TOOL,
654
692
  label: "Create DeepClause Diagram",
655
- description: "Create a presentation-grade or specification-grade Mermaid diagram from any .dml file, write a self-contained offline viewer under .pi/deepclause/diagrams/, and open it.",
693
+ description: "Create a presentation-grade or specification-grade Mermaid diagram from any .dml file, write a self-contained offline viewer under .pi/deepclause/diagrams/, and open it. The specification grade preserves the core decision logic and rule facts.",
656
694
  promptSnippet: "Create a presentation- or specification-grade diagram from a DML file",
657
695
  promptGuidelines: [
658
696
  "Use dc_diagram whenever the user asks for a diagram, flowchart, or visual of a .dml file; pass the exact path the user named.",
659
- "Choose grade=presentation for slides and overviews and grade=specification for engineering detail; use grade=both only when the user asks for both.",
697
+ "Choose grade=presentation for slides and overviews and grade=specification for engineering detail (decision predicates, thresholds, rule facts); use grade=both only when the user asks for both.",
660
698
  "Do not hand-write Mermaid or run diagram tools yourself; call dc_diagram and report the viewer result.",
661
699
  ],
662
700
  parameters: Type.Object({
@@ -665,55 +703,51 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
665
703
  view: Type.Optional(StringEnum(["flow", "sequence"] as const, { description: "Base layout used to seed the grade; default flow." })),
666
704
  }),
667
705
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
668
- if (activeController) {
706
+ const controller = claimExecution("dc_diagram");
707
+ if (!controller) {
669
708
  return {
670
709
  content: [{ type: "text", text: "Another DeepClause operation is already active; wait for it to finish." }],
671
710
  details: { success: false, error: "execution_already_active" },
672
711
  };
673
712
  }
674
-
675
- const requested = resolveGrade(String(params.grade ?? "")) ?? "presentation";
676
- const grades: DiagramGrade[] = requested === "both" ? ["presentation", "specification"] : [requested];
677
- const view: MermaidView = params.view === "sequence" ? "sequence" : "flow";
678
-
679
- let sourcePath: string;
680
- try {
681
- sourcePath = await resolveDiagramSource(ctx.cwd, params.dml);
682
- } catch (error) {
683
- const message = error instanceof Error ? error.message : String(error);
684
- return { content: [{ type: "text", text: `dc_diagram failed: ${message}` }], details: { success: false, error: message } };
685
- }
686
- if (!ctx.model) {
687
- return {
688
- content: [{ type: "text", text: "dc_diagram requires an active pi model. Select one and try again." }],
689
- details: { success: false, error: "no_model" },
690
- };
691
- }
692
-
693
- const config = await loadConfig(getPaths(ctx.cwd).config);
694
- const source = await readFile(sourcePath, "utf8");
695
- const display = displayPath(ctx.cwd, sourcePath);
696
- const seed = view === "sequence"
697
- ? renderSequence(display, source)
698
- : renderDml(display, source, { hideOutput: true });
699
- const targets = await collectDiagramTargets(ctx.cwd, [sourcePath]);
700
- const name = diagramNameFor(sourcePath, targets, ctx.cwd);
701
- const { diagrams, vendor } = await ensureDiagramDir(ctx.cwd, viewerVendorAssetPath());
702
- const templateText = await bundledViewerTemplate();
703
-
704
- const controller = new AbortController();
705
713
  const cancel = () => controller.abort(signal?.reason ?? new Error("dc_diagram cancelled"));
706
714
  if (signal?.aborted) cancel();
707
715
  else signal?.addEventListener("abort", cancel, { once: true });
708
- activeController = controller;
709
- activeDescription = `diagram ${name} (${grades.join("+")})`;
710
-
711
- const run = (command: string, args: string[], options?: { timeout?: number }) => pi.exec(command, args, options);
712
- let chrome: string | undefined;
713
- let chromeResolved = false;
714
716
 
715
717
  try {
718
+ const requested = resolveGrade(String(params.grade ?? "")) ?? "presentation";
719
+ const grades: DiagramGrade[] = requested === "both" ? ["presentation", "specification"] : [requested];
720
+ const view: MermaidView = params.view === "sequence" ? "sequence" : "flow";
721
+
722
+ const sourcePath = await resolveDiagramSource(ctx.cwd, params.dml);
723
+ if (!ctx.model) {
724
+ return {
725
+ content: [{ type: "text", text: "dc_diagram requires an active pi model. Select one and try again." }],
726
+ details: { success: false, error: "no_model" },
727
+ };
728
+ }
729
+
730
+ const config = await loadConfig(getPaths(ctx.cwd).config);
731
+ const source = await readFile(sourcePath, "utf8");
732
+ const display = displayPath(ctx.cwd, sourcePath);
733
+ const targets = await collectDiagramTargets(ctx.cwd, [sourcePath]);
734
+ const name = diagramNameFor(sourcePath, targets, ctx.cwd);
735
+ const { diagrams, vendor } = await ensureDiagramDir(ctx.cwd, viewerVendorAssetPath());
736
+ const templateText = await bundledViewerTemplate();
737
+
738
+ activeDescription = `diagram ${name} (${grades.join("+")})`;
739
+
740
+ const run = (command: string, args: string[], options?: { timeout?: number }) => pi.exec(command, args, options);
741
+ let chrome: string | undefined;
742
+ let chromeResolved = false;
743
+
716
744
  for (const grade of grades) {
745
+ // The specification seed carries the core decision logic and rule
746
+ // facts; the presentation seed stays small so the model can keep
747
+ // it to a general-audience overview.
748
+ const seed = view === "sequence"
749
+ ? renderSequence(display, source)
750
+ : renderDml(display, source, { hideOutput: true, includeLogic: grade === "specification" });
717
751
  const result = await polishDiagram({
718
752
  grade,
719
753
  view,
@@ -758,8 +792,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
758
792
  return { content: [{ type: "text", text: `dc_diagram failed: ${message}` }], details: { success: false, error: message } };
759
793
  } finally {
760
794
  signal?.removeEventListener("abort", cancel);
761
- activeController = undefined;
762
- activeDescription = undefined;
795
+ releaseExecution(controller);
763
796
  }
764
797
  },
765
798
  });
@@ -833,13 +866,12 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
833
866
  args: string[] = [],
834
867
  options: { verifyCommands?: string[]; piAgentStep?: boolean; changeJsonPath?: string } = {},
835
868
  ): Promise<string> => {
836
- const paths = await initializeWorkspace(ctx.cwd);
837
- const config = await loadConfig(paths.config);
838
- const filePath = await resolveDmlPath(paths, skill);
839
- const controller = new AbortController();
840
- activeController = controller;
841
- activeDescription = `running ${skill}`;
869
+ const controller = claimExecution(`running ${skill}`);
870
+ if (!controller) throw new Error("Another DeepClause execution is already active");
842
871
  try {
872
+ const paths = await initializeWorkspace(ctx.cwd);
873
+ const config = await loadConfig(paths.config);
874
+ const filePath = await resolveDmlPath(paths, skill);
843
875
  const result = await executeDml(
844
876
  filePath,
845
877
  args,
@@ -859,8 +891,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
859
891
  if (result.errors.length) throw new Error(result.errors.join("\n"));
860
892
  return result.answer ?? "(no answer)";
861
893
  } finally {
862
- activeController = undefined;
863
- activeDescription = undefined;
894
+ releaseExecution(controller);
864
895
  }
865
896
  };
866
897
 
@@ -1287,8 +1318,8 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
1287
1318
  pi.registerCommand("dc-run", {
1288
1319
  description: "Run a DML skill with pi's active model",
1289
1320
  handler: async (rawArgs, ctx) => {
1290
-
1291
- if (activeController) {
1321
+ const controller = claimExecution("dc-run");
1322
+ if (!controller) {
1292
1323
  ctx.ui.notify("A DeepClause execution is already active", "warning");
1293
1324
  return;
1294
1325
  }
@@ -1329,8 +1360,6 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
1329
1360
  mode,
1330
1361
  config.branchMessageLimit,
1331
1362
  );
1332
- const controller = new AbortController();
1333
- activeController = controller;
1334
1363
  const outputLines: string[] = [];
1335
1364
  const recentEvents: string[] = [];
1336
1365
  const events: Array<Record<string, unknown>> = [];
@@ -1409,7 +1438,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
1409
1438
  onEvent,
1410
1439
  onDiagnostic,
1411
1440
  onInput: async (prompt, signal) => {
1412
- const answer = await ctx.ui.input("DeepClause input", prompt, { signal });
1441
+ const answer = await requestDeepClauseInput(ctx, skillName, prompt, signal);
1413
1442
  if (answer === undefined) throw new Error("Input cancelled");
1414
1443
  return answer;
1415
1444
  },
@@ -1437,8 +1466,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
1437
1466
  publishResult(pi, `DeepClause execution failed: ${message}`, { error: message });
1438
1467
  ctx.ui.notify(message, activeController?.signal.aborted ? "warning" : "error");
1439
1468
  } finally {
1440
- activeController = undefined;
1441
- activeDescription = undefined;
1469
+ releaseExecution(controller);
1442
1470
  ctx.ui.setStatus(STATUS_KEY, undefined);
1443
1471
  ctx.ui.setWidget(WIDGET_KEY, undefined);
1444
1472
  }