@selesai/code 0.3.10 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/defaults/models.json +2 -3
  2. package/dist/extensions/caveman/index.js +16 -1
  3. package/dist/extensions/caveman/test/extension.test.js +7 -4
  4. package/dist/extensions/caveman/test/helpers.test.js +12 -1
  5. package/dist/extensions/context-compaction-reminder.test.ts +82 -0
  6. package/dist/extensions/context-compaction-reminder.ts +28 -0
  7. package/dist/extensions/handoff-new.test.ts +2 -13
  8. package/dist/extensions/handoff-new.ts +16 -25
  9. package/dist/extensions/package.json +1 -0
  10. package/dist/extensions/pi-subagents/agents/architect.md +25 -173
  11. package/dist/extensions/pi-subagents/agents/builder.md +16 -105
  12. package/dist/extensions/pi-subagents/agents/commentator.md +19 -116
  13. package/dist/extensions/pi-subagents/agents/explorer.md +13 -33
  14. package/dist/extensions/pi-subagents/agents/recapper.md +19 -11
  15. package/dist/extensions/pi-subagents/agents/researcher.md +10 -33
  16. package/dist/extensions/question/constants.ts +0 -7
  17. package/dist/extensions/question/index.ts +11 -108
  18. package/dist/extensions/question/schemas.ts +1 -26
  19. package/dist/extensions/question/shortcuts.ts +5 -14
  20. package/dist/extensions/question/tests/question-list.test.ts +8 -0
  21. package/dist/extensions/question/tests/ui-protocol.test.ts +1 -7
  22. package/dist/extensions/question/tui-adapter.ts +3 -20
  23. package/dist/extensions/question/types.ts +0 -6
  24. package/dist/extensions/question/ui-protocol.ts +2 -17
  25. package/dist/extensions/workflow/adapter.ts +384 -124
  26. package/dist/extensions/workflow/extension.ts +2 -1
  27. package/dist/extensions/workflow/modes/prototype.ts +6 -6
  28. package/dist/extensions/workflow/modes/quick.ts +6 -6
  29. package/dist/extensions/workflow/modes/task.ts +79 -0
  30. package/dist/extensions/workflow/run-state.ts +124 -0
  31. package/dist/extensions/workflow/state-machine.ts +4 -8
  32. package/dist/skills/workflow-creation/SKILL.md +72 -0
  33. package/docs/workflows.md +40 -6
  34. package/package.json +1 -1
@@ -7,10 +7,11 @@ import type { ExtensionAPI } from "@selesai/code";
7
7
  import { __resetWorkflowRegistryForTests, createWorkflowExtension } from "./adapter.ts";
8
8
  import { prototypeMode } from "./modes/prototype.ts";
9
9
  import { quickMode } from "./modes/quick.ts";
10
+ import { taskMode } from "./modes/task.ts";
10
11
 
11
12
  export { __resetWorkflowRegistryForTests };
12
13
 
13
- const MODES = [prototypeMode, quickMode] as const;
14
+ const MODES = [prototypeMode, quickMode, taskMode] as const;
14
15
 
