@selesai/code 0.5.16 → 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.
@@ -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 = { controllers: WorkflowController[]; writerRegisteredFor: WeakSet<object> };
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
- `Only one run may be attached; ${options.toolNames.end} explicitly completes a terminal-ready run.`,
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
- const { start, resume, end } = options.toolNames;
714
+ registerSharedWorkflowTools(pi);
562
715
  const { mode, footerLabel } = config;
716
+ const end = "end_workflow";
563
717
 
564
- // ── start tool ──
565
- pi.registerTool({
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
- // end_*_workflow writes status: completed.
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 end_quick_workflow to complete the workflow.`,
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 end_task_workflow to complete the workflow.`,
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 tool plus slash-command names;
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 `end_<mode>_workflow` marks the record completed and terminates.
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, so one extension load resolves a single shared writer tool + one start/end tool pair per mode.
24
- - **A mode file** is pure data: the phase list, the per-phase artifact filenames, the per-phase prompt generators, the terminal close artifacts, and identity strings (tool names, command name, status key, entry type). Prompts are functions that receive `{ artifactDir, userPrompt }` and return a string. Each mode exports a `WorkflowModeRegistration` object (e.g. `prototypeMode`, `quickMode`); it does not call `createWorkflowExtension` itself.
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`); start/resume/end tools and the `/rigorous` command are registered automatically. There is no `next` tool — phases auto-advance as artifacts land and only the `end` tool completes the terminal phase.
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; `end_task_workflow` completes it.
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 → end_task_workflow`
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>" })` / `resume_quick_workflow(...)` / `resume_task_workflow(...)`
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 the mode-specific `end_*_workflow` tool to write `status: "completed"`, append the done entry, and terminate. This is the only completion path.
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.16",
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": {