@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.
- package/dist/defaults/models.json +2 -3
- package/dist/extensions/caveman/index.js +16 -1
- package/dist/extensions/caveman/test/extension.test.js +7 -4
- package/dist/extensions/caveman/test/helpers.test.js +12 -1
- package/dist/extensions/context-compaction-reminder.test.ts +82 -0
- package/dist/extensions/context-compaction-reminder.ts +28 -0
- package/dist/extensions/handoff-new.test.ts +2 -13
- package/dist/extensions/handoff-new.ts +16 -25
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-subagents/agents/architect.md +25 -173
- package/dist/extensions/pi-subagents/agents/builder.md +16 -105
- package/dist/extensions/pi-subagents/agents/commentator.md +19 -116
- package/dist/extensions/pi-subagents/agents/explorer.md +13 -33
- package/dist/extensions/pi-subagents/agents/recapper.md +19 -11
- package/dist/extensions/pi-subagents/agents/researcher.md +10 -33
- package/dist/extensions/question/constants.ts +0 -7
- package/dist/extensions/question/index.ts +11 -108
- package/dist/extensions/question/schemas.ts +1 -26
- package/dist/extensions/question/shortcuts.ts +5 -14
- package/dist/extensions/question/tests/question-list.test.ts +8 -0
- package/dist/extensions/question/tests/ui-protocol.test.ts +1 -7
- package/dist/extensions/question/tui-adapter.ts +3 -20
- package/dist/extensions/question/types.ts +0 -6
- package/dist/extensions/question/ui-protocol.ts +2 -17
- package/dist/extensions/workflow/adapter.ts +384 -124
- package/dist/extensions/workflow/extension.ts +2 -1
- package/dist/extensions/workflow/modes/prototype.ts +6 -6
- package/dist/extensions/workflow/modes/quick.ts +6 -6
- package/dist/extensions/workflow/modes/task.ts +79 -0
- package/dist/extensions/workflow/run-state.ts +124 -0
- package/dist/extensions/workflow/state-machine.ts +4 -8
- package/dist/skills/workflow-creation/SKILL.md +72 -0
- package/docs/workflows.md +40 -6
- 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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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 `
|
|
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,
|
|
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.
|
|
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`);
|
|
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` |
|
|
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 }` —
|
|
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:
|