@selesai/code 0.5.16 → 0.5.18
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/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +6 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/extensions/workflow/adapter.ts +221 -196
- package/dist/extensions/workflow/extension.ts +0 -2
- package/dist/extensions/workflow/modes/prototype.ts +1 -3
- package/dist/extensions/workflow/modes/quick.ts +1 -3
- package/dist/extensions/workflow/modes/task.ts +1 -13
- package/dist/extensions/workflow/state-machine.ts +1 -6
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +1 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.test.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode.test.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-mode.test.js +45 -0
- package/dist/modes/interactive/interactive-mode.test.js.map +1 -0
- package/dist/skills/workflow-creation/SKILL.md +2 -2
- package/docs/workflows.md +10 -23
- package/package.json +1 -1
|
@@ -88,8 +88,6 @@ function disableSubagentOutput(input: Record<string, unknown>): void {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export interface WorkflowAdapterOptions {
|
|
91
|
-
toolNames: { start: string; resume: string; end: string };
|
|
92
|
-
toolLabels: { start: string; resume: string; end: string };
|
|
93
91
|
commandName: string;
|
|
94
92
|
commandDescription: string;
|
|
95
93
|
}
|
|
@@ -108,14 +106,20 @@ interface WorkflowController {
|
|
|
108
106
|
}
|
|
109
107
|
|
|
110
108
|
const WORKFLOW_ARTIFACT_TOOL = "write_workflow_artifact";
|
|
109
|
+
const WORKFLOW_CONTROL_MESSAGE = "selesai-workflow-control";
|
|
111
110
|
// ponytail: shared across all module copies so prototype + quick registration
|
|
112
111
|
// can race. Module-level state alone is not enough when the loader runs each
|
|
113
112
|
// extension in its own module record.
|
|
114
113
|
const WORKFLOW_GLOBAL = Symbol.for("selesai.workflow.registry.v1");
|
|
115
|
-
type WorkflowRegistry = {
|
|
114
|
+
type WorkflowRegistry = {
|
|
115
|
+
controllers: WorkflowController[];
|
|
116
|
+
writerRegisteredFor: WeakSet<object>;
|
|
117
|
+
toolsRegisteredFor: WeakSet<object>;
|
|
118
|
+
};
|
|
116
119
|
const registry: WorkflowRegistry = ((globalThis as any)[WORKFLOW_GLOBAL] ??= {
|
|
117
120
|
controllers: [],
|
|
118
121
|
writerRegisteredFor: new WeakSet(),
|
|
122
|
+
toolsRegisteredFor: new WeakSet(),
|
|
119
123
|
});
|
|
120
124
|
|
|
121
125
|
// ponytail: tests call this between cases to drop pi references and reset
|
|
@@ -371,21 +375,43 @@ function applyFooter(ctx: ExtensionContext, footer: FooterState): void {
|
|
|
371
375
|
ctx.ui.setStatus(footer.statusKey, footerText(footer, ctx));
|
|
372
376
|
}
|
|
373
377
|
|
|
374
|
-
function continueAgent(pi: ExtensionAPI,
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
378
|
+
function continueAgent(pi: ExtensionAPI, _ctx: ExtensionContext, prompt: string): void {
|
|
379
|
+
// Engine prompts are hidden custom messages. steer delivers after the current
|
|
380
|
+
// tool batch; triggerTurn starts the continuation when the agent is idle.
|
|
381
|
+
pi.sendMessage(
|
|
382
|
+
{ customType: WORKFLOW_CONTROL_MESSAGE, content: prompt, display: false },
|
|
383
|
+
{ triggerTurn: true, deliverAs: "steer" },
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function isSoleToolCall(ctx: ExtensionContext, toolCallId: unknown): boolean {
|
|
388
|
+
if (typeof toolCallId !== "string") return false;
|
|
389
|
+
try {
|
|
390
|
+
const manager = (ctx as any)?.sessionManager;
|
|
391
|
+
if (!manager || typeof manager.getBranch !== "function") return false;
|
|
392
|
+
const entries = manager.getBranch();
|
|
393
|
+
if (!Array.isArray(entries)) return false;
|
|
394
|
+
const assistant = [...entries].reverse().find((entry: any) =>
|
|
395
|
+
entry?.type === "message" && entry.message?.role === "assistant",
|
|
396
|
+
)?.message;
|
|
397
|
+
if (!assistant || !Array.isArray(assistant.content)) return false;
|
|
398
|
+
const calls = assistant.content.filter((part: any) => part?.type === "toolCall");
|
|
399
|
+
return calls.length === 1 && calls[0]?.id === toolCallId;
|
|
400
|
+
} catch {
|
|
401
|
+
return false;
|
|
383
402
|
}
|
|
384
403
|
}
|
|
385
404
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
405
|
+
function transitionBatchBlock(ctx: ExtensionContext, toolCallId: unknown): { block: true; reason: string } | undefined {
|
|
406
|
+
if (isSoleToolCall(ctx, toolCallId)) return undefined;
|
|
407
|
+
return {
|
|
408
|
+
block: true,
|
|
409
|
+
reason: "Workflow transition tools must be called alone. Retry this call without any other tool calls in the same assistant turn.",
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Apply side effects for effects that carry entry/footer/prompt. Engine prompts
|
|
414
|
+
// are hidden custom messages; tool results remain concise and user-facing.
|
|
389
415
|
function applyEffect(
|
|
390
416
|
pi: ExtensionAPI,
|
|
391
417
|
ctx: ExtensionContext,
|
|
@@ -461,7 +487,7 @@ function commandHelp(config: WorkflowConfig, options: WorkflowAdapterOptions): s
|
|
|
461
487
|
`${command} resume — list resumable runs.`,
|
|
462
488
|
`${command} resume <run-id|artifact-dir|workflow.json> — explicitly resume one.`,
|
|
463
489
|
`Phases advance when their artifacts are written. Runs never auto-resume after reload.`,
|
|
464
|
-
|
|
490
|
+
"Only one run may be attached; end_workflow explicitly completes a terminal-ready run.",
|
|
465
491
|
].join("\n");
|
|
466
492
|
}
|
|
467
493
|
|
|
@@ -506,11 +532,10 @@ function registerSharedArtifactWriter(pi: ExtensionAPI): void {
|
|
|
506
532
|
details: { mode: controller.config.mode, phase: snap.phase, path, persistenceError: true },
|
|
507
533
|
};
|
|
508
534
|
}
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
applyControllerEffect(controller, ctx, eff, { queuePrompt: queueNextPhase });
|
|
535
|
+
// Every valid artifact boundary is an engine transition. Queue exactly
|
|
536
|
+
// one hidden continuation and terminate this assistant turn.
|
|
537
|
+
const transition = eff.kind === "advanced" || eff.kind === "terminalReady" || eff.kind === "terminalNeedsArtifacts";
|
|
538
|
+
applyControllerEffect(controller, ctx, eff, { queuePrompt: transition });
|
|
514
539
|
if (eff.kind === "blocked" && eff.reason) {
|
|
515
540
|
return {
|
|
516
541
|
content: [{ type: "text", text: `Wrote ${path}, but it is not approved: ${eff.reason}. Re-write it via write_workflow_artifact to add the required marker.` }],
|
|
@@ -520,14 +545,10 @@ function registerSharedArtifactWriter(pi: ExtensionAPI): void {
|
|
|
520
545
|
const advanced = eff.kind === "advanced";
|
|
521
546
|
return {
|
|
522
547
|
content: [{ type: "text", text: advanced
|
|
523
|
-
?
|
|
524
|
-
? `Wrote ${path}. Phase advanced to ${eff.phase}; the next workflow phase is queued.`
|
|
525
|
-
: `Wrote ${path}. Phase advanced to ${eff.phase}; wait for the user to continue the workflow.`
|
|
548
|
+
? `Wrote ${path}. Phase advanced to ${eff.phase}; the next workflow phase is queued.`
|
|
526
549
|
: `Wrote ${path}.` }],
|
|
527
550
|
details: { mode: controller.config.mode, phase: snap.phase, path, file, advanced },
|
|
528
|
-
|
|
529
|
-
// its loop prompt, so terminating here would discard that continuation.
|
|
530
|
-
terminate: advanced && !queueNextPhase,
|
|
551
|
+
terminate: transition,
|
|
531
552
|
};
|
|
532
553
|
},
|
|
533
554
|
renderResult(result, _options, theme) {
|
|
@@ -538,6 +559,156 @@ function registerSharedArtifactWriter(pi: ExtensionAPI): void {
|
|
|
538
559
|
} satisfies ToolDefinition);
|
|
539
560
|
}
|
|
540
561
|
|
|
562
|
+
function controllerForMode(pi: ExtensionAPI, mode: string): WorkflowController | undefined {
|
|
563
|
+
return registry.controllers.find((controller) =>
|
|
564
|
+
controller.pi === pi && controller.config.mode === mode && isRegisteredController(controller),
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function unknownMode(mode: string) {
|
|
569
|
+
return {
|
|
570
|
+
content: [{ type: "text" as const, text: `Unknown workflow mode: ${mode}. Use an installed mode (prototype, quick, or task).` }],
|
|
571
|
+
details: { rejected: true, mode },
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function startController(controller: WorkflowController, goal: string, ctx: ExtensionContext) {
|
|
576
|
+
const { pi, config, sm, deps } = controller;
|
|
577
|
+
const other = activeControllersFor(pi).find((candidate) => candidate.sm !== sm);
|
|
578
|
+
if (other) {
|
|
579
|
+
return {
|
|
580
|
+
content: [{ type: "text" as const, text: `A ${other.config.mode} workflow is already active (phase: ${other.sm.snapshot.phase}). Close it before starting ${config.mode}.` }],
|
|
581
|
+
details: { phase: other.sm.snapshot.phase, alreadyActive: true },
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
const before = checkpoint(controller);
|
|
585
|
+
const eff = await sm.start(goal, deps);
|
|
586
|
+
if (eff.kind === "started") {
|
|
587
|
+
controller.run = {
|
|
588
|
+
version: 1,
|
|
589
|
+
id: basename(sm.snapshot.artifactDir),
|
|
590
|
+
mode: config.mode,
|
|
591
|
+
status: "active",
|
|
592
|
+
goal: sm.snapshot.userPrompt,
|
|
593
|
+
artifactDir: sm.snapshot.artifactDir,
|
|
594
|
+
phase: sm.snapshot.phase,
|
|
595
|
+
autoArmed: sm.snapshot.autoArmed,
|
|
596
|
+
createdAt: new Date().toISOString(),
|
|
597
|
+
updatedAt: new Date().toISOString(),
|
|
598
|
+
};
|
|
599
|
+
try {
|
|
600
|
+
await persistAfter(controller, before);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
return {
|
|
603
|
+
content: [{ type: "text" as const, text: `Could not start durable workflow: ${error instanceof Error ? error.message : String(error)}` }],
|
|
604
|
+
details: { persistenceError: true },
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
controller.seenToolCallIds.clear();
|
|
608
|
+
controller.lastBlockedKey = undefined;
|
|
609
|
+
}
|
|
610
|
+
applyControllerEffect(controller, ctx, eff, { queuePrompt: false });
|
|
611
|
+
if (eff.kind === "alreadyActive") {
|
|
612
|
+
return {
|
|
613
|
+
content: [{ type: "text" as const, text: `A ${config.mode} workflow is already active (phase: ${eff.phase}). Do not call start_workflow again. Phases auto-advance as artifacts land; call end_workflow only from the final phase.` }],
|
|
614
|
+
details: { phase: eff.phase, alreadyActive: true },
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
content: [{ type: "text" as const, text: `${config.footerLabel} workflow started. ${eff.prompt}` }],
|
|
619
|
+
details: { mode: config.mode, phase: eff.phase },
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function endController(controller: WorkflowController, ctx: ExtensionContext) {
|
|
624
|
+
const { config, sm, deps } = controller;
|
|
625
|
+
const before = checkpoint(controller);
|
|
626
|
+
const eff = await sm.end(deps);
|
|
627
|
+
if (eff.kind === "closed") {
|
|
628
|
+
try {
|
|
629
|
+
await persistAfter(controller, before);
|
|
630
|
+
} catch (error) {
|
|
631
|
+
return {
|
|
632
|
+
content: [{ type: "text" as const, text: `Cannot end: workflow state could not be persisted: ${error instanceof Error ? error.message : String(error)}` }],
|
|
633
|
+
details: { persistenceError: true },
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
applyControllerEffect(controller, ctx, eff);
|
|
638
|
+
if (eff.kind === "closed") {
|
|
639
|
+
controller.seenToolCallIds.clear();
|
|
640
|
+
controller.lastBlockedKey = undefined;
|
|
641
|
+
return {
|
|
642
|
+
content: [{ type: "text" as const, text: `Workflow ended and closed. It can no longer be used. Artifacts saved at ${eff.artifactDir}.` }],
|
|
643
|
+
details: { closed: true, mode: config.mode, phase: eff.phase, artifactDir: eff.artifactDir },
|
|
644
|
+
terminate: true,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
if (eff.kind === "endBlocked") {
|
|
648
|
+
if (eff.reason) {
|
|
649
|
+
return {
|
|
650
|
+
content: [{ type: "text" as const, text: `Cannot end: ${eff.missing} is incomplete: ${eff.reason}.` }],
|
|
651
|
+
details: { mode: config.mode, phase: eff.phase, blocked: eff.missing, reason: eff.reason },
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
const isArtifactFile = !eff.missing.includes(" ") && eff.missing.includes(".");
|
|
655
|
+
const hint = isArtifactFile
|
|
656
|
+
? ` Write ${sm.snapshot.artifactDir}/${eff.missing} first.`
|
|
657
|
+
: ` Continue through the phases to ${config.phases[config.phases.length - 1]}.`;
|
|
658
|
+
return {
|
|
659
|
+
content: [{ type: "text" as const, text: `Cannot end: ${eff.missing}.${hint}` }],
|
|
660
|
+
details: { mode: config.mode, phase: eff.phase, blocked: eff.missing },
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
return {
|
|
664
|
+
content: [{ type: "text" as const, text: `No active ${config.mode} workflow to end.` }],
|
|
665
|
+
details: { active: false, mode: config.mode },
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function registerSharedWorkflowTools(pi: ExtensionAPI): void {
|
|
670
|
+
if (registry.toolsRegisteredFor.has(pi)) return;
|
|
671
|
+
registry.toolsRegisteredFor.add(pi);
|
|
672
|
+
pi.registerTool({
|
|
673
|
+
name: "start_workflow",
|
|
674
|
+
label: "Start Workflow",
|
|
675
|
+
description: "Start a durable workflow. Select prototype, quick, or task; do not call while another workflow is active.",
|
|
676
|
+
parameters: Type.Object({
|
|
677
|
+
mode: Type.String({ description: "Workflow mode: prototype, quick, task, or another installed mode." }),
|
|
678
|
+
goal: Type.String(),
|
|
679
|
+
}),
|
|
680
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
681
|
+
const controller = controllerForMode(pi, params.mode);
|
|
682
|
+
return controller ? startController(controller, params.goal, ctx) : unknownMode(params.mode);
|
|
683
|
+
},
|
|
684
|
+
} satisfies ToolDefinition);
|
|
685
|
+
pi.registerTool({
|
|
686
|
+
name: "resume_workflow",
|
|
687
|
+
label: "Resume Workflow",
|
|
688
|
+
description: "Resume a durable workflow by mode and selected run id, artifact directory, or workflow.json path.",
|
|
689
|
+
parameters: Type.Object({
|
|
690
|
+
mode: Type.String({ description: "Workflow mode: prototype, quick, task, or another installed mode." }),
|
|
691
|
+
run: Type.String(),
|
|
692
|
+
}),
|
|
693
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
694
|
+
const controller = controllerForMode(pi, params.mode);
|
|
695
|
+
return controller ? resumeController(controller, ctx, params.run, "end_workflow") : unknownMode(params.mode);
|
|
696
|
+
},
|
|
697
|
+
} satisfies ToolDefinition);
|
|
698
|
+
pi.registerTool({
|
|
699
|
+
name: "end_workflow",
|
|
700
|
+
label: "End Workflow",
|
|
701
|
+
description: "Close a terminal-ready durable workflow. Select its mode.",
|
|
702
|
+
parameters: Type.Object({
|
|
703
|
+
mode: Type.String({ description: "Workflow mode: prototype, quick, task, or another installed mode." }),
|
|
704
|
+
}),
|
|
705
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
706
|
+
const controller = controllerForMode(pi, params.mode);
|
|
707
|
+
return controller ? endController(controller, ctx) : unknownMode(params.mode);
|
|
708
|
+
},
|
|
709
|
+
} satisfies ToolDefinition);
|
|
710
|
+
}
|
|
711
|
+
|
|
541
712
|
export function createWorkflowExtension(
|
|
542
713
|
config: WorkflowConfig,
|
|
543
714
|
options: WorkflowAdapterOptions,
|
|
@@ -558,171 +729,12 @@ export function createWorkflowExtension(
|
|
|
558
729
|
const controller: WorkflowController = { pi, config, sm, deps, seenToolCallIds: new Set() };
|
|
559
730
|
registry.controllers.push(controller);
|
|
560
731
|
registerSharedArtifactWriter(pi);
|
|
561
|
-
|
|
732
|
+
registerSharedWorkflowTools(pi);
|
|
562
733
|
const { mode, footerLabel } = config;
|
|
734
|
+
const end = "end_workflow";
|
|
563
735
|
|
|
564
|
-
//
|
|
565
|
-
|
|
566
|
-
name: start,
|
|
567
|
-
label: options.toolLabels.start,
|
|
568
|
-
description: `Start the ${mode} workflow for the given goal. Sets up ${config.phases[0]} as the first phase and returns its prompt. Do not call if a workflow is already active.`,
|
|
569
|
-
parameters: Type.Object({
|
|
570
|
-
goal: Type.String({
|
|
571
|
-
description: `What the user wants the ${mode} workflow to build or accomplish.`,
|
|
572
|
-
}),
|
|
573
|
-
}),
|
|
574
|
-
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
575
|
-
const other = activeControllersFor(pi).find((c) => c.sm !== sm);
|
|
576
|
-
if (other) {
|
|
577
|
-
return {
|
|
578
|
-
content: [{ type: "text", text: `A ${other.config.mode} workflow is already active (phase: ${other.sm.snapshot.phase}). Close it before starting ${mode}.` }],
|
|
579
|
-
details: { phase: other.sm.snapshot.phase, alreadyActive: true },
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
const before = checkpoint(controller);
|
|
583
|
-
const eff = await sm.start(params.goal, deps);
|
|
584
|
-
if (eff.kind === "started") {
|
|
585
|
-
controller.run = {
|
|
586
|
-
version: 1,
|
|
587
|
-
id: basename(sm.snapshot.artifactDir),
|
|
588
|
-
mode,
|
|
589
|
-
status: "active",
|
|
590
|
-
goal: sm.snapshot.userPrompt,
|
|
591
|
-
artifactDir: sm.snapshot.artifactDir,
|
|
592
|
-
phase: sm.snapshot.phase,
|
|
593
|
-
autoArmed: sm.snapshot.autoArmed,
|
|
594
|
-
createdAt: new Date().toISOString(),
|
|
595
|
-
updatedAt: new Date().toISOString(),
|
|
596
|
-
};
|
|
597
|
-
try {
|
|
598
|
-
await persistAfter(controller, before);
|
|
599
|
-
} catch (error) {
|
|
600
|
-
return {
|
|
601
|
-
content: [{ type: "text", text: `Could not start durable workflow: ${error instanceof Error ? error.message : String(error)}` }],
|
|
602
|
-
details: { persistenceError: true },
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
controller.seenToolCallIds.clear();
|
|
606
|
-
controller.lastBlockedKey = undefined;
|
|
607
|
-
}
|
|
608
|
-
// The start tool result already contains the grilling prompt. Queueing
|
|
609
|
-
// it again creates a duplicate autonomous turn.
|
|
610
|
-
applyControllerEffect(controller, ctx, eff, { queuePrompt: false });
|
|
611
|
-
if (eff.kind === "alreadyActive") {
|
|
612
|
-
return {
|
|
613
|
-
content: [
|
|
614
|
-
{
|
|
615
|
-
type: "text",
|
|
616
|
-
text: `A ${mode} workflow is already active (phase: ${eff.phase}). Do not call ${start} again. Phases auto-advance as artifacts land; call ${end} only from the final phase.`,
|
|
617
|
-
},
|
|
618
|
-
],
|
|
619
|
-
details: { phase: eff.phase, alreadyActive: true },
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
return {
|
|
623
|
-
content: [
|
|
624
|
-
{ type: "text", text: `${footerLabel} workflow started. ${eff.prompt}` },
|
|
625
|
-
],
|
|
626
|
-
details: { phase: eff.phase },
|
|
627
|
-
};
|
|
628
|
-
},
|
|
629
|
-
renderResult(_result, _options, theme) {
|
|
630
|
-
return new Text(
|
|
631
|
-
theme.fg("warning", `● ${footerLabel} started · 1/${config.phases.length} ${config.phases[0]}`),
|
|
632
|
-
0,
|
|
633
|
-
0,
|
|
634
|
-
);
|
|
635
|
-
},
|
|
636
|
-
} satisfies ToolDefinition);
|
|
637
|
-
|
|
638
|
-
// ── explicit resume tool ──
|
|
639
|
-
pi.registerTool({
|
|
640
|
-
name: resume,
|
|
641
|
-
label: options.toolLabels.resume,
|
|
642
|
-
description: `Resume an explicitly selected ${mode} workflow run. Pass a run id, artifact directory, or workflow.json path.`,
|
|
643
|
-
parameters: Type.Object({ run: Type.String({ description: "Run id, artifact directory, or workflow.json path." }) }),
|
|
644
|
-
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
645
|
-
return resumeController(controller, ctx, params.run, end);
|
|
646
|
-
},
|
|
647
|
-
renderResult(result, _options, theme) {
|
|
648
|
-
const d = result.details as { rejected?: boolean; persistenceError?: boolean; phase?: string };
|
|
649
|
-
const failed = d.rejected || d.persistenceError;
|
|
650
|
-
return new Text(theme.fg(failed ? "warning" : "success", failed ? "○ workflow resume rejected" : `✓ workflow resumed · ${d.phase}`), 0, 0);
|
|
651
|
-
},
|
|
652
|
-
} satisfies ToolDefinition);
|
|
653
|
-
|
|
654
|
-
// ── end tool ──
|
|
655
|
-
pi.registerTool({
|
|
656
|
-
name: end,
|
|
657
|
-
label: options.toolLabels.end,
|
|
658
|
-
description: `Close the ${mode} workflow. Must be called when the final phase is complete. Marks the workflow finished and stops the agent loop.`,
|
|
659
|
-
parameters: Type.Object({}),
|
|
660
|
-
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
661
|
-
const before = checkpoint(controller);
|
|
662
|
-
const eff = await sm.end(deps);
|
|
663
|
-
if (eff.kind === "closed") {
|
|
664
|
-
try {
|
|
665
|
-
await persistAfter(controller, before);
|
|
666
|
-
} catch (error) {
|
|
667
|
-
return {
|
|
668
|
-
content: [{ type: "text", text: `Cannot end: workflow state could not be persisted: ${error instanceof Error ? error.message : String(error)}` }],
|
|
669
|
-
details: { persistenceError: true },
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
applyControllerEffect(controller, ctx, eff);
|
|
674
|
-
if (eff.kind === "closed") {
|
|
675
|
-
controller.seenToolCallIds.clear();
|
|
676
|
-
controller.lastBlockedKey = undefined;
|
|
677
|
-
}
|
|
678
|
-
switch (eff.kind) {
|
|
679
|
-
case "closed":
|
|
680
|
-
return {
|
|
681
|
-
content: [{ type: "text", text: `Workflow ended and closed. It can no longer be used. Artifacts saved at ${eff.artifactDir}.` }],
|
|
682
|
-
details: { closed: true, phase: eff.phase, artifactDir: eff.artifactDir },
|
|
683
|
-
terminate: true,
|
|
684
|
-
};
|
|
685
|
-
case "endBlocked": {
|
|
686
|
-
// ponytail: missing is either a filename (existence failure),
|
|
687
|
-
// a phase description like "not at terminal (audit)", or — Plan 4 —
|
|
688
|
-
// a filename whose validator failed (reason set). Only suggest
|
|
689
|
-
// writing when it's an actual artifact file existence miss.
|
|
690
|
-
if (eff.reason) {
|
|
691
|
-
return {
|
|
692
|
-
content: [{ type: "text", text: `Cannot end: ${eff.missing} is incomplete: ${eff.reason}.` }],
|
|
693
|
-
details: { phase: eff.phase, blocked: eff.missing, reason: eff.reason },
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
const isArtifactFile =
|
|
697
|
-
!eff.missing.includes(" ") && eff.missing.includes(".");
|
|
698
|
-
const hint = isArtifactFile
|
|
699
|
-
? ` Write ${sm.snapshot.artifactDir}/${eff.missing} first.`
|
|
700
|
-
: ` Continue through the phases to ${config.phases[config.phases.length - 1]}.`;
|
|
701
|
-
return {
|
|
702
|
-
content: [{ type: "text", text: `Cannot end: ${eff.missing}.${hint}` }],
|
|
703
|
-
details: { phase: eff.phase, blocked: eff.missing },
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
default:
|
|
707
|
-
return {
|
|
708
|
-
content: [{ type: "text", text: `No active ${mode} workflow to end.` }],
|
|
709
|
-
details: { active: false },
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
},
|
|
713
|
-
renderResult(result, _options, theme) {
|
|
714
|
-
const d = result.details as { closed?: boolean; artifactDir?: string };
|
|
715
|
-
if (!d.closed) {
|
|
716
|
-
return new Text(theme.fg("dim", "○ no workflow to end"), 0, 0);
|
|
717
|
-
}
|
|
718
|
-
return new Text(
|
|
719
|
-
theme.fg("success", `✓ ${footerLabel} closed · ${d.artifactDir}`),
|
|
720
|
-
0,
|
|
721
|
-
0,
|
|
722
|
-
);
|
|
723
|
-
},
|
|
724
|
-
} satisfies ToolDefinition);
|
|
725
|
-
|
|
736
|
+
// Workflow lifecycle tools are registered once per Pi instance below.
|
|
737
|
+
// They dispatch by `mode`; mode-specific tool aliases are intentionally absent.
|
|
726
738
|
// ── session_start: disk state is never auto-attached. A session entry is
|
|
727
739
|
// merely a convenience pointer for rendering a stale-but-useful footer. ──
|
|
728
740
|
pi.on("session_start", async (_event: any, ctx: ExtensionContext) => {
|
|
@@ -747,13 +759,23 @@ export function createWorkflowExtension(
|
|
|
747
759
|
}
|
|
748
760
|
});
|
|
749
761
|
|
|
750
|
-
// ── tool_call: enforce parent-owned artifact boundaries
|
|
751
|
-
|
|
762
|
+
// ── tool_call: enforce parent-owned artifact boundaries and exclusive
|
|
763
|
+
// transition calls. The session manager exposes the current assistant
|
|
764
|
+
// message, so fail closed when its sole-call shape cannot be proven. ──
|
|
765
|
+
pi.on("tool_call", (event: any, ctx: ExtensionContext) => {
|
|
752
766
|
if (!isRegisteredController(controller)) return;
|
|
753
767
|
const tool = event.toolName;
|
|
754
|
-
if (tool !== "subagent" && tool !== "write" && tool !== "edit") return;
|
|
755
768
|
const snap = sm.snapshot;
|
|
756
769
|
if (!snap.active) return;
|
|
770
|
+
const commentatorTransition = tool === "subagent" &&
|
|
771
|
+
event.input?.agent === "commentator" && snap.phase === "loop" &&
|
|
772
|
+
!isSubagentManagementAction(event.input);
|
|
773
|
+
const endTransition = tool === "end_workflow" && event.input?.mode === mode;
|
|
774
|
+
if (tool === WORKFLOW_ARTIFACT_TOOL || endTransition || commentatorTransition) {
|
|
775
|
+
const blocked = transitionBatchBlock(ctx, event.toolCallId);
|
|
776
|
+
if (blocked) return blocked;
|
|
777
|
+
}
|
|
778
|
+
if (tool !== "subagent" && tool !== "write" && tool !== "edit") return;
|
|
757
779
|
if (tool === "write" || tool === "edit") {
|
|
758
780
|
return {
|
|
759
781
|
block: true,
|
|
@@ -846,8 +868,11 @@ export function createWorkflowExtension(
|
|
|
846
868
|
}
|
|
847
869
|
// The only artifact written in this handler is loop-complete.md, which
|
|
848
870
|
// is engine-owned. Parent-written phase artifacts advance inside
|
|
849
|
-
// write_workflow_artifact instead.
|
|
850
|
-
|
|
871
|
+
// write_workflow_artifact instead. A clean review is an exclusive
|
|
872
|
+
// transition boundary, so terminate and let the hidden steer continue.
|
|
873
|
+
const transition = eff.kind === "advanced" || eff.kind === "terminalReady" || eff.kind === "terminalNeedsArtifacts";
|
|
874
|
+
applyControllerEffect(controller, ctx, eff, { queuePrompt: transition });
|
|
875
|
+
return transition ? { terminate: true } : undefined;
|
|
851
876
|
});
|
|
852
877
|
|
|
853
878
|
// ── /<command> ──
|
|
@@ -932,7 +957,7 @@ export function createWorkflowExtension(
|
|
|
932
957
|
const closed = sm.closeCurrent();
|
|
933
958
|
if (closed) {
|
|
934
959
|
// Detach only: the on-disk active record remains resumable. Only
|
|
935
|
-
//
|
|
960
|
+
// end_workflow({ mode }) writes status: completed.
|
|
936
961
|
applyEntry(pi, config, { ...closed, done: false }, controller.run);
|
|
937
962
|
ctx.ui.notify(`Detached ${mode} workflow at ${closed.artifactDir}; it remains resumable.`, "info");
|
|
938
963
|
}
|
|
@@ -16,8 +16,6 @@ const MODES = [prototypeMode, quickMode, taskMode] as const;
|
|
|
16
16
|
export default function workflowModesExtension(pi: ExtensionAPI): void {
|
|
17
17
|
for (const mode of MODES) {
|
|
18
18
|
createWorkflowExtension(mode.config, {
|
|
19
|
-
toolNames: mode.toolNames,
|
|
20
|
-
toolLabels: mode.toolLabels,
|
|
21
19
|
commandName: mode.commandName,
|
|
22
20
|
commandDescription: mode.commandDescription,
|
|
23
21
|
})(pi);
|
|
@@ -135,7 +135,7 @@ Review uncommitted changes for correctness, plan adherence, and over-engineering
|
|
|
135
135
|
2. If that review lists actionable issues, call the subagent tool with { agent: "builder", task: "...", output: false } (do NOT pass a model parameter). Instruct it to fix every issue in the workspace, never in ${artifactDir}, then return its completion summary inline.
|
|
136
136
|
3. Re-dispatch the commentator and overwrite the parent-owned review artifact through write_workflow_artifact until it ends with WORKFLOW_REVIEW_STATUS: clean.
|
|
137
137
|
|
|
138
|
-
Once ${artifactDir}/review.md exists and ends with the WORKFLOW_REVIEW_STATUS: clean marker, call end_workflow to complete the workflow.`,
|
|
138
|
+
Once ${artifactDir}/review.md exists and ends with the WORKFLOW_REVIEW_STATUS: clean marker, call end_workflow with { mode: "prototype" } to complete the workflow.`,
|
|
139
139
|
};
|
|
140
140
|
|
|
141
141
|
const config: WorkflowConfig = {
|
|
@@ -172,8 +172,6 @@ export const prototypeMode: WorkflowModeRegistration = {
|
|
|
172
172
|
commandName: "workflow-prototype",
|
|
173
173
|
commandDescription:
|
|
174
174
|
"Run the prototype workflow (grill → research → plan → reuse → handoff → loop → audit)",
|
|
175
|
-
toolNames: { start: "start_workflow", resume: "resume_workflow", end: "end_workflow" },
|
|
176
|
-
toolLabels: { start: "Start Workflow", resume: "Resume Workflow", end: "End Workflow" },
|
|
177
175
|
};
|
|
178
176
|
|
|
179
177
|
export default prototypeMode;
|
|
@@ -118,7 +118,7 @@ Review uncommitted changes for correctness, plan adherence, and over-engineering
|
|
|
118
118
|
2. If that review lists actionable issues, call the subagent tool with { agent: "builder", task: "...", output: false } (do NOT pass a model parameter). Instruct it to fix every issue in the workspace, never in ${artifactDir}, then return its completion summary inline.
|
|
119
119
|
3. Re-run the commentator and overwrite the parent-owned review artifact through write_workflow_artifact until it ends with WORKFLOW_REVIEW_STATUS: clean.
|
|
120
120
|
|
|
121
|
-
Once ${artifactDir}/review.md exists and ends with the WORKFLOW_REVIEW_STATUS: clean marker, call
|
|
121
|
+
Once ${artifactDir}/review.md exists and ends with the WORKFLOW_REVIEW_STATUS: clean marker, call end_workflow with { mode: "quick" } to complete the workflow.`,
|
|
122
122
|
};
|
|
123
123
|
|
|
124
124
|
const config: WorkflowConfig = {
|
|
@@ -152,8 +152,6 @@ export const quickMode: WorkflowModeRegistration = {
|
|
|
152
152
|
commandName: "workflow-quick",
|
|
153
153
|
commandDescription:
|
|
154
154
|
"Run the quick workflow (grill → plan → reuse → handoff → loop → audit)",
|
|
155
|
-
toolNames: { start: "start_quick_workflow", resume: "resume_quick_workflow", end: "end_quick_workflow" },
|
|
156
|
-
toolLabels: { start: "Start Quick Workflow", resume: "Resume Quick Workflow", end: "End Quick Workflow" },
|
|
157
155
|
};
|
|
158
156
|
|
|
159
157
|
export default quickMode;
|
|
@@ -68,7 +68,7 @@ After the builder returns, call the subagent tool with { agent: "commentator", t
|
|
|
68
68
|
OR
|
|
69
69
|
WORKFLOW_REVIEW_STATUS: blocking
|
|
70
70
|
|
|
71
|
-
If a review is blocking, call the builder again with the recorded issues. This repeats up to ${loopMaxIterations ?? 3} round(s). When a review is clean, the engine writes loop-complete.md and the workflow becomes terminal-ready. Do NOT write loop-complete.md yourself. Call
|
|
71
|
+
If a review is blocking, call the builder again with the recorded issues. This repeats up to ${loopMaxIterations ?? 3} round(s). When a review is clean, the engine writes loop-complete.md and the workflow becomes terminal-ready. Do NOT write loop-complete.md yourself. Call end_workflow with { mode: "task" } to complete the workflow.`,
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
const config: WorkflowConfig = {
|
|
@@ -90,8 +90,6 @@ const config: WorkflowConfig = {
|
|
|
90
90
|
closeValidators: { "loop-complete.md": loopCompleteValidator },
|
|
91
91
|
closeArtifacts: ["loop-complete.md"],
|
|
92
92
|
loopMaxIterations: 3,
|
|
93
|
-
// Each parent-owned artifact automatically queues the next phase prompt.
|
|
94
|
-
continueAfterArtifact: true,
|
|
95
93
|
statusKey: "task",
|
|
96
94
|
entryType: "task-phase",
|
|
97
95
|
footerLabel: "task",
|
|
@@ -102,16 +100,6 @@ export const taskMode: WorkflowModeRegistration = {
|
|
|
102
100
|
commandName: "workflow-task",
|
|
103
101
|
commandDescription:
|
|
104
102
|
"Run the task workflow (plan → reuse → handoff → build↔review loop)",
|
|
105
|
-
toolNames: {
|
|
106
|
-
start: "start_task_workflow",
|
|
107
|
-
resume: "resume_task_workflow",
|
|
108
|
-
end: "end_task_workflow",
|
|
109
|
-
},
|
|
110
|
-
toolLabels: {
|
|
111
|
-
start: "Start Task Workflow",
|
|
112
|
-
resume: "Resume Task Workflow",
|
|
113
|
-
end: "End Task Workflow",
|
|
114
|
-
},
|
|
115
103
|
};
|
|
116
104
|
|
|
117
105
|
export default taskMode;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ponytail: pure workflow state machine. No node:fs, no pi API, no pi-tui,
|
|
2
2
|
// no typebox. Transitions are pure given two injected predicates
|
|
3
3
|
// (artifactExists, shouldSkip). Side effects (appendEntry, setStatus,
|
|
4
|
-
//
|
|
4
|
+
// sendMessage) are NOT called here — every method returns a domain
|
|
5
5
|
// Effect the adapter pattern-matches on and applies. The adapter owns
|
|
6
6
|
// all pi/fs wiring; this module owns the phase graph + gating + reentrancy.
|
|
7
7
|
|
|
@@ -47,8 +47,6 @@ export interface WorkflowModeRegistration {
|
|
|
47
47
|
config: WorkflowConfig;
|
|
48
48
|
commandName: string;
|
|
49
49
|
commandDescription: string;
|
|
50
|
-
toolNames: { start: string; resume: string; end: string };
|
|
51
|
-
toolLabels: { start: string; resume: string; end: string };
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
export interface WorkflowConfig {
|
|
@@ -82,9 +80,6 @@ export interface WorkflowConfig {
|
|
|
82
80
|
// ponytail: Plan 3 — engine-owned loop. Max review rounds before the loop
|
|
83
81
|
// stops and asks the user instead of silently passing. Default 3.
|
|
84
82
|
loopMaxIterations?: number;
|
|
85
|
-
// Most workflows pause at artifact boundaries. A mode can opt into queuing
|
|
86
|
-
// the next phase prompt after the parent writes a valid artifact.
|
|
87
|
-
continueAfterArtifact?: boolean;
|
|
88
83
|
}
|
|
89
84
|
|
|
90
85
|
export interface WorkflowDeps {
|