@selesai/code 0.5.15 → 0.5.17
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/defaults/settings.json +2 -2
- package/dist/extensions/workflow/adapter.ts +162 -168
- 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 -11
- package/dist/extensions/workflow/state-machine.ts +0 -2
- package/dist/skills/workflow-creation/SKILL.md +2 -2
- package/docs/workflows.md +7 -19
- package/package.json +1 -1
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"defaultModel": "tokenin/glm-5.2",
|
|
43
43
|
"agentOverrides": {
|
|
44
44
|
"architect": {
|
|
45
|
-
"model": "tokenin/glm-5.
|
|
45
|
+
"model": "tokenin/glm-5.2"
|
|
46
46
|
},
|
|
47
47
|
"builder": {
|
|
48
48
|
"model": "tokenin/kimi-k2.7-code"
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"model": "tokenin/deepseek-v4-flash"
|
|
58
58
|
},
|
|
59
59
|
"researcher": {
|
|
60
|
-
"model": "tokenin/kimi-k2.7-
|
|
60
|
+
"model": "tokenin/kimi-k2.7-code"
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
},
|
|
@@ -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
|
}
|
|
@@ -112,10 +110,15 @@ const WORKFLOW_ARTIFACT_TOOL = "write_workflow_artifact";
|
|
|
112
110
|
// can race. Module-level state alone is not enough when the loader runs each
|
|
113
111
|
// extension in its own module record.
|
|
114
112
|
const WORKFLOW_GLOBAL = Symbol.for("selesai.workflow.registry.v1");
|
|
115
|
-
type WorkflowRegistry = {
|
|
113
|
+
type WorkflowRegistry = {
|
|
114
|
+
controllers: WorkflowController[];
|
|
115
|
+
writerRegisteredFor: WeakSet<object>;
|
|
116
|
+
toolsRegisteredFor: WeakSet<object>;
|
|
117
|
+
};
|
|
116
118
|
const registry: WorkflowRegistry = ((globalThis as any)[WORKFLOW_GLOBAL] ??= {
|
|
117
119
|
controllers: [],
|
|
118
120
|
writerRegisteredFor: new WeakSet(),
|
|
121
|
+
toolsRegisteredFor: new WeakSet(),
|
|
119
122
|
});
|
|
120
123
|
|
|
121
124
|
// ponytail: tests call this between cases to drop pi references and reset
|
|
@@ -461,7 +464,7 @@ function commandHelp(config: WorkflowConfig, options: WorkflowAdapterOptions): s
|
|
|
461
464
|
`${command} resume — list resumable runs.`,
|
|
462
465
|
`${command} resume <run-id|artifact-dir|workflow.json> — explicitly resume one.`,
|
|
463
466
|
`Phases advance when their artifacts are written. Runs never auto-resume after reload.`,
|
|
464
|
-
|
|
467
|
+
"Only one run may be attached; end_workflow explicitly completes a terminal-ready run.",
|
|
465
468
|
].join("\n");
|
|
466
469
|
}
|
|
467
470
|
|
|
@@ -538,6 +541,156 @@ function registerSharedArtifactWriter(pi: ExtensionAPI): void {
|
|
|
538
541
|
} satisfies ToolDefinition);
|
|
539
542
|
}
|
|
540
543
|
|
|
544
|
+
function controllerForMode(pi: ExtensionAPI, mode: string): WorkflowController | undefined {
|
|
545
|
+
return registry.controllers.find((controller) =>
|
|
546
|
+
controller.pi === pi && controller.config.mode === mode && isRegisteredController(controller),
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function unknownMode(mode: string) {
|
|
551
|
+
return {
|
|
552
|
+
content: [{ type: "text" as const, text: `Unknown workflow mode: ${mode}. Use an installed mode (prototype, quick, or task).` }],
|
|
553
|
+
details: { rejected: true, mode },
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function startController(controller: WorkflowController, goal: string, ctx: ExtensionContext) {
|
|
558
|
+
const { pi, config, sm, deps } = controller;
|
|
559
|
+
const other = activeControllersFor(pi).find((candidate) => candidate.sm !== sm);
|
|
560
|
+
if (other) {
|
|
561
|
+
return {
|
|
562
|
+
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}.` }],
|
|
563
|
+
details: { phase: other.sm.snapshot.phase, alreadyActive: true },
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
const before = checkpoint(controller);
|
|
567
|
+
const eff = await sm.start(goal, deps);
|
|
568
|
+
if (eff.kind === "started") {
|
|
569
|
+
controller.run = {
|
|
570
|
+
version: 1,
|
|
571
|
+
id: basename(sm.snapshot.artifactDir),
|
|
572
|
+
mode: config.mode,
|
|
573
|
+
status: "active",
|
|
574
|
+
goal: sm.snapshot.userPrompt,
|
|
575
|
+
artifactDir: sm.snapshot.artifactDir,
|
|
576
|
+
phase: sm.snapshot.phase,
|
|
577
|
+
autoArmed: sm.snapshot.autoArmed,
|
|
578
|
+
createdAt: new Date().toISOString(),
|
|
579
|
+
updatedAt: new Date().toISOString(),
|
|
580
|
+
};
|
|
581
|
+
try {
|
|
582
|
+
await persistAfter(controller, before);
|
|
583
|
+
} catch (error) {
|
|
584
|
+
return {
|
|
585
|
+
content: [{ type: "text" as const, text: `Could not start durable workflow: ${error instanceof Error ? error.message : String(error)}` }],
|
|
586
|
+
details: { persistenceError: true },
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
controller.seenToolCallIds.clear();
|
|
590
|
+
controller.lastBlockedKey = undefined;
|
|
591
|
+
}
|
|
592
|
+
applyControllerEffect(controller, ctx, eff, { queuePrompt: false });
|
|
593
|
+
if (eff.kind === "alreadyActive") {
|
|
594
|
+
return {
|
|
595
|
+
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.` }],
|
|
596
|
+
details: { phase: eff.phase, alreadyActive: true },
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
content: [{ type: "text" as const, text: `${config.footerLabel} workflow started. ${eff.prompt}` }],
|
|
601
|
+
details: { mode: config.mode, phase: eff.phase },
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function endController(controller: WorkflowController, ctx: ExtensionContext) {
|
|
606
|
+
const { config, sm, deps } = controller;
|
|
607
|
+
const before = checkpoint(controller);
|
|
608
|
+
const eff = await sm.end(deps);
|
|
609
|
+
if (eff.kind === "closed") {
|
|
610
|
+
try {
|
|
611
|
+
await persistAfter(controller, before);
|
|
612
|
+
} catch (error) {
|
|
613
|
+
return {
|
|
614
|
+
content: [{ type: "text" as const, text: `Cannot end: workflow state could not be persisted: ${error instanceof Error ? error.message : String(error)}` }],
|
|
615
|
+
details: { persistenceError: true },
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
applyControllerEffect(controller, ctx, eff);
|
|
620
|
+
if (eff.kind === "closed") {
|
|
621
|
+
controller.seenToolCallIds.clear();
|
|
622
|
+
controller.lastBlockedKey = undefined;
|
|
623
|
+
return {
|
|
624
|
+
content: [{ type: "text" as const, text: `Workflow ended and closed. It can no longer be used. Artifacts saved at ${eff.artifactDir}.` }],
|
|
625
|
+
details: { closed: true, mode: config.mode, phase: eff.phase, artifactDir: eff.artifactDir },
|
|
626
|
+
terminate: true,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
if (eff.kind === "endBlocked") {
|
|
630
|
+
if (eff.reason) {
|
|
631
|
+
return {
|
|
632
|
+
content: [{ type: "text" as const, text: `Cannot end: ${eff.missing} is incomplete: ${eff.reason}.` }],
|
|
633
|
+
details: { mode: config.mode, phase: eff.phase, blocked: eff.missing, reason: eff.reason },
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
const isArtifactFile = !eff.missing.includes(" ") && eff.missing.includes(".");
|
|
637
|
+
const hint = isArtifactFile
|
|
638
|
+
? ` Write ${sm.snapshot.artifactDir}/${eff.missing} first.`
|
|
639
|
+
: ` Continue through the phases to ${config.phases[config.phases.length - 1]}.`;
|
|
640
|
+
return {
|
|
641
|
+
content: [{ type: "text" as const, text: `Cannot end: ${eff.missing}.${hint}` }],
|
|
642
|
+
details: { mode: config.mode, phase: eff.phase, blocked: eff.missing },
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
content: [{ type: "text" as const, text: `No active ${config.mode} workflow to end.` }],
|
|
647
|
+
details: { active: false, mode: config.mode },
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function registerSharedWorkflowTools(pi: ExtensionAPI): void {
|
|
652
|
+
if (registry.toolsRegisteredFor.has(pi)) return;
|
|
653
|
+
registry.toolsRegisteredFor.add(pi);
|
|
654
|
+
pi.registerTool({
|
|
655
|
+
name: "start_workflow",
|
|
656
|
+
label: "Start Workflow",
|
|
657
|
+
description: "Start a durable workflow. Select prototype, quick, or task; do not call while another workflow is active.",
|
|
658
|
+
parameters: Type.Object({
|
|
659
|
+
mode: Type.String({ description: "Workflow mode: prototype, quick, task, or another installed mode." }),
|
|
660
|
+
goal: Type.String(),
|
|
661
|
+
}),
|
|
662
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
663
|
+
const controller = controllerForMode(pi, params.mode);
|
|
664
|
+
return controller ? startController(controller, params.goal, ctx) : unknownMode(params.mode);
|
|
665
|
+
},
|
|
666
|
+
} satisfies ToolDefinition);
|
|
667
|
+
pi.registerTool({
|
|
668
|
+
name: "resume_workflow",
|
|
669
|
+
label: "Resume Workflow",
|
|
670
|
+
description: "Resume a durable workflow by mode and selected run id, artifact directory, or workflow.json path.",
|
|
671
|
+
parameters: Type.Object({
|
|
672
|
+
mode: Type.String({ description: "Workflow mode: prototype, quick, task, or another installed mode." }),
|
|
673
|
+
run: Type.String(),
|
|
674
|
+
}),
|
|
675
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
676
|
+
const controller = controllerForMode(pi, params.mode);
|
|
677
|
+
return controller ? resumeController(controller, ctx, params.run, "end_workflow") : unknownMode(params.mode);
|
|
678
|
+
},
|
|
679
|
+
} satisfies ToolDefinition);
|
|
680
|
+
pi.registerTool({
|
|
681
|
+
name: "end_workflow",
|
|
682
|
+
label: "End Workflow",
|
|
683
|
+
description: "Close a terminal-ready durable workflow. Select its mode.",
|
|
684
|
+
parameters: Type.Object({
|
|
685
|
+
mode: Type.String({ description: "Workflow mode: prototype, quick, task, or another installed mode." }),
|
|
686
|
+
}),
|
|
687
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
688
|
+
const controller = controllerForMode(pi, params.mode);
|
|
689
|
+
return controller ? endController(controller, ctx) : unknownMode(params.mode);
|
|
690
|
+
},
|
|
691
|
+
} satisfies ToolDefinition);
|
|
692
|
+
}
|
|
693
|
+
|
|
541
694
|
export function createWorkflowExtension(
|
|
542
695
|
config: WorkflowConfig,
|
|
543
696
|
options: WorkflowAdapterOptions,
|
|
@@ -558,171 +711,12 @@ export function createWorkflowExtension(
|
|
|
558
711
|
const controller: WorkflowController = { pi, config, sm, deps, seenToolCallIds: new Set() };
|
|
559
712
|
registry.controllers.push(controller);
|
|
560
713
|
registerSharedArtifactWriter(pi);
|
|
561
|
-
|
|
714
|
+
registerSharedWorkflowTools(pi);
|
|
562
715
|
const { mode, footerLabel } = config;
|
|
716
|
+
const end = "end_workflow";
|
|
563
717
|
|
|
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
|
-
|
|
718
|
+
// Workflow lifecycle tools are registered once per Pi instance below.
|
|
719
|
+
// They dispatch by `mode`; mode-specific tool aliases are intentionally absent.
|
|
726
720
|
// ── session_start: disk state is never auto-attached. A session entry is
|
|
727
721
|
// merely a convenience pointer for rendering a stale-but-useful footer. ──
|
|
728
722
|
pi.on("session_start", async (_event: any, ctx: ExtensionContext) => {
|
|
@@ -932,7 +926,7 @@ export function createWorkflowExtension(
|
|
|
932
926
|
const closed = sm.closeCurrent();
|
|
933
927
|
if (closed) {
|
|
934
928
|
// Detach only: the on-disk active record remains resumable. Only
|
|
935
|
-
//
|
|
929
|
+
// end_workflow({ mode }) writes status: completed.
|
|
936
930
|
applyEntry(pi, config, { ...closed, done: false }, controller.run);
|
|
937
931
|
ctx.ui.notify(`Detached ${mode} workflow at ${closed.artifactDir}; it remains resumable.`, "info");
|
|
938
932
|
}
|
|
@@ -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 = {
|
|
@@ -102,16 +102,6 @@ export const taskMode: WorkflowModeRegistration = {
|
|
|
102
102
|
commandName: "workflow-task",
|
|
103
103
|
commandDescription:
|
|
104
104
|
"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
105
|
};
|
|
116
106
|
|
|
117
107
|
export default taskMode;
|
|
@@ -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 {
|
|
@@ -33,7 +33,7 @@ Done when every proposed behavior has one owner: state machine, shared adapter,
|
|
|
33
33
|
For a new mode, add `src/extensions/workflow/modes/<name>.ts`, modeled on `quick.ts`, with only `WorkflowConfig` and `WorkflowModeRegistration`:
|
|
34
34
|
|
|
35
35
|
- ordered phases, phase artifacts, prompts, validators, close artifacts/validators;
|
|
36
|
-
- unique mode/status/entry identities and start/resume/end
|
|
36
|
+
- unique mode/status/entry identities and slash-command name; shared start/resume/end tools select the mode;
|
|
37
37
|
- prompts that name exact artifact paths and use `write_workflow_artifact` only for workflow artifacts.
|
|
38
38
|
|
|
39
39
|
Register the mode once in `MODES` in `extension.ts`; document its lifecycle and commands in `docs/workflows.md`.
|
|
@@ -47,7 +47,7 @@ The shared adapter owns UUID artifact directories, atomic saves, resume, loop re
|
|
|
47
47
|
- Persisted state changes after start, artifact/loop transition, resume reconciliation, and explicit end.
|
|
48
48
|
- Never auto-resume on `session_start`; an explicit selector attaches a run.
|
|
49
49
|
- Artifact completion advances durable state then stops the parent turn; the user deliberately continues the attached mode.
|
|
50
|
-
- Terminal-ready stays active. Only `
|
|
50
|
+
- Terminal-ready stays active. Only `end_workflow({ mode })` marks the record completed and terminates.
|
|
51
51
|
- One `ExtensionAPI` hosts all modes: shared writer once, stale reload handlers inert, one attached run total.
|
|
52
52
|
- Builder/reviewer loops use adapter-owned rounds, review files, markers, and max-iteration pause.
|
|
53
53
|
|
package/docs/workflows.md
CHANGED
|
@@ -20,8 +20,8 @@ src/extensions/workflow/
|
|
|
20
20
|
- **`state-machine.ts`** is the deep module. It owns the phase graph, artifact gating, skip rules, the terminal close gate, and the reentrancy guard. It imports nothing external — no `node:fs`, no pi API, no `pi-tui`, no `typebox`. Every method returns a `WorkflowEffect` (a discriminated union in domain vocabulary) that the adapter pattern-matches on.
|
|
21
21
|
- **`adapter.ts`** is the thin glue. It owns Pi/fs wiring, durable state, explicit resume, loop review persistence, and the git-based `reuse` skip predicate. Parent-written artifacts advance durable phase state. `prototype` and `quick` stop at user-controlled boundaries; `task` queues its build loop as soon as its plan is ready.
|
|
22
22
|
- **`workflow.json`** in each artifact directory is the canonical, versioned run record. It is atomically replaced after state changes; session custom entries are only pointers for UI/history and never reconstruct an active run.
|
|
23
|
-
- **`extension.ts`** imports each mode's registration object and calls `createWorkflowExtension(config, options)(pi)` for each
|
|
24
|
-
- **A mode file** is pure data: the phase list,
|
|
23
|
+
- **`extension.ts`** imports each mode's registration object and calls `createWorkflowExtension(config, options)(pi)` for each. One extension load registers one shared writer plus `start_workflow`, `resume_workflow`, and `end_workflow`; each lifecycle call selects a mode.
|
|
24
|
+
- **A mode file** is pure data: the phase list, per-phase artifact filenames, prompt generators, terminal close artifacts, and command/status/entry identities. Prompts receive `{ artifactDir, userPrompt }`. Each mode exports a `WorkflowModeRegistration` object (e.g. `prototypeMode`, `quickMode`); it does not call `createWorkflowExtension` itself.
|
|
25
25
|
|
|
26
26
|
## To add a future mode
|
|
27
27
|
|
|
@@ -91,16 +91,6 @@ export const rigorousMode: WorkflowModeRegistration = {
|
|
|
91
91
|
commandName: "rigorous",
|
|
92
92
|
commandDescription:
|
|
93
93
|
"Run the rigorous workflow (grill → spec → research → plan → reuse → handoff → loop → audit → sign-off)",
|
|
94
|
-
toolNames: {
|
|
95
|
-
start: "start_rigorous_workflow",
|
|
96
|
-
resume: "resume_rigorous_workflow",
|
|
97
|
-
end: "end_rigorous_workflow",
|
|
98
|
-
},
|
|
99
|
-
toolLabels: {
|
|
100
|
-
start: "Start Rigorous Workflow",
|
|
101
|
-
resume: "Resume Rigorous Workflow",
|
|
102
|
-
end: "End Rigorous Workflow",
|
|
103
|
-
},
|
|
104
94
|
};
|
|
105
95
|
|
|
106
96
|
export default rigorousMode;
|
|
@@ -116,15 +106,15 @@ import { rigorousMode } from "./modes/rigorous.ts";
|
|
|
116
106
|
const MODES = [prototypeMode, quickMode, rigorousMode] as const;
|
|
117
107
|
```
|
|
118
108
|
|
|
119
|
-
That's it. The loader picks it up at boot (`package.json` loads only `./extension.ts`);
|
|
109
|
+
That's it. The loader picks it up at boot (`package.json` loads only `./extension.ts`); the shared lifecycle tools accept `mode: "rigorous"`, and the `/rigorous` command is registered automatically. There is no `next` tool — phases auto-advance as artifacts land and only `end_workflow({ mode: "rigorous" })` completes the terminal phase.
|
|
120
110
|
|
|
121
111
|
## Built-in modes
|
|
122
112
|
|
|
123
113
|
### `task` — plan → codebase exploration → handoff → build/review loop
|
|
124
114
|
|
|
125
|
-
Task now follows the same phase shape as the other modes, minus grilling/research/audit: an architect subagent produces a validated `plan.md`, an optional explorer subagent produces `reuse.md`, a recapper subagent produces a validated `handoff.md`, and then a builder↔commentator review loop runs (max 3 blocking rounds). A clean review makes the workflow terminal-ready; `
|
|
115
|
+
Task now follows the same phase shape as the other modes, minus grilling/research/audit: an architect subagent produces a validated `plan.md`, an optional explorer subagent produces `reuse.md`, a recapper subagent produces a validated `handoff.md`, and then a builder↔commentator review loop runs (max 3 blocking rounds). A clean review makes the workflow terminal-ready; `end_workflow({ mode: "task" })` completes it.
|
|
126
116
|
|
|
127
|
-
Lifecycle: `plan → reuse → handoff → loop (build ↔ review) → terminal-ready →
|
|
117
|
+
Lifecycle: `plan → reuse → handoff → loop (build ↔ review) → terminal-ready → end_workflow({ mode: "task" })`
|
|
128
118
|
|
|
129
119
|
- `/workflow-task <goal>` — start a new run
|
|
130
120
|
- `/workflow-task resume` — list and resume active runs
|
|
@@ -154,8 +144,6 @@ The second argument to `createWorkflowExtension`:
|
|
|
154
144
|
|
|
155
145
|
| Field | Description |
|
|
156
146
|
|---|---|
|
|
157
|
-
| `toolNames` | `{ start, resume, end }` — registered tool names. Artifacts advance phase state; the user explicitly continues the attached run. |
|
|
158
|
-
| `toolLabels` | Human-readable labels for the tools. |
|
|
159
147
|
| `commandName` | The `/<command>` name users type to kick off the workflow. |
|
|
160
148
|
| `commandDescription` | Description shown in the command list. |
|
|
161
149
|
|
|
@@ -165,14 +153,14 @@ Each started workflow receives a UUID artifact directory under `.selesai/artifac
|
|
|
165
153
|
|
|
166
154
|
Runs are **never** auto-resumed on session start. At most one run can be attached to a Pi instance, but older active runs remain resumable:
|
|
167
155
|
|
|
168
|
-
- `resume_workflow({ run: "<id-or-path>" })
|
|
156
|
+
- `start_workflow({ mode, goal })`, `resume_workflow({ mode, run: "<id-or-path>" })`, and `end_workflow({ mode })`, where `mode` is `prototype`, `quick`, or `task`
|
|
169
157
|
- `/workflow-prototype resume <id-or-artifact-dir-or-workflow.json>` / `/workflow-quick resume ...` / `/workflow-task resume ...`
|
|
170
158
|
- `/workflow-prototype resume`, `/workflow-quick resume`, or `/workflow-task resume` lists active runs (and offers a UI picker when available).
|
|
171
159
|
- `/workflow-prototype help`, `/workflow-quick help`, or `/workflow-task help` shows the start, resume, continue, and explicit-completion lifecycle.
|
|
172
160
|
|
|
173
161
|
Resume validates the selected file is under the artifacts base, belongs to that mode, is active, and matches its containing directory. It reconciles the current expected artifact once before emitting the current prompt, covering a crash after `write_workflow_artifact` writes the file but before the phase-state write. Artifact writes do not inject the next phase prompt or launch the next subagent; they terminate the parent turn and wait for the user to continue. A mode can opt out of that pause after a parent artifact write; `task` does so at every parent-owned artifact boundary (`plan.md`, `reuse.md`, `handoff.md`) so the build loop starts immediately after a valid handoff. Corrupt records are skipped during discovery.
|
|
174
162
|
|
|
175
|
-
A valid terminal artifact makes a workflow **terminal-ready**; it does not complete the run. Call
|
|
163
|
+
A valid terminal artifact makes a workflow **terminal-ready**; it does not complete the run. Call `end_workflow({ mode })` to write `status: "completed"`, append the done entry, and terminate. This is the only completion path.
|
|
176
164
|
|
|
177
165
|
## Artifact ownership
|
|
178
166
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selesai/code",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.17",
|
|
4
4
|
"description": "Maintained, extension-first Pi coding agent with built-in workflows, subagents, web research, questions, skills, and an enhanced terminal UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|