@selesai/code 0.9.3 → 0.9.5
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/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/extensions/question/index.ts +81 -6
- package/dist/extensions/question/tests/wizard.test.ts +19 -3
- package/dist/extensions/workflow/extension.ts +19 -19
- package/dist/extensions/workflow/modes.ts +34 -14
- package/dist/modes/rpc/rpc-mode.js +1 -0
- package/dist/modes/rpc/rpc-types.d.ts +11 -0
- package/docs/extensions.md +4 -1
- package/docs/rpc.md +26 -3
- package/docs/workflows.md +37 -230
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.9.5] - 2026-08-23
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Questions in non-TUI modes.** The bundled `question` tool now works outside the terminal TUI (e.g. RPC/VS Code hosts): questions are answered through the host's `extension_ui_request` dialogs (`select`/`input`) instead of throwing, mirroring Cline/Kilo. Multi-select questions preserve full multi-selection when the host supports the new `multiselect` UI method, with single-select fallback for older hosts.
|
|
9
|
+
- **`multiselect` extension UI method.** `ExtensionUIContext.multiselect()` prompts for multiple choices and returns `string[] | undefined`. In RPC mode it emits an `extension_ui_request` with `method: "multiselect"` and accepts an `extension_ui_response` with a `values` array.
|
|
10
|
+
|
|
11
|
+
## [0.9.4] - 2026-08-22
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **Extensible pi-subagents workflows.** The bundled `/workflow-*` extension is now a thin registry-based adapter over pi-subagents orchestration. Modes can use ordered runs, parallel discovery, or scripted conditional loops without adding command plumbing. The prototype workflow now runs external research and codebase exploration in parallel.
|
|
15
|
+
- **Workflow completion contract.** Build/review/fix loops now finish only when the reviewer reports `clean` with no remaining work, preventing a clean review of one slice from ending an incomplete plan.
|
|
16
|
+
- **Workflow documentation.** Replaced stale durable-state-machine documentation with the current pi-subagents launch, recovery, and extension model.
|
|
17
|
+
|
|
5
18
|
## [0.9.3] - 2026-08-22
|
|
6
19
|
|
|
7
20
|
### Fixed
|
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ DuckDuckGo works as the default search path. Optional first-run onboarding can c
|
|
|
81
81
|
|
|
82
82
|
### Interactive questions
|
|
83
83
|
|
|
84
|
-
The bundled `question` tool gives the agent a real
|
|
84
|
+
The bundled `question` tool gives the agent a real UI for decisions instead of forcing every clarification through plain text. It supports:
|
|
85
85
|
|
|
86
86
|
- single- and multi-select options
|
|
87
87
|
- descriptions and context summaries
|
|
@@ -68,6 +68,8 @@ export type EditorFactory = (tui: TUI, theme: EditorTheme, keybindings: Keybindi
|
|
|
68
68
|
export interface ExtensionUIContext {
|
|
69
69
|
/** Show a selector and return the user's choice. */
|
|
70
70
|
select(title: string, options: string[], opts?: ExtensionUIDialogOptions): Promise<string | undefined>;
|
|
71
|
+
/** Show a multi-select dialog and return the user's selections (labels), or undefined if cancelled. */
|
|
72
|
+
multiselect?(title: string, options: string[], opts?: ExtensionUIDialogOptions): Promise<string[] | undefined>;
|
|
71
73
|
/** Show a confirmation dialog. */
|
|
72
74
|
confirm(title: string, message: string, opts?: ExtensionUIDialogOptions): Promise<boolean>;
|
|
73
75
|
/** Show a text input dialog. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Batch-first interactive question tool.
|
|
2
2
|
|
|
3
|
-
import type { ExtensionAPI, KeybindingsManager, Theme } from "@selesai/code";
|
|
3
|
+
import type { ExtensionAPI, ExtensionUIContext, KeybindingsManager, Theme } from "@selesai/code";
|
|
4
4
|
import {
|
|
5
5
|
Container,
|
|
6
6
|
type Component,
|
|
@@ -18,6 +18,78 @@ import {
|
|
|
18
18
|
import { buildQuestionAnswers, prepareQuestions, saveDraftQuestion } from "./batch.ts";
|
|
19
19
|
import { BOX_BORDER_LEFT, BOX_BORDER_OVERHEAD, BOX_BORDER_RIGHT, DISABLED_SHORTCUT, QUESTION_VERSION } from "./constants.ts";
|
|
20
20
|
import { createSelectionResponse, createTextResponse, formatQuestionResponse, formatResponseSummary, oneLine, previewText, trimText } from "./helpers.ts";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Answer a question outside the terminal TUI (e.g. RPC/extension mode) using
|
|
24
|
+
* the per-mode ExtensionUIContext, which emits extension_ui_request dialogs
|
|
25
|
+
* (select/input) that the VS Code host renders. Mirrors Cline/Kilo: a pending
|
|
26
|
+
* question is surfaced in the webview and blocks until the user answers.
|
|
27
|
+
*/
|
|
28
|
+
function answerQuestionViaUi(question: PreparedQuestion, ctx: { ui: ExtensionUIContext }): Promise<QuestionResponse | null> {
|
|
29
|
+
const title = question.context ? `${question.question}
|
|
30
|
+
|
|
31
|
+
${question.context}` : question.question;
|
|
32
|
+
if (question.type === "text") {
|
|
33
|
+
return ctx.ui.input(title).then((text) => createTextResponse(text));
|
|
34
|
+
}
|
|
35
|
+
const optionLabels = new Map(question.options.map((option) => [option.label, option.value] as const));
|
|
36
|
+
const optionValues = question.options.map((option) => option.value);
|
|
37
|
+
const labels = question.options.map((option) => option.label);
|
|
38
|
+
if (question.type === "select") {
|
|
39
|
+
return ctx.ui
|
|
40
|
+
.select(title, labels)
|
|
41
|
+
.then((selected) => {
|
|
42
|
+
if (selected === undefined) return null;
|
|
43
|
+
const value = optionLabels.get(selected);
|
|
44
|
+
const isOther = value === undefined;
|
|
45
|
+
if (isOther) {
|
|
46
|
+
if (!question.allowOther) return null;
|
|
47
|
+
return ctx.ui.input(title).then((text) => createSelectionResponse([], text ?? undefined));
|
|
48
|
+
}
|
|
49
|
+
return createSelectionResponse([value]);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// RPC/webview hosts can preserve true multi-selection. Older hosts fall back to
|
|
53
|
+
// one choice rather than making the question tool unavailable entirely. When
|
|
54
|
+
// allowOther is set the host sees an explicit "Other" choice, surfacing a free
|
|
55
|
+
// text follow-up exactly like the TUI wizard.
|
|
56
|
+
if (ctx.ui.multiselect) {
|
|
57
|
+
const other = question.allowOther ? ["Other"] : [];
|
|
58
|
+
return ctx.ui.multiselect(title, [...labels, ...other]).then((selected) => {
|
|
59
|
+
if (selected === undefined) return null;
|
|
60
|
+
const values = selected.filter((label) => label !== "Other").map((label) => optionLabels.get(label)).filter((value): value is string => value !== undefined);
|
|
61
|
+
if (question.allowOther && selected.includes("Other")) {
|
|
62
|
+
return ctx.ui.input(title).then((text) => createSelectionResponse(values, text ?? undefined));
|
|
63
|
+
}
|
|
64
|
+
return createSelectionResponse(values);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return ctx.ui.select(title, labels).then((selected) => {
|
|
68
|
+
if (selected === undefined) return null;
|
|
69
|
+
const value = optionLabels.get(selected);
|
|
70
|
+
return value === undefined
|
|
71
|
+
? question.allowOther ? ctx.ui.input(title).then((text) => createSelectionResponse([], text ?? undefined)) : null
|
|
72
|
+
: createSelectionResponse([value]);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Non-TUI fallback that answers each question in sequence via the host UI.
|
|
78
|
+
* Returns null when the user cancels any question.
|
|
79
|
+
*/
|
|
80
|
+
async function answerQuestionsViaUi(questions: PreparedQuestion[], ctx: { ui: ExtensionUIContext }): Promise<QuestionResponse[] | null> {
|
|
81
|
+
const answers: QuestionResponse[] = [];
|
|
82
|
+
for (const question of questions) {
|
|
83
|
+
const response = await answerQuestionViaUi(question, ctx);
|
|
84
|
+
if (response === null) return null;
|
|
85
|
+
answers.push(response);
|
|
86
|
+
}
|
|
87
|
+
return answers;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildAnswersFromResponses(questions: PreparedQuestion[], responses: QuestionResponse[]): QuestionAnswer[] {
|
|
91
|
+
return questions.map((question, index) => ({ id: question.id, status: "answered" as const, response: responses[index]! }));
|
|
92
|
+
}
|
|
21
93
|
import { QuestionList } from "./question-list.ts";
|
|
22
94
|
import { MultiSelect, SingleSelect } from "./selection-mode.ts";
|
|
23
95
|
import { QuestionParamsSchema } from "./schemas.ts";
|
|
@@ -557,14 +629,17 @@ export default function questionExtension(pi: ExtensionAPI) {
|
|
|
557
629
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
558
630
|
const prepared = prepareQuestions(params.questions);
|
|
559
631
|
if ("error" in prepared) throw new Error(prepared.error);
|
|
560
|
-
if (ctx.mode !== "tui") throw new Error("Question requires terminal TUI mode.");
|
|
561
632
|
if (signal?.aborted) return { content: [{ type: "text", text: "Question cancelled" }], details: { status: "cancelled", reason: "user" } satisfies QuestionToolResult };
|
|
562
633
|
|
|
563
634
|
onUpdate?.({ content: [{ type: "text", text: `Waiting for answers to ${prepared.questions.length} question${prepared.questions.length === 1 ? "" : "s"}...` }] });
|
|
564
|
-
const result =
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
635
|
+
const result: QuestionToolResult | undefined = ctx.mode === "tui"
|
|
636
|
+
? await ctx.ui.custom<QuestionToolResult>((tui, theme, keybindings, done) => {
|
|
637
|
+
if (signal) signal.addEventListener("abort", () => done({ status: "cancelled", reason: "user" }), { once: true });
|
|
638
|
+
return new BatchQuestionComponent(prepared.questions, tui, theme, keybindings, done);
|
|
639
|
+
})
|
|
640
|
+
: await answerQuestionsViaUi(prepared.questions, ctx).then((responses) =>
|
|
641
|
+
responses ? { status: "submitted" as const, answers: buildAnswersFromResponses(prepared.questions, responses) } : { status: "cancelled" as const, reason: "user" as const },
|
|
642
|
+
);
|
|
568
643
|
if (!result) throw new Error("Question TUI did not return a result.");
|
|
569
644
|
|
|
570
645
|
if (result.status === "cancelled") {
|
|
@@ -197,13 +197,29 @@ describe("question wizard", () => {
|
|
|
197
197
|
expect(emitted2).toEqual(["question:submitted"]);
|
|
198
198
|
});
|
|
199
199
|
|
|
200
|
-
it("
|
|
200
|
+
it("uses extension UI requests in RPC mode", async () => {
|
|
201
201
|
let tool: any;
|
|
202
202
|
questionExtension({ registerTool: (entry: unknown) => (tool = entry), events: { emit() {} } } as any);
|
|
203
|
+
const result = await tool.execute("call", { questions: [{ type: "select", question: "Choose", options: [{ value: "x", label: "X" }] }] }, undefined, undefined, {
|
|
204
|
+
mode: "rpc",
|
|
205
|
+
ui: { select: async () => "X" },
|
|
206
|
+
} as any);
|
|
207
|
+
expect(result.details).toEqual({ status: "submitted", answers: [{ id: "q1", status: "answered", response: { kind: "selection", values: ["x"] } }] });
|
|
208
|
+
});
|
|
203
209
|
|
|
204
|
-
|
|
210
|
+
it("preserves multiple RPC selections", async () => {
|
|
211
|
+
let tool: any;
|
|
212
|
+
questionExtension({ registerTool: (entry: unknown) => (tool = entry), events: { emit() {} } } as any);
|
|
213
|
+
const result = await tool.execute("call", { questions: [{ type: "multiselect", question: "Choose many", options: [{ value: "a", label: "A" }, { value: "b", label: "B" }] }] }, undefined, undefined, {
|
|
205
214
|
mode: "rpc",
|
|
206
|
-
|
|
215
|
+
ui: { multiselect: async () => ["A", "B"] },
|
|
216
|
+
} as any);
|
|
217
|
+
expect(result.details).toEqual({ status: "submitted", answers: [{ id: "q1", status: "answered", response: { kind: "selection", values: ["a", "b"] } }] });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("rejects invalid params", async () => {
|
|
221
|
+
let tool: any;
|
|
222
|
+
questionExtension({ registerTool: (entry: unknown) => (tool = entry), events: { emit() {} } } as any);
|
|
207
223
|
|
|
208
224
|
await expect(tool.execute("call", { questions: [] }, undefined, undefined, {
|
|
209
225
|
mode: "tui",
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
// ponytail: thin
|
|
2
|
-
// each mode's scripted workflow through pi-subagents' launchSlashSubagent.
|
|
1
|
+
// ponytail: thin slash-command adapter for the pi-subagents workflow runtime.
|
|
3
2
|
|
|
4
3
|
import type { ExtensionAPI } from "@selesai/code";
|
|
5
4
|
import { launchSlashSubagent } from "../pi-subagents/src/slash/slash-commands.ts";
|
|
6
|
-
import {
|
|
5
|
+
import { WORKFLOW_MODES, type WorkflowMode } from "./modes.ts";
|
|
7
6
|
|
|
8
7
|
export default function workflowModesExtension(pi: ExtensionAPI): void {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
const register = (mode: WorkflowMode) => pi.registerCommand(mode.command, {
|
|
9
|
+
description: mode.description,
|
|
10
|
+
handler: async (args, ctx) => {
|
|
11
|
+
const goal = args.trim();
|
|
12
|
+
if (!goal) {
|
|
13
|
+
ctx.ui.notify(`${mode.description}\nUsage: /${mode.command} <goal>`, "info");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
launchSlashSubagent(pi, ctx, {
|
|
17
|
+
...mode.launch(goal),
|
|
18
|
+
async: true,
|
|
19
|
+
agentScope: "both",
|
|
20
|
+
mission: { title: goal },
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
});
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
register("workflow-prototype", "Run the full prototype workflow (research → plan → reuse → handoff → auto loop → audit) as a scripted workflow.", buildPrototypeScript);
|
|
24
|
-
register("workflow-quicktype", "Run the quicker prototype workflow without research (plan → reuse → handoff → auto loop → audit) as a scripted workflow.", buildQuicktypeScript);
|
|
25
|
-
register("workflow-loop", "Run a direct auto build→review→fix loop for an already-agreed plan as a scripted workflow.", buildLoopScript);
|
|
25
|
+
for (const mode of WORKFLOW_MODES) register(mode);
|
|
26
26
|
}
|
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
// ponytail:
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// ponytail: workflow mode registry over pi-subagents' public workflowScript seam.
|
|
2
|
+
// A mode returns launch parameters; the extension owns slash-command plumbing.
|
|
3
|
+
|
|
4
|
+
import type { SubagentParamsLike } from "../pi-subagents/src/runs/foreground/subagent-executor.ts";
|
|
5
|
+
|
|
6
|
+
export interface WorkflowMode {
|
|
7
|
+
command: string;
|
|
8
|
+
description: string;
|
|
9
|
+
launch(goal: string): Pick<SubagentParamsLike, "workflowScript" | "chain" | "tasks" | "concurrency">;
|
|
10
|
+
}
|
|
5
11
|
|
|
6
12
|
function js(value: string): string {
|
|
7
|
-
|
|
13
|
+
return JSON.stringify(value);
|
|
8
14
|
}
|
|
9
15
|
|
|
16
|
+
// A loop depends on the previous review's output, so it belongs in pi-subagents'
|
|
17
|
+
// scripted workflow runtime rather than a fixed native chain.
|
|
10
18
|
const AUTO_LOOP = String.raw`
|
|
11
19
|
const autoLoop = async (goal, context, progressFile) => {
|
|
12
20
|
emit({ phase: 'start', goal });
|
|
@@ -25,14 +33,15 @@ const autoLoop = async (goal, context, progressFile) => {
|
|
|
25
33
|
timeoutMs: 15 * 60 * 1000,
|
|
26
34
|
task: 'Independently review the builder work for this round and report concrete evidence (what you inspected and what you ran). Do not modify the workspace.\n\nAcceptance criteria (source of truth):\n' + context + '\n\nProgress file (scope your review to its latest round entry; also re-check the files from the immediately preceding fix entry if one exists; fall back to the full uncommitted diff if it is missing or empty):\n' + progressFile + '\n\nBuilder completion summary:\n' + build.output + '\n\nIf the plan is not yet complete, add a "Remaining work:" section listing the next concrete step(s). End with exactly one line: WORKFLOW_REVIEW_STATUS: clean OR WORKFLOW_REVIEW_STATUS: blocking.',
|
|
27
35
|
});
|
|
28
|
-
|
|
36
|
+
const hasRemainingWork = /Remaining work\s*:\s*\S/i.test(review.output);
|
|
37
|
+
if (/WORKFLOW_REVIEW_STATUS\s*:\s*clean/i.test(review.output) && !hasRemainingWork) {
|
|
29
38
|
return { result: 'clean', rounds: completed + 1 };
|
|
30
39
|
}
|
|
31
40
|
previousReview = review.output;
|
|
32
41
|
await runs.run('fix-' + round, {
|
|
33
42
|
agent: 'builder',
|
|
34
43
|
timeoutMs: 45 * 60 * 1000,
|
|
35
|
-
task: 'Address ONLY the findings from the review below. The "Remaining work:" section (if present) is for the next round; do not act on it.\n\nProgress ledger: append a "## Round ' + round + ' fix" entry to the progress file at ' + progressFile + ' before finishing. List every file you changed and a short summary of the fixes.\n\nReviewer findings:\n' + review.output,
|
|
44
|
+
task: 'Address ONLY the findings from the review below. The "Remaining work:" section (if present) is for the next round; do not act on it. If the review is clean but has Remaining work, make no changes and record that fact.\n\nProgress ledger: append a "## Round ' + round + ' fix" entry to the progress file at ' + progressFile + ' before finishing. List every file you changed and a short summary of the fixes.\n\nReviewer findings:\n' + review.output,
|
|
36
45
|
});
|
|
37
46
|
completed += 1;
|
|
38
47
|
round += 1;
|
|
@@ -49,13 +58,13 @@ const autoLoop = async (goal, context, progressFile) => {
|
|
|
49
58
|
const PROGRESS_DIR = ".pi-subagents/progress/";
|
|
50
59
|
|
|
51
60
|
export function buildLoopScript(goal: string): string {
|
|
52
|
-
|
|
61
|
+
return String.raw`const goal = ${js(goal)};
|
|
53
62
|
${AUTO_LOOP}
|
|
54
63
|
return await autoLoop(goal, goal, ${js(PROGRESS_DIR + "loop.md")});`;
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
export function buildTaskScript(goal: string): string {
|
|
58
|
-
|
|
67
|
+
return String.raw`const goal = ${js(goal)};
|
|
59
68
|
const plan = await runs.run('plan', { agent: 'architect', task: 'Produce a concrete implementation plan for: ' + goal + '. Cover what to build, how, in what order, which files and components, and the finished result. Return inline.' });
|
|
60
69
|
const reuse = await runs.run('reuse', { agent: 'explorer', task: 'Explore the codebase for reusable patterns relevant to: ' + plan.output + '. Point at relevant areas and dependencies; skip cleanly if wholly new. Return inline.' });
|
|
61
70
|
const handoff = await runs.run('handoff', { agent: 'recapper', task: 'Compile a self-contained handoff from the plan and reuse findings so fresh agents understand the goal, constraints, and acceptance criteria without re-planning.\n\nPlan:\n' + plan.output + '\n\nReuse findings:\n' + reuse.output + '\n\nReturn inline.' });
|
|
@@ -64,10 +73,14 @@ return await autoLoop(goal, handoff.output, ${js(PROGRESS_DIR + "task.md")});`;
|
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
export function buildPrototypeScript(goal: string): string {
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
return String.raw`const goal = ${js(goal)};
|
|
77
|
+
const discovery = await runs.all([
|
|
78
|
+
{ key: 'research', agent: 'researcher', task: 'Research the external, fast-changing knowledge this task depends on (libraries, SDKs, APIs, unfamiliar alternatives). Task: ' + goal + '. Synthesize actionable findings with sources. Return inline.' },
|
|
79
|
+
{ key: 'explore', agent: 'explorer', task: 'Map existing code, dependencies, and reusable patterns relevant to: ' + goal + '. Return inline.' },
|
|
80
|
+
]);
|
|
81
|
+
const research = discovery.find(result => result.key === 'research');
|
|
82
|
+
const reuse = discovery.find(result => result.key === 'explore');
|
|
83
|
+
const plan = await runs.run('plan', { agent: 'architect', task: 'Produce a concrete build plan from the research and codebase findings.\n\nResearch:\n' + research.output + '\n\nCodebase findings:\n' + reuse.output + '\n\nRequest:\n' + goal + '\n\nReturn inline.' });
|
|
71
84
|
const handoff = await runs.run('handoff', { agent: 'recapper', task: 'Compile a self-contained handoff from the plan and reuse findings.\n\nPlan:\n' + plan.output + '\n\nReuse:\n' + reuse.output + '\n\nReturn inline.' });
|
|
72
85
|
${AUTO_LOOP}
|
|
73
86
|
const loop = await autoLoop(goal, handoff.output, ${js(PROGRESS_DIR + "prototype.md")});
|
|
@@ -76,7 +89,7 @@ return { ...loop, audited: true };`;
|
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
export function buildQuicktypeScript(goal: string): string {
|
|
79
|
-
|
|
92
|
+
return String.raw`const goal = ${js(goal)};
|
|
80
93
|
const plan = await runs.run('plan', { agent: 'architect', task: 'Produce a concrete build plan for: ' + goal + '. Cover what to build, how, in what order, which components, and the finished result. Return inline.' });
|
|
81
94
|
const reuse = await runs.run('reuse', { agent: 'explorer', task: 'Explore the codebase for reusable patterns relevant to: ' + plan.output + '. Return inline.' });
|
|
82
95
|
const handoff = await runs.run('handoff', { agent: 'recapper', task: 'Compile a self-contained handoff from the plan and reuse findings.\n\nPlan:\n' + plan.output + '\n\nReuse:\n' + reuse.output + '\n\nReturn inline.' });
|
|
@@ -85,3 +98,10 @@ const loop = await autoLoop(goal, handoff.output, ${js(PROGRESS_DIR + "quicktype
|
|
|
85
98
|
const audit = await runs.run('audit', { agent: 'commentator', task: 'Final audit of the uncommitted changes for correctness, plan adherence, and over-engineering (cut bloat, dead flexibility, reinvented stdlib). Plan:\n' + plan.output + '\n\nReport concrete evidence. Do not modify the workspace.' });
|
|
86
99
|
return { ...loop, audited: true };`;
|
|
87
100
|
}
|
|
101
|
+
|
|
102
|
+
export const WORKFLOW_MODES: readonly WorkflowMode[] = [
|
|
103
|
+
{ command: "workflow-task", description: "Run the task workflow (plan → reuse → handoff → build/review/fix loop).", launch: (goal) => ({ workflowScript: buildTaskScript(goal) }) },
|
|
104
|
+
{ command: "workflow-prototype", description: "Run the prototype workflow (parallel research/reuse → plan → handoff → loop → audit).", launch: (goal) => ({ workflowScript: buildPrototypeScript(goal) }) },
|
|
105
|
+
{ command: "workflow-quicktype", description: "Run the quicker prototype workflow (plan → reuse → handoff → loop → audit).", launch: (goal) => ({ workflowScript: buildQuicktypeScript(goal) }) },
|
|
106
|
+
{ command: "workflow-loop", description: "Run a direct build/review/fix loop for an already-agreed plan.", launch: (goal) => ({ workflowScript: buildLoopScript(goal) }) },
|
|
107
|
+
];
|
|
@@ -81,6 +81,7 @@ export async function runRpcMode(runtimeHost) {
|
|
|
81
81
|
*/
|
|
82
82
|
const createExtensionUIContext = () => ({
|
|
83
83
|
select: (title, options, opts) => createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined),
|
|
84
|
+
multiselect: (title, options, opts) => createDialogPromise(opts, undefined, { method: "multiselect", title, options, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? undefined : "values" in r ? r.values : undefined),
|
|
84
85
|
confirm: (title, message, opts) => createDialogPromise(opts, false, { method: "confirm", title, message, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? false : "confirmed" in r ? r.confirmed : false),
|
|
85
86
|
input: (title, placeholder, opts) => createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined),
|
|
86
87
|
notify(message, type) {
|
|
@@ -418,6 +418,13 @@ export type RpcExtensionUIRequest = {
|
|
|
418
418
|
title: string;
|
|
419
419
|
options: string[];
|
|
420
420
|
timeout?: number;
|
|
421
|
+
} | {
|
|
422
|
+
type: "extension_ui_request";
|
|
423
|
+
id: string;
|
|
424
|
+
method: "multiselect";
|
|
425
|
+
title: string;
|
|
426
|
+
options: string[];
|
|
427
|
+
timeout?: number;
|
|
421
428
|
} | {
|
|
422
429
|
type: "extension_ui_request";
|
|
423
430
|
id: string;
|
|
@@ -473,6 +480,10 @@ export type RpcExtensionUIResponse = {
|
|
|
473
480
|
type: "extension_ui_response";
|
|
474
481
|
id: string;
|
|
475
482
|
value: string;
|
|
483
|
+
} | {
|
|
484
|
+
type: "extension_ui_response";
|
|
485
|
+
id: string;
|
|
486
|
+
values: string[];
|
|
476
487
|
} | {
|
|
477
488
|
type: "extension_ui_response";
|
|
478
489
|
id: string;
|
package/docs/extensions.md
CHANGED
|
@@ -9,7 +9,7 @@ Extensions are TypeScript modules that extend pi's behavior. They can subscribe
|
|
|
9
9
|
**Key capabilities:**
|
|
10
10
|
- **Custom tools** - Register tools the LLM can call via `pi.registerTool()`
|
|
11
11
|
- **Event interception** - Block or modify tool calls, inject context, customize compaction
|
|
12
|
-
- **User interaction** - Prompt users via `ctx.ui` (select, confirm, input, notify)
|
|
12
|
+
- **User interaction** - Prompt users via `ctx.ui` (select, multiselect, confirm, input, notify)
|
|
13
13
|
- **Custom UI components** - Full TUI components with keyboard input via `ctx.ui.custom()` for complex interactions
|
|
14
14
|
- **Custom commands** - Register commands like `/mycommand` via `pi.registerCommand()`
|
|
15
15
|
- **Session persistence** - Store state that survives restarts via `pi.appendEntry()`
|
|
@@ -2201,6 +2201,9 @@ Extensions can interact with users via `ctx.ui` methods and customize how messag
|
|
|
2201
2201
|
// Select from options
|
|
2202
2202
|
const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);
|
|
2203
2203
|
|
|
2204
|
+
// Multi-select from options (string[] | undefined; optional in hosts that do not support it)
|
|
2205
|
+
const choices = await ctx.ui.multiselect?.("Pick many:", ["A", "B", "C"]);
|
|
2206
|
+
|
|
2204
2207
|
// Confirm dialog
|
|
2205
2208
|
const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
|
|
2206
2209
|
|
package/docs/rpc.md
CHANGED
|
@@ -1008,7 +1008,7 @@ Extensions can request user interaction via `ctx.ui.select()`, `ctx.ui.confirm()
|
|
|
1008
1008
|
|
|
1009
1009
|
There are two categories of extension UI methods:
|
|
1010
1010
|
|
|
1011
|
-
- **Dialog methods** (`select`, `confirm`, `input`, `editor`): emit an `extension_ui_request` on stdout and block until the client sends back an `extension_ui_response` on stdin with the matching `id`.
|
|
1011
|
+
- **Dialog methods** (`select`, `multiselect`, `confirm`, `input`, `editor`): emit an `extension_ui_request` on stdout and block until the client sends back an `extension_ui_response` on stdin with the matching `id`.
|
|
1012
1012
|
- **Fire-and-forget methods** (`notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`): emit an `extension_ui_request` on stdout but do not expect a response. The client can display the information or ignore it.
|
|
1013
1013
|
|
|
1014
1014
|
If a dialog method includes a `timeout` field, the agent-side will auto-resolve with a default value when the timeout expires. The client does not need to track timeouts.
|
|
@@ -1046,6 +1046,23 @@ Prompt the user to choose from a list. Dialog methods with a `timeout` field inc
|
|
|
1046
1046
|
|
|
1047
1047
|
Expected response: `extension_ui_response` with `value` (the selected option string) or `cancelled: true`.
|
|
1048
1048
|
|
|
1049
|
+
#### multiselect
|
|
1050
|
+
|
|
1051
|
+
Prompt the user to choose zero or more options from a list. Dialog methods with a `timeout` field include the timeout in milliseconds; the agent auto-resolves with `undefined` if the client doesn't respond in time.
|
|
1052
|
+
|
|
1053
|
+
```json
|
|
1054
|
+
{
|
|
1055
|
+
"type": "extension_ui_request",
|
|
1056
|
+
"id": "uuid-10",
|
|
1057
|
+
"method": "multiselect",
|
|
1058
|
+
"title": "Which regions?",
|
|
1059
|
+
"options": ["us-east", "eu-west", "ap-southeast"],
|
|
1060
|
+
"timeout": 10000
|
|
1061
|
+
}
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
Expected response: `extension_ui_response` with `values` (the selected option strings) or `cancelled: true`.
|
|
1065
|
+
|
|
1049
1066
|
#### confirm
|
|
1050
1067
|
|
|
1051
1068
|
Prompt the user for yes/no confirmation.
|
|
@@ -1172,7 +1189,7 @@ Set the text in the input editor. Fire-and-forget.
|
|
|
1172
1189
|
|
|
1173
1190
|
### Extension UI Responses (stdin)
|
|
1174
1191
|
|
|
1175
|
-
Responses are sent for dialog methods only (`select`, `confirm`, `input`, `editor`). The `id` must match the request.
|
|
1192
|
+
Responses are sent for dialog methods only (`select`, `multiselect`, `confirm`, `input`, `editor`). The `id` must match the request.
|
|
1176
1193
|
|
|
1177
1194
|
#### Value response (select, input, editor)
|
|
1178
1195
|
|
|
@@ -1180,6 +1197,12 @@ Responses are sent for dialog methods only (`select`, `confirm`, `input`, `edito
|
|
|
1180
1197
|
{"type": "extension_ui_response", "id": "uuid-1", "value": "Allow"}
|
|
1181
1198
|
```
|
|
1182
1199
|
|
|
1200
|
+
#### Values response (multiselect)
|
|
1201
|
+
|
|
1202
|
+
```json
|
|
1203
|
+
{"type": "extension_ui_response", "id": "uuid-10", "values": ["us-east", "ap-southeast"]}
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1183
1206
|
#### Confirmation response (confirm)
|
|
1184
1207
|
|
|
1185
1208
|
```json
|
|
@@ -1188,7 +1211,7 @@ Responses are sent for dialog methods only (`select`, `confirm`, `input`, `edito
|
|
|
1188
1211
|
|
|
1189
1212
|
#### Cancellation response (any dialog)
|
|
1190
1213
|
|
|
1191
|
-
Dismiss any dialog method. The extension receives `undefined` (for select/input/editor) or `false` (for confirm).
|
|
1214
|
+
Dismiss any dialog method. The extension receives `undefined` (for select/multiselect/input/editor) or `false` (for confirm).
|
|
1192
1215
|
|
|
1193
1216
|
```json
|
|
1194
1217
|
{"type": "extension_ui_response", "id": "uuid-3", "cancelled": true}
|
package/docs/workflows.md
CHANGED
|
@@ -1,250 +1,57 @@
|
|
|
1
1
|
# Workflows
|
|
2
2
|
|
|
3
|
-
Selesai
|
|
3
|
+
Selesai's built-in workflows are thin slash-command adapters over the **pi-subagents** orchestration runtime. They do not have a separate state machine, artifact protocol, or `workflow.json` format.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Run a workflow
|
|
6
6
|
|
|
7
|
+
```text
|
|
8
|
+
/workflow-task <goal>
|
|
9
|
+
/workflow-prototype <goal>
|
|
10
|
+
/workflow-quicktype <goal>
|
|
11
|
+
/workflow-loop <goal>
|
|
7
12
|
```
|
|
8
|
-
src/extensions/workflow/
|
|
9
|
-
package.json pi package manifest; loads ./extension.ts as the single entry
|
|
10
|
-
state-machine.ts pure phase state machine (no fs, no pi API)
|
|
11
|
-
adapter.ts pi wiring: tools, commands, events, fs, durable-state lifecycle
|
|
12
|
-
run-state.ts versioned atomic workflow.json load/save/discovery
|
|
13
|
-
extension.ts single pi extension that mounts every workflow mode
|
|
14
|
-
modes/
|
|
15
|
-
prototype.ts mode config + registration object (exported as `prototypeMode`)
|
|
16
|
-
quicktype.ts mode config + registration object (exported as `quicktypeMode`)
|
|
17
|
-
task.ts mode config + registration object (exported as `taskMode`)
|
|
18
|
-
loop.ts mode config + registration object (exported as `loopMode`)
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
- **`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.
|
|
22
|
-
- **`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 and queue hidden engine continuations; every built-in mode flows automatically.
|
|
23
|
-
- **`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.
|
|
24
|
-
- **`extension.ts`** imports each mode's registration object and calls `createWorkflowExtension(config, options)(pi)` for each. One extension load registers the model-facing artifact writer and `end_workflow` tool. Starting and resuming are user-only actions exposed by each mode's slash command.
|
|
25
|
-
- **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`, `quicktypeMode`); it does not call `createWorkflowExtension` itself.
|
|
26
|
-
|
|
27
|
-
## To add a future mode
|
|
28
|
-
|
|
29
|
-
Copy `modes/quicktype.ts` (the smaller one) and change the config. That's the whole change — the engine never needs editing.
|
|
30
|
-
|
|
31
|
-
### 1. Create the mode file
|
|
32
|
-
|
|
33
|
-
`src/extensions/workflow/modes/rigorous.ts`:
|
|
34
|
-
|
|
35
|
-
```typescript
|
|
36
|
-
import type {
|
|
37
|
-
Phase,
|
|
38
|
-
PromptContext,
|
|
39
|
-
WorkflowConfig,
|
|
40
|
-
WorkflowModeRegistration,
|
|
41
|
-
} from "../state-machine.ts";
|
|
42
|
-
|
|
43
|
-
const phases: Phase[] = [
|
|
44
|
-
"grilling",
|
|
45
|
-
"spec", // ← new phase, not in the built-in set
|
|
46
|
-
"research",
|
|
47
|
-
"plan",
|
|
48
|
-
"reuse",
|
|
49
|
-
"handoff",
|
|
50
|
-
"loop",
|
|
51
|
-
"audit",
|
|
52
|
-
"sign-off", // ← new terminal phase
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
const prompts: Partial<Record<Phase, (ctx: PromptContext) => string>> = {
|
|
56
|
-
grilling: ({ artifactDir, userPrompt }) => `…grilling prompt…`,
|
|
57
|
-
spec: ({ artifactDir }) => `…spec prompt…`,
|
|
58
|
-
research: ({ artifactDir }) => `…research prompt…`,
|
|
59
|
-
plan: ({ artifactDir }) => `…plan prompt…`,
|
|
60
|
-
reuse: ({ artifactDir }) => `…reuse prompt…`,
|
|
61
|
-
handoff: ({ artifactDir }) => `…handoff prompt…`,
|
|
62
|
-
loop: ({ artifactDir }) => `…loop prompt…`,
|
|
63
|
-
audit: ({ artifactDir }) => `…audit prompt…`,
|
|
64
|
-
"sign-off": ({ artifactDir }) => `…sign-off prompt…`,
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const config: WorkflowConfig = {
|
|
68
|
-
mode: "rigorous",
|
|
69
|
-
phases,
|
|
70
|
-
phaseArtifacts: {
|
|
71
|
-
grilling: "requirements.md",
|
|
72
|
-
spec: "spec.md",
|
|
73
|
-
research: "research.md",
|
|
74
|
-
plan: "plan.md",
|
|
75
|
-
reuse: "reuse.md",
|
|
76
|
-
handoff: "handoff.md",
|
|
77
|
-
loop: "loop-complete.md",
|
|
78
|
-
audit: "review.md",
|
|
79
|
-
"sign-off": "acceptance.md",
|
|
80
|
-
},
|
|
81
|
-
prompts,
|
|
82
|
-
// Files that must exist before end() can close the workflow.
|
|
83
|
-
// Config-owned — declare whatever your terminal phase requires.
|
|
84
|
-
closeArtifacts: ["acceptance.md", "sign-off-report.md"],
|
|
85
|
-
statusKey: "rigorous",
|
|
86
|
-
entryType: "rigorous-phase",
|
|
87
|
-
footerLabel: "rigorous",
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
export const rigorousMode: WorkflowModeRegistration = {
|
|
91
|
-
config,
|
|
92
|
-
commandName: "rigorous",
|
|
93
|
-
commandDescription:
|
|
94
|
-
"Run the rigorous workflow (grill → spec → research → plan → reuse → handoff → loop → audit → sign-off)",
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
export default rigorousMode;
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### 2. Register it in `extension.ts`
|
|
101
|
-
|
|
102
|
-
Add the mode to the `MODES` array in `src/extensions/workflow/extension.ts`:
|
|
103
13
|
|
|
104
|
-
|
|
105
|
-
import { rigorousMode } from "./modes/rigorous.ts";
|
|
106
|
-
|
|
107
|
-
const MODES = [prototypeMode, quicktypeMode, rigorousMode] as const;
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
That's it. The loader picks it up at boot (`package.json` loads only `./extension.ts`), and the `/rigorous` command is registered automatically. There is no model-facing start/resume or `next` tool — users start and resume through `/rigorous`, phases auto-advance as artifacts land, and only `end_workflow({ mode: "rigorous" })` completes the terminal phase.
|
|
14
|
+
Each command starts an async pi-subagents mission. Recover a completed, paused, or confusing run with the pi-subagents mission and status controls (`/subagents`, `/subagents-doctor`, or the corresponding `subagent` tool actions); there is no `/workflow-* resume` command.
|
|
111
15
|
|
|
112
16
|
## Built-in modes
|
|
113
17
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
-
|
|
119
|
-
-
|
|
120
|
-
|
|
121
|
-
### `task` — plan → codebase exploration → handoff → build/review loop
|
|
122
|
-
|
|
123
|
-
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.
|
|
124
|
-
|
|
125
|
-
Lifecycle: `plan → reuse → handoff → loop (build ↔ review) → terminal-ready → end_workflow({ mode: "task" })`
|
|
126
|
-
|
|
127
|
-
- `/workflow-task <goal>` — start a new run
|
|
128
|
-
- `/workflow-task resume` — list and resume active runs
|
|
129
|
-
- `/workflow-task help` — show the lifecycle
|
|
130
|
-
- Valid phase artifacts automatically queue the next phase prompt (the workflow does not pause at artifact boundaries)
|
|
131
|
-
- No grilling, research, or audit phases
|
|
132
|
-
- `reuse.md` is optional; it is skipped automatically when the project has no git history
|
|
133
|
-
|
|
134
|
-
### `loop` — direct build/review loop
|
|
135
|
-
|
|
136
|
-
Use this after the plan was already agreed in the current conversation. It captures the agreed context into a parent-owned `handoff.md` artifact, then runs an engine-owned `loop` phase: builder changes workspace code, commentator independently validates the diff and relevant checks, then blocking feedback returns to the builder (max 3 blocking rounds). A clean review writes `loop-complete.md`, makes the run terminal-ready, and requires explicit completion.
|
|
137
|
-
|
|
138
|
-
Fresh subagents do not inherit the parent conversation. The parent forks a `recapper` subagent once to synthesize a concise, self-contained handoff document directly from the inherited conversation. The parent validates the handoff marker and writes `handoff.md` via `write_workflow_artifact`. After that, every builder and commentator call reads `handoff.md` instead of relying on the parent conversation. Persisted `loop-review-N.md` files feed blocking fixes back to the builder.
|
|
139
|
-
|
|
140
|
-
Lifecycle: `handoff → loop (build ↔ review) → terminal-ready → end_workflow({ mode: "loop" })`
|
|
141
|
-
|
|
142
|
-
- `/workflow-loop <goal>` — start a direct build/review run
|
|
143
|
-
- `/workflow-loop resume` / `/workflow-loop resume <id-or-artifact-dir-or-workflow.json>` — list or resume a run
|
|
144
|
-
- `/workflow-loop help` — show the lifecycle
|
|
145
|
-
|
|
146
|
-
## Config reference
|
|
18
|
+
| Command | Shape |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `/workflow-task` | plan → reuse → handoff → build/review/fix loop |
|
|
21
|
+
| `/workflow-prototype` | parallel research + codebase exploration → plan → handoff → build/review/fix loop → audit |
|
|
22
|
+
| `/workflow-quicktype` | plan → reuse → handoff → build/review/fix loop → audit |
|
|
23
|
+
| `/workflow-loop` | direct build/review/fix loop for an already-agreed plan |
|
|
147
24
|
|
|
148
|
-
|
|
149
|
-
|---|---|---|
|
|
150
|
-
| `mode` | `string` | Mode name, echoed in entry payloads and messages. |
|
|
151
|
-
| `phases` | `Phase[]` | Ordered phase list. `Phase` is `string` — new phase names are allowed. |
|
|
152
|
-
| `phaseArtifacts` | `Partial<Record<Phase, string>>` | The artifact file each phase must produce before advancing. Omit a phase to skip its gate. |
|
|
153
|
-
| `prompts` | `Partial<Record<Phase, (ctx) => string>>` | Prompt generator per phase. `ctx = { artifactDir, userPrompt }`. |
|
|
154
|
-
| `closeArtifacts` | `string[]` | Files that must exist before `end()` succeeds. Config-owned, no built-in default. |
|
|
155
|
-
| `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` when the project has no git history). |
|
|
156
|
-
| `statusKey` | `string` | Footer status key. |
|
|
157
|
-
| `entryType` | `string` | Session-history custom-type. It stores a pointer only; `workflow.json` is canonical. |
|
|
158
|
-
| `footerLabel` | `string` | Label shown in the footer (`● label · step/total phase`). |
|
|
25
|
+
The prototype mode uses `runs.all` for its independent research and codebase-exploration work. All modes use `runs.run` for ordered handoffs. The build/review/fix loop uses `workflowScript` because its next step depends on the reviewer result; a blocking review gets a scoped fix round, while `clean` plus no remaining work ends the run.
|
|
159
26
|
|
|
160
|
-
|
|
27
|
+
## Extending workflows
|
|
161
28
|
|
|
162
|
-
The
|
|
29
|
+
The extension seam is `src/extensions/workflow/modes.ts`.
|
|
163
30
|
|
|
164
|
-
|
|
165
|
-
|---|---|
|
|
166
|
-
| `commandName` | The `/<command>` name users type to kick off the workflow. |
|
|
167
|
-
| `commandDescription` | Description shown in the command list. |
|
|
31
|
+
Add one `WorkflowMode` entry to `WORKFLOW_MODES`:
|
|
168
32
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
- `/workflow-prototype help`, `/workflow-quicktype help`, `/workflow-task help`, or `/workflow-loop help` shows the start, resume, continue, and explicit-completion lifecycle.
|
|
179
|
-
|
|
180
|
-
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. Valid artifact writes queue one hidden engine-controlled continuation using `steer` and terminate the current parent turn; invalid writes stay in the current phase and do not terminate. Prompts injected by start, resume, and continue commands are hidden custom messages rather than visible synthetic user messages. Transition-capable calls (`write_workflow_artifact`, loop commentator transitions, and `end_workflow`) must be the sole tool call in their assistant batch; the adapter fails closed when that cannot be proven. Corrupt records are skipped during discovery. Reloads never auto-resume; the user must explicitly resume through a mode's slash command.
|
|
181
|
-
|
|
182
|
-
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.
|
|
183
|
-
|
|
184
|
-
## Artifact ownership
|
|
185
|
-
|
|
186
|
-
Workflow artifacts have one writer: the parent session's `write_workflow_artifact` tool. Every workflow child call uses `output: false` and returns inline. In artifact phases (`plan`, `reuse`, `handoff`, and `audit`), the parent inspects that result, validates any required marker, and immediately passes it to `write_workflow_artifact`. Child output paths and fallback persistence are deliberately disabled; a child result alone cannot create an artifact or advance a phase.
|
|
187
|
-
|
|
188
|
-
The implement/review loop is the explicit exception to parent persistence, not inline return: the engine persists `loop-review-<round>.md` and `loop-complete.md` from commentator results so it can manage review rounds. Builders only change workspace code.
|
|
189
|
-
|
|
190
|
-
Every state-machine method returns a `WorkflowEffect` — a discriminated union the adapter switches on:
|
|
191
|
-
|
|
192
|
-
| Effect | Meaning |
|
|
193
|
-
|---|---|
|
|
194
|
-
| `started` | `start()` succeeded; first phase prompt + entry + footer. |
|
|
195
|
-
| `alreadyActive` | `start()` called while a workflow is active. |
|
|
196
|
-
| `advanced` | Phase moved forward (optionally `skipped` a phase). |
|
|
197
|
-
| `blocked` | Current phase's artifact is missing. |
|
|
198
|
-
| `terminalNeedsArtifacts` | At the last phase; a close artifact is missing. |
|
|
199
|
-
| `terminalReady` | At the last phase; all close artifacts present — call `end()`. |
|
|
200
|
-
| `closed` | `end()` succeeded; workflow finished. |
|
|
201
|
-
| `endBlocked` | `end()` called from the wrong phase or with close artifacts missing. |
|
|
202
|
-
| `idle` | No active workflow. |
|
|
203
|
-
| `noOp` | Auto-advance checked, nothing to do (not active, not armed, artifact not present, or already advancing). |
|
|
204
|
-
|
|
205
|
-
The `tool_result` auto-advance hook is one line:
|
|
206
|
-
|
|
207
|
-
```typescript
|
|
208
|
-
const eff = await sm.onArtifactMaybe(deps);
|
|
209
|
-
applyEffect(pi, ctx, config, eff);
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
The reentrancy guard lives inside `onArtifactMaybe` — concurrent calls return `noOp`, so a double `write` in one turn cannot double-advance the phase.
|
|
213
|
-
|
|
214
|
-
## Skip rules
|
|
215
|
-
|
|
216
|
-
By default the adapter skips the `reuse` phase when the project has no git history. To override, supply `skipRules` in your config:
|
|
217
|
-
|
|
218
|
-
```typescript
|
|
219
|
-
skipRules: [
|
|
220
|
-
{ phase: "research", shouldSkip: async () => isWellUnderstoodDomain() },
|
|
221
|
-
{ phase: "reuse", shouldSkip: async () => isEmptyProject() },
|
|
222
|
-
],
|
|
33
|
+
```ts
|
|
34
|
+
{
|
|
35
|
+
command: "workflow-rigorous",
|
|
36
|
+
description: "Run the rigorous workflow.",
|
|
37
|
+
launch: (goal) => ({
|
|
38
|
+
workflowScript: `const goal = ${JSON.stringify(goal)};
|
|
39
|
+
return runs.run("plan", { agent: "architect", task: "Plan: " + goal });`,
|
|
40
|
+
}),
|
|
41
|
+
}
|
|
223
42
|
```
|
|
224
43
|
|
|
225
|
-
`
|
|
226
|
-
|
|
227
|
-
## Testing a mode
|
|
44
|
+
`launch(goal)` returns pi-subagents public execution fields. Prefer the native execution shapes where the mode is static:
|
|
228
45
|
|
|
229
|
-
|
|
46
|
+
- `chain` for a fixed ordered sequence, including human checkpoints.
|
|
47
|
+
- `tasks` for independent, read-only parallel work.
|
|
48
|
+
- `workflowScript` only when the orchestration is conditional, iterative, needs dynamic fan-out, or combines native run operations.
|
|
230
49
|
|
|
231
|
-
|
|
232
|
-
import { WorkflowStateMachine } from "../extensions/workflow/state-machine.ts";
|
|
50
|
+
`extension.ts` automatically registers every entry in `WORKFLOW_MODES`; no new command plumbing is needed. The mode owns task wording and execution shape. The extension owns only argument validation, async launch, agent scope, and mission creation.
|
|
233
51
|
|
|
234
|
-
|
|
235
|
-
const deps = {
|
|
236
|
-
async artifactExists(phase, dir) {
|
|
237
|
-
const file = config.phaseArtifacts[phase];
|
|
238
|
-
return file ? files.has(`${dir}/${file}`) : true;
|
|
239
|
-
},
|
|
240
|
-
async fileExists(path) { return files.has(path); },
|
|
241
|
-
async mkdirArtifactDir() {},
|
|
242
|
-
artifactPathFor: (goal) => `/fake/${goal}`,
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const sm = new WorkflowStateMachine(config);
|
|
246
|
-
const eff = await sm.start("build X", deps);
|
|
247
|
-
expect(eff.kind).toBe("started");
|
|
248
|
-
```
|
|
52
|
+
## Constraints
|
|
249
53
|
|
|
250
|
-
|
|
54
|
+
- `workflowScript`, `chain`, and `tasks` are alternative top-level pi-subagents execution modes. A mode that needs an auto-loop and preceding/following phases should use `workflowScript` and call `runs.run` / `runs.all` within it.
|
|
55
|
+
- Keep one writer at a time. Parallel lanes should be research or review unless they are isolated in worktrees.
|
|
56
|
+
- Workflow progress ledgers are under `.pi-subagents/progress/` and are local runtime artifacts, not durable workflow state.
|
|
57
|
+
- The outer mission and pi-subagents run artifacts are the recovery record.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selesai/code",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
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": {
|