@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 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 TUI for decisions instead of forcing every clarification through plain text. It supports:
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 = await ctx.ui.custom<QuestionToolResult>((tui, theme, keybindings, done) => {
565
- if (signal) signal.addEventListener("abort", () => done({ status: "cancelled", reason: "user" }), { once: true });
566
- return new BatchQuestionComponent(prepared.questions, tui, theme, keybindings, done);
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("rejects non-TUI modes and invalid params", async () => {
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
- await expect(tool.execute("call", { questions: [{ type: "select", question: "Q", options: [{ value: "x", label: "X" }] }] }, undefined, undefined, {
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
- } as any)).rejects.toThrow("Question requires terminal TUI mode.");
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 shell. Registers the four /workflow-* commands and launches
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 { buildLoopScript, buildPrototypeScript, buildQuicktypeScript, buildTaskScript } from "./modes.ts";
5
+ import { WORKFLOW_MODES, type WorkflowMode } from "./modes.ts";
7
6
 
8
7
  export default function workflowModesExtension(pi: ExtensionAPI): void {
9
- const register = (name: string, description: string, build: (goal: string) => string) =>
10
- pi.registerCommand(name, {
11
- description,
12
- handler: async (args, ctx) => {
13
- const goal = args.trim();
14
- if (!goal) {
15
- ctx.ui.notify(`${description}\nUsage: /${name} <goal>`, "info");
16
- return;
17
- }
18
- launchSlashSubagent(pi, ctx, { workflowScript: build(goal), async: true, agentScope: "both", mission: { title: goal } });
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
- register("workflow-task", "Run the task workflow (plan → reuse → handoff → auto build→review→fix loop) as a scripted workflow.", buildTaskScript);
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: four workflow modes as pi-subagents workflowScript builders.
2
- // One-shot-and-sleep: phases run as runs.run steps and the build → review → fix
3
- // round auto-repeats until the reviewer reports clean. A workflowScript has no
4
- // checkpoints, so there are no human gates.
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
- return JSON.stringify(value);
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
- if (/WORKFLOW_REVIEW_STATUS\s*:\s*clean/i.test(review.output)) {
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
- return String.raw`const goal = ${js(goal)};
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
- return String.raw`const goal = ${js(goal)};
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
- return String.raw`const goal = ${js(goal)};
68
- const research = await runs.run('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.' });
69
- const plan = await runs.run('plan', { agent: 'architect', task: 'Produce a concrete build plan from the research findings.\n\nResearch:\n' + research.output + '\n\nRequest:\n' + goal + '\n\nReturn inline.' });
70
- const reuse = await runs.run('reuse', { agent: 'explorer', task: 'Explore the codebase for reusable patterns relevant to: ' + plan.output + '. Return inline.' });
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
- return String.raw`const goal = ${js(goal)};
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;
@@ -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 ships a workflow engine under `src/extensions/workflow/`. It powers the built-in `prototype`, `quicktype`, `task`, and `loop` workflows and is designed so you can add a new workflow mode as a thin config file — no engine changes.
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
- ## How it fits together
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
- ```typescript
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
- ### `prototype` and `quicktype` — full vs. quicker prototype
115
-
116
- `quicktype` has the same prototype flow except for research: it goes from grilling directly to planning. Use `prototype` when external research is needed; use `quicktype` when it is not.
117
-
118
- - `prototype`: `grilling → research → plan → reuse → handoff → loop → audit`
119
- - `quicktype`: `grilling → plan → reuse → handoff → loop → audit`
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
- | Field | Type | Description |
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
- ### Adapter options
27
+ ## Extending workflows
161
28
 
162
- The second argument to `createWorkflowExtension`:
29
+ The extension seam is `src/extensions/workflow/modes.ts`.
163
30
 
164
- | Field | Description |
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
- ## Durable runs and explicit resume
170
-
171
- 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.
172
-
173
- 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:
174
-
175
- - Workflow initiation is user-only: `/workflow-prototype <goal>`, `/workflow-quicktype <goal>`, `/workflow-task <goal>`, or `/workflow-loop <goal>`
176
- - `/workflow-prototype resume <id-or-artifact-dir-or-workflow.json>` / `/workflow-quicktype resume ...` / `/workflow-task resume ...` / `/workflow-loop resume ...`
177
- - `/workflow-prototype resume`, `/workflow-quicktype resume`, `/workflow-task resume`, or `/workflow-loop resume` lists active runs (and offers a UI picker when available).
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
- `shouldSkip` is a boolean predicate. When it returns `true`, the engine skips to the next phase in `phases` — the mode owns the transition graph, not the adapter.
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
- The state machine is tested directly with in-memory stubs — no filesystem, no pi mock, no events:
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
- ```typescript
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
- const files = new Set<string>();
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
- See `src/__tests__/state-machine.test.ts` for the full set of transition, skip, terminal, rehydrate, and validation tests.
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",
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": {