15
16
  export default function workflowModesExtension(pi: ExtensionAPI): void {
16
17
  for (const mode of MODES) {
@@ -120,12 +120,12 @@ Use read to inspect ${artifactDir}/plan.md, ${artifactDir}/handoff.md, ${artifac
120
120
 
121
121
  Call the subagent tool with { agent: "builder", task: "..." } (do NOT pass a model parameter). Give it plan + handoff + any research/reuse context, tailored to this task. Instruct it to implement every task in plan.md in order. All code changes go in the workspace, never in ${artifactDir}.
122
122
 
123
- After the builder returns, the workflow engine will automatically prompt you to call the commentator. Craft the review task YOURSELF based on what matters for this task. Each commentator review MUST end with exactly one machine-readable line:
123
+ After the builder returns, call the commentator. Craft the review task YOURSELF based on what matters for this task. Each commentator review MUST end with exactly one machine-readable line:
124
124
  WORKFLOW_REVIEW_STATUS: clean
125
125
  OR
126
126
  WORKFLOW_REVIEW_STATUS: blocking
127
127
 
128
- If a review is blocking, the engine prompts you to call the builder again with the issues. This repeats up to ${loopMaxIterations ?? 3} round(s). When a review is clean, the engine writes loop-complete.md and advances to audit. Do NOT write loop-complete.md yourself.`,
128
+ 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 advances to audit. Do NOT write loop-complete.md yourself.`,
129
129
  audit: ({ artifactDir }) =>
130
130
  `You are in the AUDIT (review) phase.
131
131
 
@@ -137,7 +137,7 @@ Review uncommitted changes for correctness, plan adherence, and over-engineering
137
137
  2. If ${artifactDir}/review.md lists actionable issues, call the subagent tool with { agent: "builder", task: "..." } (do NOT pass a model parameter). Instruct it to fix every issue in the workspace, never in ${artifactDir}.
138
138
  3. Re-dispatch the commentator until ${artifactDir}/review.md ends with WORKFLOW_REVIEW_STATUS: clean.
139
139
 
140
- The workflow closes only once ${artifactDir}/review.md exists AND ends with the WORKFLOW_REVIEW_STATUS: clean marker.`,
140
+ Once ${artifactDir}/review.md exists and ends with the WORKFLOW_REVIEW_STATUS: clean marker, call end_workflow to complete the workflow.`,
141
141
  };
142
142
 
143
143
  const config: WorkflowConfig = {
@@ -171,11 +171,11 @@ const config: WorkflowConfig = {
171
171
 
172
172
  export const prototypeMode: WorkflowModeRegistration = {
173
173
  config,
174
- commandName: "prototype",
174
+ commandName: "workflow-prototype",
175
175
  commandDescription:
176
176
  "Run the prototype workflow (grill → research → plan → reuse → handoff → loop → audit)",
177
- toolNames: { start: "start_workflow", end: "end_workflow" },
178
- toolLabels: { start: "Start Workflow", end: "End Workflow" },
177
+ toolNames: { start: "start_workflow", resume: "resume_workflow", end: "end_workflow" },
178
+ toolLabels: { start: "Start Workflow", resume: "Resume Workflow", end: "End Workflow" },
179
179
  };
180
180
 
181
181
  export default prototypeMode;
@@ -103,12 +103,12 @@ Use read to inspect ${artifactDir}/plan.md, ${artifactDir}/handoff.md, and ${art
103
103
 
104
104
  Call the subagent tool with { agent: "builder", task: "..." } (do NOT pass a model parameter). Give it plan + handoff + reuse context, tailored to this task. Instruct it to implement every task in plan.md in order. All code changes go in the workspace, never in ${artifactDir}.
105
105
 
106
- After the builder returns, the workflow engine will automatically prompt you to call the commentator. Craft the review task YOURSELF. Each commentator review MUST end with exactly one machine-readable line:
106
+ After the builder returns, call the commentator. Craft the review task YOURSELF. Each commentator review MUST end with exactly one machine-readable line:
107
107
  WORKFLOW_REVIEW_STATUS: clean
108
108
  OR
109
109
  WORKFLOW_REVIEW_STATUS: blocking
110
110
 
111
- If a review is blocking, the engine prompts you to call the builder again with the issues. This repeats up to ${loopMaxIterations ?? 3} round(s). When a review is clean, the engine writes loop-complete.md and advances to audit. Do NOT write loop-complete.md yourself.`,
111
+ 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 advances to audit. Do NOT write loop-complete.md yourself.`,
112
112
  audit: ({ artifactDir }) =>
113
113
  `You are in the AUDIT (review) phase.
114
114
 
@@ -120,7 +120,7 @@ Review uncommitted changes for correctness, plan adherence, and over-engineering
120
120
  2. If ${artifactDir}/review.md lists actionable issues, call the subagent tool with { agent: "builder", task: "..." } (do NOT pass a model parameter). Instruct it to fix every issue in the workspace, never in ${artifactDir}.
121
121
  3. Re-run the commentator until ${artifactDir}/review.md ends with WORKFLOW_REVIEW_STATUS: clean.
122
122
 
123
- The workflow closes only once ${artifactDir}/review.md exists AND ends with the WORKFLOW_REVIEW_STATUS: clean marker.`,
123
+ Once ${artifactDir}/review.md exists and ends with the WORKFLOW_REVIEW_STATUS: clean marker, call end_quick_workflow to complete the workflow.`,
124
124
  };
125
125
 
126
126
  const config: WorkflowConfig = {
@@ -151,11 +151,11 @@ const config: WorkflowConfig = {
151
151
 
152
152
  export const quickMode: WorkflowModeRegistration = {
153
153
  config,
154
- commandName: "quick",
154
+ commandName: "workflow-quick",
155
155
  commandDescription:
156
156
  "Run the quick workflow (grill → plan → reuse → handoff → loop → audit)",
157
- toolNames: { start: "start_quick_workflow", end: "end_quick_workflow" },
158
- toolLabels: { start: "Start Quick Workflow", end: "End Quick Workflow" },
157
+ toolNames: { start: "start_quick_workflow", resume: "resume_quick_workflow", end: "end_quick_workflow" },
158
+ toolLabels: { start: "Start Quick Workflow", resume: "Resume Quick Workflow", end: "End Quick Workflow" },
159
159
  };
160
160
 
161
161
  export default quickMode;
@@ -0,0 +1,79 @@
1
+ import type {
2
+ Phase,
3
+ PromptContext,
4
+ WorkflowConfig,
5
+ WorkflowModeRegistration,
6
+ } from "../state-machine.ts";
7
+ import { loopCompleteValidator, planValidator } from "../validators.ts";
8
+
9
+ // ponytail: task workflow is only plan → build↔review; the loop's clean marker
10
+ // makes it terminal-ready, and the explicit end tool is the only completion.
11
+ const phases: Phase[] = ["plan", "loop"];
12
+
13
+ const prompts: Partial<Record<Phase, (ctx: PromptContext) => string>> = {
14
+ plan: ({ artifactDir, userPrompt }) =>
15
+ `You are in the PLAN phase of a TASK workflow.
16
+
17
+ User request:
18
+ ${userPrompt}
19
+
20
+ Produce a concrete implementation plan: what to build, how, in what order, which files, components, and what the finished result looks like.
21
+
22
+ Call the subagent tool with { agent: "architect", task: "..." } (do NOT pass a model parameter). Craft the task so the architect plans for THIS request. The architect returns the plan; the workflow saves it to ${artifactDir}/plan.md.
23
+
24
+ The plan MUST end with exactly one machine-readable line on its own:
25
+ WORKFLOW_PLAN_STATUS: ready
26
+ The workflow will NOT advance until ${artifactDir}/plan.md contains that marker.`,
27
+ loop: ({ artifactDir, loopMaxIterations }) =>
28
+ `You are in the LOOP (orchestration) phase of a TASK workflow. The workflow ENGINE owns the implement→review loop — you do NOT track iterations or decide when the loop is clean.
29
+
30
+ Use read to inspect ${artifactDir}/plan.md. Using that context, GENERATE YOUR OWN delegation prompt:
31
+
32
+ Call the subagent tool with { agent: "builder", task: "..." } (do NOT pass a model parameter). Give it the plan context, tailored to this task. Instruct it to implement every task in plan.md in order. All code changes go in the workspace, never in ${artifactDir}.
33
+
34
+ After the builder returns, call the commentator. Craft the review task YOURSELF. Each commentator review MUST end with exactly one machine-readable line:
35
+ WORKFLOW_REVIEW_STATUS: clean
36
+ OR
37
+ WORKFLOW_REVIEW_STATUS: blocking
38
+
39
+ 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.`,
40
+ };
41
+
42
+ const config: WorkflowConfig = {
43
+ mode: "task",
44
+ phases,
45
+ phaseArtifacts: {
46
+ plan: "plan.md",
47
+ loop: "loop-complete.md",
48
+ },
49
+ prompts,
50
+ skipRules: [],
51
+ artifactValidators: {
52
+ plan: planValidator,
53
+ loop: loopCompleteValidator,
54
+ },
55
+ closeValidators: { "loop-complete.md": loopCompleteValidator },
56
+ closeArtifacts: ["loop-complete.md"],
57
+ loopMaxIterations: 3,
58
+ statusKey: "task",
59
+ entryType: "task-phase",
60
+ footerLabel: "task",
61
+ };
62
+
63
+ export const taskMode: WorkflowModeRegistration = {
64
+ config,
65
+ commandName: "workflow-task",
66
+ commandDescription: "Run the task workflow (plan → build↔review loop)",
67
+ toolNames: {
68
+ start: "start_task_workflow",
69
+ resume: "resume_task_workflow",
70
+ end: "end_task_workflow",
71
+ },
72
+ toolLabels: {
73
+ start: "Start Task Workflow",
74
+ resume: "Resume Task Workflow",
75
+ end: "End Task Workflow",
76
+ },
77
+ };
78
+
79
+ export default taskMode;
@@ -0,0 +1,124 @@
1
+ import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
2
+ import { basename, dirname, relative, resolve } from "node:path";
3
+ import { withFileMutationQueue } from "@selesai/code";
4
+
5
+ import type { Phase } from "./state-machine.ts";
6
+
7
+ export const WORKFLOW_STATE_FILENAME = "workflow.json";
8
+
9
+ export interface LoopState {
10
+ reviewRound: number;
11
+ maxIterations: number;
12
+ stage: "building" | "reviewing" | "clean" | "maxed";
13
+ reviewPath?: string;
14
+ }
15
+
16
+ export interface PersistedWorkflowRun {
17
+ version: 1;
18
+ id: string;
19
+ mode: string;
20
+ status: "active" | "completed";
21
+ goal: string;
22
+ artifactDir: string;
23
+ phase: Phase;
24
+ autoArmed: boolean;
25
+ loopState?: LoopState;
26
+ createdAt: string;
27
+ updatedAt: string;
28
+ }
29
+
30
+ export type WorkflowModes = Record<string, readonly Phase[]>;
31
+ export interface ListedWorkflowRun { statePath: string; run: PersistedWorkflowRun }
32
+
33
+ function isInside(base: string, path: string): boolean {
34
+ const rel = relative(resolve(base), resolve(path));
35
+ return rel === "" || (!rel.startsWith("..") && !rel.includes(`..${process.platform === "win32" ? "\\" : "/"}`));
36
+ }
37
+
38
+ function invalid(message: string): never {
39
+ throw new Error(`Invalid ${WORKFLOW_STATE_FILENAME}: ${message}`);
40
+ }
41
+
42
+ function validateLoopState(value: unknown): LoopState | undefined {
43
+ if (value === undefined) return undefined;
44
+ if (!value || typeof value !== "object") invalid("loopState must be an object");
45
+ const loop = value as Record<string, unknown>;
46
+ if (!Number.isInteger(loop.reviewRound) || (loop.reviewRound as number) < 0) invalid("loopState.reviewRound must be a non-negative integer");
47
+ if (!Number.isInteger(loop.maxIterations) || (loop.maxIterations as number) < 1) invalid("loopState.maxIterations must be a positive integer");
48
+ if (!(["building", "reviewing", "clean", "maxed"] as string[]).includes(loop.stage as string)) invalid("loopState.stage is invalid");
49
+ if (loop.reviewPath !== undefined && (typeof loop.reviewPath !== "string" || !/^loop-review-\d+\.md$/.test(loop.reviewPath))) invalid("loopState.reviewPath is invalid");
50
+ return loop as unknown as LoopState;
51
+ }
52
+
53
+ export function validateWorkflowRun(value: unknown, statePath: string, modes: WorkflowModes): PersistedWorkflowRun {
54
+ if (!value || typeof value !== "object") invalid("record must be an object");
55
+ const run = value as Record<string, unknown>;
56
+ if (run.version !== 1) invalid("unsupported version");
57
+ if (typeof run.id !== "string" || !run.id || run.id !== basename(dirname(resolve(statePath)))) invalid("id must match its containing directory");
58
+ if (typeof run.mode !== "string" || !modes[run.mode]) invalid("unknown mode");
59
+ if (run.status !== "active" && run.status !== "completed") invalid("status is invalid");
60
+ if (typeof run.goal !== "string") invalid("goal must be a string");
61
+ if (typeof run.artifactDir !== "string" || resolve(run.artifactDir) !== dirname(resolve(statePath))) invalid("artifactDir does not match its containing directory");
62
+ if (typeof run.phase !== "string" || !modes[run.mode].includes(run.phase)) invalid("phase is not valid for the selected mode");
63
+ if (typeof run.autoArmed !== "boolean") invalid("autoArmed must be boolean");
64
+ if (typeof run.createdAt !== "string" || typeof run.updatedAt !== "string") invalid("timestamps must be strings");
65
+ validateLoopState(run.loopState);
66
+ return run as unknown as PersistedWorkflowRun;
67
+ }
68
+
69
+ export async function loadWorkflowRun(statePath: string, modes: WorkflowModes): Promise<PersistedWorkflowRun> {
70
+ let parsed: unknown;
71
+ try {
72
+ parsed = JSON.parse(await readFile(statePath, "utf8"));
73
+ } catch (error) {
74
+ throw new Error(`Cannot read ${statePath}: ${error instanceof Error ? error.message : String(error)}`);
75
+ }
76
+ return validateWorkflowRun(parsed, statePath, modes);
77
+ }
78
+
79
+ export async function saveWorkflowRun(run: PersistedWorkflowRun): Promise<void> {
80
+ const statePath = resolve(run.artifactDir, WORKFLOW_STATE_FILENAME);
81
+ await withFileMutationQueue(statePath, async () => {
82
+ await mkdir(dirname(statePath), { recursive: true });
83
+ const tempPath = `${statePath}.${process.pid}.${Date.now()}.tmp`;
84
+ await writeFile(tempPath, `${JSON.stringify(run, null, 2)}\n`, "utf8");
85
+ await rename(tempPath, statePath);
86
+ });
87
+ }
88
+
89
+ export async function listResumableWorkflowRuns(base: string, modes: WorkflowModes): Promise<ListedWorkflowRun[]> {
90
+ let entries;
91
+ try {
92
+ entries = await readdir(resolve(base), { withFileTypes: true });
93
+ } catch (error: any) {
94
+ if (error?.code === "ENOENT") return [];
95
+ throw error;
96
+ }
97
+ const listed: ListedWorkflowRun[] = [];
98
+ for (const entry of entries) {
99
+ if (!entry.isDirectory()) continue;
100
+ const statePath = resolve(base, entry.name, WORKFLOW_STATE_FILENAME);
101
+ try {
102
+ const run = await loadWorkflowRun(statePath, modes);
103
+ if (run.status === "active") listed.push({ statePath, run });
104
+ } catch {
105
+ // A corrupt or unrelated artifact is not a resumable run.
106
+ }
107
+ }
108
+ return listed.sort((a, b) => b.run.updatedAt.localeCompare(a.run.updatedAt));
109
+ }
110
+
111
+ export async function resolveWorkflowRun(selector: string, base: string, modes: WorkflowModes): Promise<ListedWorkflowRun> {
112
+ const root = resolve(base);
113
+ const candidate = resolve(root, selector);
114
+ if (selector.includes("/") || selector.includes("\\") || selector.endsWith(".json") || selector.startsWith(".")) {
115
+ if (!isInside(root, candidate)) throw new Error("Workflow run path must be inside the artifacts directory.");
116
+ const statePath = candidate.endsWith(WORKFLOW_STATE_FILENAME) ? candidate : resolve(candidate, WORKFLOW_STATE_FILENAME);
117
+ const run = await loadWorkflowRun(statePath, modes);
118
+ return { statePath, run };
119
+ }
120
+ const runs = await listResumableWorkflowRuns(root, modes);
121
+ const found = runs.find(({ run }) => run.id === selector);
122
+ if (!found) throw new Error(`No resumable workflow run with id ${selector}.`);
123
+ return found;
124
+ }
@@ -47,8 +47,8 @@ export interface WorkflowModeRegistration {
47
47
  config: WorkflowConfig;
48
48
  commandName: string;
49
49
  commandDescription: string;
50
- toolNames: { start: string; end: string };
51
- toolLabels: { start: string; end: string };
50
+ toolNames: { start: string; resume: string; end: string };
51
+ toolLabels: { start: string; resume: string; end: string };
52
52
  }
53
53
 
54
54
  export interface WorkflowConfig {
@@ -328,7 +328,7 @@ export class WorkflowStateMachine {
328
328
  kind: "terminalNeedsArtifacts",
329
329
  phase: this.phase,
330
330
  missing: file,
331
- promptToQueue: `Terminal phase still needs ${this.artifactDir}/${file}. Write it, then the workflow closes automatically.`,
331
+ promptToQueue: `Terminal phase still needs ${this.artifactDir}/${file}. Write it, then call the explicit end workflow tool.`,
332
332
  };
333
333
  }
334
334
  if (sat.reason) {
@@ -337,7 +337,7 @@ export class WorkflowStateMachine {
337
337
  phase: this.phase,
338
338
  missing: file,
339
339
  reason: sat.reason,
340
- promptToQueue: `Terminal artifact ${this.artifactDir}/${file} is incomplete: ${sat.reason}. Fix it, then the workflow closes automatically.`,
340
+ promptToQueue: `Terminal artifact ${this.artifactDir}/${file} is incomplete: ${sat.reason}. Fix it, then call the explicit end workflow tool.`,
341
341
  };
342
342
  }
343
343
  }
@@ -449,10 +449,6 @@ export class WorkflowStateMachine {
449
449
  }
450
450
  this.autoArmed = false;
451
451
  const out = await this.advancePhase(deps, true);
452
- if (out.kind === "terminalReady") {
453
- // ponytail: both close artifacts present — close directly.
454
- return this.end(deps);
455
- }
456
452
  if (out.kind === "terminalNeedsArtifacts") {
457
453
  // Keep listening: audit/review may land in separate writes.
458
454
  this.autoArmed = true;
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: workflow-creation
3
+ description: Creates durable Selesai workflow modes. Use when a user asks to create or change a workflow mode, phased agent flow, or slash-command workflow.
4
+ ---
5
+
6
+ # Durable Workflows
7
+
8
+ **Durable** means `workflow.json`, not session history, is the run authority. Build on the shared engine in `src/extensions/workflow/`; a mode is configuration, not a second orchestrator.
9
+
10
+ ## 1. Choose the smallest fit
11
+
12
+ Read `docs/workflows.md`, every file in `src/extensions/workflow/modes/`, and the relevant workflow tests.
13
+
14
+ - Reuse `prototype`, `quick`, or `task` when its phase graph and terminal gate fit; change only its prompts/configuration.
15
+ - Add a mode only for a materially different graph, artifact contract, or close gate.
16
+
17
+ Done when the request is mapped to one existing mode or a named new mode with its phase list and terminal artifact.
18
+
19
+ ## 2. Trace the durable seam
20
+
21
+ Before changing engine-facing behavior, read completely:
22
+
23
+ - `state-machine.ts` — graph, artifact gates, terminal-ready, completion;
24
+ - `adapter.ts` — tools, event handlers, persistence, reload guards;
25
+ - `run-state.ts` — canonical record and resume validation;
26
+ - `extension.ts` — single extension mounting all modes.
27
+
28
+ Done when every proposed behavior has one owner: state machine, shared adapter, or mode configuration.
29
+
30
+ ## 3. Implement the mode
31
+
32
+ For a new mode, add `src/extensions/workflow/modes/<name>.ts`, modeled on `quick.ts`, with only `WorkflowConfig` and `WorkflowModeRegistration`:
33
+
34
+ - ordered phases, phase artifacts, prompts, validators, close artifacts/validators;
35
+ - unique mode/status/entry identities and start/resume/end tool plus slash-command names;
36
+ - prompts that name exact artifact paths and use `write_workflow_artifact` only for workflow artifacts.
37
+
38
+ Register the mode once in `MODES` in `extension.ts`; document its lifecycle and commands in `docs/workflows.md`.
39
+
40
+ Done when the mode file has no filesystem, persistence, event-registration, or controller code.
41
+
42
+ ## 4. Preserve the durable contract
43
+
44
+ The shared adapter owns UUID artifact directories, atomic saves, resume, loop review state, and reload safety. Do not reimplement them per mode.
45
+
46
+ - Persisted state changes after start, artifact/loop transition, resume reconciliation, and explicit end.
47
+ - Never auto-resume on `session_start`; an explicit selector attaches a run.
48
+ - Artifact completion advances durable state then stops the parent turn; the user deliberately continues the attached mode.
49
+ - Terminal-ready stays active. Only `end_<mode>_workflow` marks the record completed and terminates.
50
+ - One `ExtensionAPI` hosts all modes: shared writer once, stale reload handlers inert, one attached run total.
51
+ - Builder/reviewer loops use adapter-owned rounds, review files, markers, and max-iteration pause.
52
+
53
+ Done when the new behavior preserves every applicable invariant above.
54
+
55
+ ## 5. Lock the mode with real seams
56
+
57
+ Extend the existing fake-Pi tests; do not add another framework. Cover the real mode, not only state-machine units:
58
+
59
+ - start writes valid `workflow.json`; artifact transition updates it; explicit end completes it;
60
+ - explicit resume reconciles an artifact written before a phase save;
61
+ - loop round/review path resumes when the mode has a loop;
62
+ - terminal-ready does not complete early;
63
+ - extension reload ignores stale handlers; inactive sibling modes do not react.
64
+
65
+ Run the narrow mode test, then:
66
+
67
+ ```bash
68
+ npx vitest run src/__tests__/state-machine.test.ts src/__tests__/adapter.test.ts src/__tests__/workflow-race.test.ts src/__tests__/workflow-run-state.test.ts src/__tests__/<mode>-workflow.test.ts
69
+ npm run build
70
+ ```
71
+
72
+ Done when those checks pass and the diff contains only the selected mode, shared-engine changes proven necessary by a failing regression, documentation, and tests.
package/docs/workflows.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Workflows
2
2
 
3
- Selesai ships a workflow engine under `src/extensions/workflow/`. It powers the built-in `prototype` and `quick` workflows and is designed so you can add a new workflow mode as a thin config file — no engine changes.
3
+ Selesai ships a workflow engine under `src/extensions/workflow/`. It powers the built-in `prototype`, `quick`, and `task` workflows and is designed so you can add a new workflow mode as a thin config file — no engine changes.
4
4
 
5
5
  ## How it fits together
6
6
 
@@ -8,15 +8,18 @@ Selesai ships a workflow engine under `src/extensions/workflow/`. It powers the
8
8
  src/extensions/workflow/
9
9
  package.json pi package manifest; loads ./extension.ts as the single entry
10
10
  state-machine.ts pure phase state machine (no fs, no pi API)
11
- adapter.ts pi wiring: tools, commands, events, fs, agent continuation
11
+ adapter.ts pi wiring: tools, commands, events, fs, durable-state lifecycle
12
+ run-state.ts versioned atomic workflow.json load/save/discovery
12
13
  extension.ts single pi extension that mounts every workflow mode
13
14
  modes/
14
15
  prototype.ts mode config + registration object (exported as `prototypeMode`)
15
16
  quick.ts mode config + registration object (exported as `quickMode`)
17
+ task.ts mode config + registration object (exported as `taskMode`)
16
18
  ```
17
19
 
18
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.
19
- - **`adapter.ts`** is the thin glue. One state-machine method per call site (`start` tool, `end` tool, `tool_result` auto-advance, `/<command>`) plus one `applyEffect` switch. It owns all pi/fs wiring and the git-based `reuse` skip predicate. There is no `next` tool — phase advance is automatic once the phase artifact lands, and terminal close is via the `end` tool.
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. An artifact advances durable phase state, then stops at a user-controlled boundary; `/workflow-prototype`, `/workflow-quick`, or `/workflow-task` continues the attached run.
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.
20
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.
21
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.
22
25
 
@@ -90,10 +93,12 @@ export const rigorousMode: WorkflowModeRegistration = {
90
93
  "Run the rigorous workflow (grill → spec → research → plan → reuse → handoff → loop → audit → sign-off)",
91
94
  toolNames: {
92
95
  start: "start_rigorous_workflow",
96
+ resume: "resume_rigorous_workflow",
93
97
  end: "end_rigorous_workflow",
94
98
  },
95
99
  toolLabels: {
96
100
  start: "Start Rigorous Workflow",
101
+ resume: "Resume Rigorous Workflow",
97
102
  end: "End Rigorous Workflow",
98
103
  },
99
104
  };
@@ -111,7 +116,21 @@ import { rigorousMode } from "./modes/rigorous.ts";
111
116
  const MODES = [prototypeMode, quickMode, rigorousMode] as const;
112
117
  ```
113
118
 
114
- That's it. The loader picks it up at boot (`package.json` loads only `./extension.ts`); the two tools (`start_rigorous_workflow`, `end_rigorous_workflow`) and the `/rigorous` command are registered automatically. There is no `next` tool — phases auto-advance as artifacts land and the `end` tool closes the terminal phase.
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.
120
+
121
+ ## Built-in modes
122
+
123
+ ### `task` — rapid plan + build/review loop
124
+
125
+ The simplest workflow: an architect subagent produces a validated `plan.md`, then a builder↔commentator review loop runs (max 3 blocking rounds). A clean review makes the workflow terminal-ready; `end_task_workflow` completes it.
126
+
127
+ Lifecycle: `plan → loop (build ↔ review) → terminal-ready → end_task_workflow`
128
+
129
+ - `/workflow-task <goal>` — start a new run
130
+ - `/workflow-task resume` — list and resume active runs
131
+ - `/workflow-task help` — show the lifecycle
132
+ - Phases auto-advance as artifacts land
133
+ - No grilling, research, reuse, or handoff phases
115
134
 
116
135
  ## Config reference
117
136
 
@@ -124,7 +143,7 @@ That's it. The loader picks it up at boot (`package.json` loads only `./extensio
124
143
  | `closeArtifacts` | `string[]` | Files that must exist before `end()` succeeds. Config-owned, no built-in default. |
125
144
  | `skipRules?` | `{ phase, shouldSkip }[]` | Optional per-phase skip rules. `shouldSkip` is a boolean predicate; when true the engine skips to the next phase. Omit to use the adapter's default (skip `reuse` on empty projects). |
126
145
  | `statusKey` | `string` | Footer status key. |
127
- | `entryType` | `string` | Persisted entry custom-type, used for session resume. |
146
+ | `entryType` | `string` | Session-history custom-type. It stores a pointer only; `workflow.json` is canonical. |
128
147
  | `footerLabel` | `string` | Label shown in the footer (`● label · step/total phase`). |
129
148
 
130
149
  ### Adapter options
@@ -133,11 +152,26 @@ The second argument to `createWorkflowExtension`:
133
152
 
134
153
  | Field | Description |
135
154
  |---|---|
136
- | `toolNames` | `{ start, end }` — the two registered tool names. There is no `next` tool; phases auto-advance as artifacts land. |
155
+ | `toolNames` | `{ start, resume, end }` — registered tool names. Artifacts advance phase state; the user explicitly continues the attached run. |
137
156
  | `toolLabels` | Human-readable labels for the tools. |
138
157
  | `commandName` | The `/<command>` name users type to kick off the workflow. |
139
158
  | `commandDescription` | Description shown in the command list. |
140
159
 
160
+ ## Durable runs and explicit resume
161
+
162
+ Each started workflow receives a UUID artifact directory under `.selesai/artifacts/` and an adjacent `workflow.json`. It stores the mode, phase, armed state, loop round/review path, and timestamps. Writes use a temporary sibling file plus rename, so a crash cannot partially overwrite the canonical record.
163
+
164
+ 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:
165
+
166
+ - `resume_workflow({ run: "<id-or-path>" })` / `resume_quick_workflow(...)` / `resume_task_workflow(...)`
167
+ - `/workflow-prototype resume <id-or-artifact-dir-or-workflow.json>` / `/workflow-quick resume ...` / `/workflow-task resume ...`
168
+ - `/workflow-prototype resume`, `/workflow-quick resume`, or `/workflow-task resume` lists active runs (and offers a UI picker when available).
169
+ - `/workflow-prototype help`, `/workflow-quick help`, or `/workflow-task help` shows the start, resume, continue, and explicit-completion lifecycle.
170
+
171
+ 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 an artifact write 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. Corrupt records are skipped during discovery.
172
+
173
+ 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.
174
+
141
175
  ## How transitions work
142
176
 
143
177
  Every state-machine method returns a `WorkflowEffect` — a discriminated union the adapter switches on:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selesai/code",
3
- "version": "0.3.10",
3
+ "version": "0.4.0",
4
4
  "description": "Selesai coding agent",
5
5
  "type": "module",
6
6
  "piConfig": {