@selesai/code 0.5.9 → 0.5.11

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.
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildSearchUrl, formatSearchResponse } from "./index.ts";
3
+
4
+ describe("grep-app extension", () => {
5
+ it("maps search filters to grep.app parameters", () => {
6
+ const url = buildSearchUrl({
7
+ query: "useEffect cleanup",
8
+ page: 2,
9
+ caseSensitive: true,
10
+ useRegex: true,
11
+ repo: "facebook/react",
12
+ path: "packages/",
13
+ language: "TypeScript",
14
+ });
15
+
16
+ expect(Object.fromEntries(url.searchParams)).toEqual({
17
+ q: "useEffect cleanup",
18
+ page: "2",
19
+ case: "true",
20
+ regexp: "true",
21
+ "f.repo.pattern": "facebook/react",
22
+ "f.path.pattern": "packages/",
23
+ "f.lang": "TypeScript",
24
+ });
25
+ });
26
+
27
+ it("turns grep.app HTML snippets into readable numbered source", () => {
28
+ const output = formatSearchResponse(
29
+ {
30
+ hits: {
31
+ total: 42,
32
+ hits: [
33
+ {
34
+ repo: "owner/repo",
35
+ branch: "main",
36
+ path: "src/a file.ts",
37
+ content: {
38
+ snippet: '<table><tr data-line="7"><td><pre>const x = &lt;mark&gt;;</pre></td></tr></table>',
39
+ },
40
+ },
41
+ ],
42
+ },
43
+ },
44
+ 3,
45
+ );
46
+
47
+ expect(output).toContain("42 total matches; page 3");
48
+ expect(output).toContain("L7: const x = <mark>;");
49
+ expect(output).toContain("https://github.com/owner/repo/blob/main/src/a%20file.ts#L7");
50
+ });
51
+ });
@@ -0,0 +1,200 @@
1
+ /** Native Selesai extension inspired by ai-tools-all/grep_app_mcp (ISC). */
2
+
3
+ import { mkdtemp, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import {
7
+ DEFAULT_MAX_BYTES,
8
+ DEFAULT_MAX_LINES,
9
+ formatSize,
10
+ truncateHead,
11
+ type ExtensionAPI,
12
+ withFileMutationQueue,
13
+ } from "@selesai/code";
14
+ import { load } from "cheerio";
15
+ import { Type } from "typebox";
16
+
17
+ const API_URL = "https://grep.app/api/search";
18
+ const RAW_GITHUB_URL = "https://raw.githubusercontent.com";
19
+
20
+ interface GrepAppHit {
21
+ repo: string;
22
+ branch: string;
23
+ path: string;
24
+ content: { snippet: string };
25
+ total_matches?: string;
26
+ }
27
+
28
+ interface GrepAppResponse {
29
+ hits: { total: number; hits: GrepAppHit[] };
30
+ }
31
+
32
+ interface SearchParams {
33
+ query: string;
34
+ page?: number;
35
+ caseSensitive?: boolean;
36
+ useRegex?: boolean;
37
+ wholeWords?: boolean;
38
+ repo?: string;
39
+ path?: string;
40
+ language?: string;
41
+ }
42
+
43
+ export function buildSearchUrl(params: SearchParams): URL {
44
+ const url = new URL(API_URL);
45
+ url.searchParams.set("q", params.query);
46
+ url.searchParams.set("page", String(params.page ?? 1));
47
+ if (params.caseSensitive) url.searchParams.set("case", "true");
48
+ if (params.useRegex) url.searchParams.set("regexp", "true");
49
+ if (params.wholeWords) url.searchParams.set("words", "true");
50
+ if (params.repo) url.searchParams.set("f.repo.pattern", params.repo);
51
+ if (params.path) url.searchParams.set("f.path.pattern", params.path);
52
+ if (params.language) url.searchParams.set("f.lang", params.language);
53
+ return url;
54
+ }
55
+
56
+ function snippetLines(snippet: string): Array<{ line: string; text: string }> {
57
+ const $ = load(snippet);
58
+ return $("tr")
59
+ .toArray()
60
+ .map((row) => ({
61
+ line: $(row).attr("data-line") ?? "?",
62
+ text: $(row).find("pre").text().replace(/\s+$/, ""),
63
+ }));
64
+ }
65
+
66
+ function encodePath(value: string): string {
67
+ return value
68
+ .split("/")
69
+ .filter(Boolean)
70
+ .map(encodeURIComponent)
71
+ .join("/");
72
+ }
73
+
74
+ export function formatSearchResponse(response: GrepAppResponse, page: number): string {
75
+ if (response.hits.hits.length === 0) return "No matches found.";
76
+
77
+ const results = response.hits.hits.map((hit, index) => {
78
+ const lines = snippetLines(hit.content.snippet);
79
+ const firstLine = lines.find((line) => line.line !== "?")?.line;
80
+ const url = `https://github.com/${hit.repo}/blob/${encodePath(hit.branch)}/${encodePath(hit.path)}${firstLine ? `#L${firstLine}` : ""}`;
81
+ const body = lines.map(({ line, text }) => ` L${line}: ${text}`).join("\n");
82
+ return `${index + 1}. ${hit.repo}:${hit.path} (${hit.branch})\n${body}\n ${url}`;
83
+ });
84
+
85
+ return `grep.app: ${response.hits.total} total matches; page ${page}\n\n${results.join("\n\n")}`;
86
+ }
87
+
88
+ async function fetchWithBrowser(url: URL | string, signal: AbortSignal | undefined): Promise<string> {
89
+ const { chromium } = await import("playwright");
90
+ const browser = await chromium.launch({ headless: true });
91
+ const abort = () => void browser.close();
92
+ signal?.addEventListener("abort", abort, { once: true });
93
+ try {
94
+ const page = await browser.newPage();
95
+ await page.goto(String(url), { waitUntil: "networkidle", timeout: 30_000 });
96
+ await page.waitForFunction(() => document.body.innerText.trimStart().startsWith("{"), undefined, { timeout: 15_000 });
97
+ return await page.locator("body").innerText();
98
+ } catch (error) {
99
+ if (signal?.aborted) throw signal.reason ?? new Error("Cancelled");
100
+ throw error;
101
+ } finally {
102
+ signal?.removeEventListener("abort", abort);
103
+ await browser.close();
104
+ }
105
+ }
106
+
107
+ async function fetchText(url: URL | string, signal: AbortSignal | undefined, browserFallback = false): Promise<string> {
108
+ const response = await fetch(url, {
109
+ signal,
110
+ headers: { Accept: "application/json, text/plain;q=0.9", "User-Agent": "selesai-grep-app-extension" },
111
+ });
112
+ if (browserFallback && response.status === 429 && response.headers.get("x-vercel-mitigated") === "challenge") {
113
+ return fetchWithBrowser(url, signal);
114
+ }
115
+ if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
116
+ return response.text();
117
+ }
118
+
119
+ async function truncateOutput(output: string): Promise<{ text: string; fullOutputPath?: string }> {
120
+ const truncated = truncateHead(output, { maxBytes: DEFAULT_MAX_BYTES, maxLines: DEFAULT_MAX_LINES });
121
+ if (!truncated.truncated) return { text: output };
122
+
123
+ const directory = await mkdtemp(join(tmpdir(), "selesai-grep-app-"));
124
+ const fullOutputPath = join(directory, "output.txt");
125
+ await withFileMutationQueue(fullOutputPath, () => writeFile(fullOutputPath, output, "utf8"));
126
+ return {
127
+ text: `${truncated.content}\n\n[Output truncated to ${truncated.outputLines} lines / ${formatSize(truncated.outputBytes)}. Full output: ${fullOutputPath}]`,
128
+ fullOutputPath,
129
+ };
130
+ }
131
+
132
+ export default function grepAppExtension(pi: ExtensionAPI): void {
133
+ pi.registerTool({
134
+ name: "grep_app_search",
135
+ label: "grep.app Search",
136
+ description: "Search public GitHub code through grep.app. Returns one page (up to 10 files); use page to continue.",
137
+ promptSnippet: "Search public GitHub code through grep.app",
138
+ promptGuidelines: ["Use grep_app_search to find real public-code implementations and usage examples across GitHub."],
139
+ parameters: Type.Object({
140
+ query: Type.String({ minLength: 1, description: "Code or pattern to search for." }),
141
+ page: Type.Optional(Type.Integer({ minimum: 1, description: "Result page; defaults to 1." })),
142
+ caseSensitive: Type.Optional(Type.Boolean({ description: "Match case." })),
143
+ useRegex: Type.Optional(Type.Boolean({ description: "Treat query as a regular expression." })),
144
+ wholeWords: Type.Optional(Type.Boolean({ description: "Match whole words. Cannot be combined with useRegex." })),
145
+ repo: Type.Optional(Type.String({ description: "Repository filter, for example facebook/react." })),
146
+ path: Type.Optional(Type.String({ description: "File path filter, for example src/components/." })),
147
+ language: Type.Optional(Type.String({ description: "Language filter, for example TypeScript." })),
148
+ }),
149
+ async execute(_toolCallId, params, signal) {
150
+ if (params.useRegex && params.wholeWords) throw new Error("useRegex and wholeWords cannot both be true.");
151
+
152
+ const page = params.page ?? 1;
153
+ const raw = await fetchText(buildSearchUrl(params), signal, true);
154
+ let response: GrepAppResponse;
155
+ try {
156
+ response = JSON.parse(raw) as GrepAppResponse;
157
+ } catch {
158
+ throw new Error("grep.app returned invalid JSON.");
159
+ }
160
+ if (!response.hits || !Array.isArray(response.hits.hits)) throw new Error("grep.app returned an unexpected response.");
161
+
162
+ const output = await truncateOutput(formatSearchResponse(response, page));
163
+ return {
164
+ content: [{ type: "text", text: output.text }],
165
+ details: { total: response.hits.total, page, resultCount: response.hits.hits.length, fullOutputPath: output.fullOutputPath },
166
+ };
167
+ },
168
+ });
169
+
170
+ pi.registerTool({
171
+ name: "grep_app_fetch",
172
+ label: "GitHub File",
173
+ description: "Fetch a public GitHub file found via grep.app. Supports line ranges; output is truncated to 50KB or 2000 lines.",
174
+ promptSnippet: "Fetch a public GitHub file found via grep.app",
175
+ promptGuidelines: ["Use grep_app_fetch after grep_app_search when the full source context is needed."],
176
+ parameters: Type.Object({
177
+ repo: Type.String({ pattern: "^[^/\\s]+/[^/\\s]+$", description: "owner/repository." }),
178
+ path: Type.String({ minLength: 1, description: "Path inside the repository." }),
179
+ ref: Type.Optional(Type.String({ minLength: 1, description: "Branch, tag, or commit; defaults to HEAD." })),
180
+ startLine: Type.Optional(Type.Integer({ minimum: 1, description: "First line; defaults to 1." })),
181
+ endLine: Type.Optional(Type.Integer({ minimum: 1, description: "Last line; defaults to end of file." })),
182
+ }),
183
+ async execute(_toolCallId, params, signal) {
184
+ const startLine = params.startLine ?? 1;
185
+ if (params.endLine !== undefined && params.endLine < startLine) throw new Error("endLine must be greater than or equal to startLine.");
186
+
187
+ const rawUrl = `${RAW_GITHUB_URL}/${encodePath(params.repo)}/${encodePath(params.ref ?? "HEAD")}/${encodePath(params.path)}`;
188
+ const source = await fetchText(rawUrl, signal);
189
+ const allLines = source.split("\n");
190
+ const endLine = Math.min(params.endLine ?? allLines.length, allLines.length);
191
+ const selected = allLines.slice(startLine - 1, endLine).map((line, index) => `${startLine + index}: ${line}`).join("\n");
192
+ const output = await truncateOutput(selected);
193
+
194
+ return {
195
+ content: [{ type: "text", text: output.text || "(empty file or range)" }],
196
+ details: { repo: params.repo, path: params.path, ref: params.ref ?? "HEAD", startLine, endLine, totalLines: allLines.length, fullOutputPath: output.fullOutputPath },
197
+ };
198
+ },
199
+ });
200
+ }
@@ -9,6 +9,7 @@
9
9
  "./context-compaction-reminder.ts",
10
10
  "./tool-error-autofix.ts",
11
11
  "./question",
12
+ "./grep-app",
12
13
  "./handoff-new.ts",
13
14
  "./rtk.ts",
14
15
  "./tokenin-onboarding.ts",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: researcher
3
- description: Autonomous web researcher that produces a focused sourced brief
4
- tools: read, web_explore
3
+ description: Autonomous code-first researcher that produces a focused sourced brief
4
+ tools: read, grep_app_search, grep_app_fetch, web_explore
5
5
  thinking: medium
6
6
  systemPromptMode: replace
7
7
  inheritProjectContext: false
@@ -10,9 +10,11 @@ defaultContext: fresh
10
10
  skill: ponytail, caveman
11
11
  ---
12
12
 
13
- You are a web research subagent. Answer the supplied question with a concise, well-sourced brief. Do not edit project files, write output files, or launch subagents.
13
+ You are a code-first research subagent. Answer the supplied question with a concise, well-sourced brief. Do not edit project files, write output files, or launch subagents.
14
14
 
15
- Use `web_explore` for focused search/fetch/source ranking. Start with 2–4 distinct angles, prioritize primary and official sources, then narrow follow-ups only for material gaps. Do not fetch URLs through shell commands.
15
+ Start with `grep_app_search` for public-code evidence and use `grep_app_fetch` only for the most relevant source files. Search 2–4 distinct angles, prefer original implementations and repository-owned examples, then narrow follow-ups only for material gaps.
16
+
17
+ Treat `web_explore` as a last resort: use it only when grep.app cannot answer the question, the task requires non-code sources or official documentation, or grep.app fails. If `web_explore` is unavailable, report the gap instead of fetching URLs through shell commands.
16
18
 
17
19
  Output:
18
20
 
@@ -226,8 +226,38 @@ async function resumeController(
226
226
  try {
227
227
  await persistAfter(controller, before);
228
228
  const reconcileBefore = checkpoint(controller);
229
- const reconciled = await sm.onArtifactMaybe(controller.deps);
230
- await persistAfter(controller, reconcileBefore);
229
+ let reconciled: WorkflowEffect = { kind: "noOp" };
230
+ try {
231
+ reconciled = await sm.onArtifactMaybe(controller.deps);
232
+ await persistRun(controller);
233
+ // ponytail: when a resumed run has multiple already-written parent-owned
234
+ // artifacts, one onArtifactMaybe only advances one phase. Keep reconciling
235
+ // until we either catch up to the durable file state or hit a blocked/
236
+ // terminal effect, so resume lands on the furthest valid phase.
237
+ while (sm.snapshot.active && sm.snapshot.autoArmed) {
238
+ const further = await sm.onArtifactMaybe(controller.deps);
239
+ if (
240
+ further.kind === "noOp" ||
241
+ further.kind === "blocked" ||
242
+ further.kind === "terminalNeedsArtifacts"
243
+ ) break;
244
+ reconciled = further;
245
+ await persistRun(controller);
246
+ if (further.kind === "terminalReady") break;
247
+ }
248
+ } catch (error) {
249
+ // Atomic rollback: on any failure during reconciliation, restore both
250
+ // in-memory state and durable state to the checkpoint before the whole
251
+ // reconcile sequence. Do not leave the controller at an intermediate
252
+ // phase while the run file has already advanced further.
253
+ restoreCheckpoint(controller, reconcileBefore);
254
+ try {
255
+ await persistRun(controller);
256
+ } catch {
257
+ // Best-effort rollback failed; still report the original error.
258
+ }
259
+ throw error;
260
+ }
231
261
  const current = sm.continueCurrent();
232
262
  const terminal = config.phases[config.phases.length - 1];
233
263
  const terminalReady = reconciled.kind === "terminalReady" || (sm.snapshot.phase === terminal && !sm.snapshot.autoArmed);
@@ -304,7 +334,7 @@ function makeDeps(pi: ExtensionAPI, config: WorkflowConfig): WorkflowDeps {
304
334
 
305
335
  // ponytail: the git-based skip predicate shared by both modes today.
306
336
  // A mode that wants a different skip rule supplies its own SkipRule.
307
- async function isEmptyProject(pi: ExtensionAPI): Promise<boolean> {
337
+ async function hasNoGitHistory(pi: ExtensionAPI): Promise<boolean> {
308
338
  try {
309
339
  const result = await pi.exec("git", ["log", "--oneline", "-1"]);
310
340
  return result.code !== 0 || !result.stdout.trim();
@@ -482,7 +512,8 @@ function registerSharedArtifactWriter(pi: ExtensionAPI): void {
482
512
  };
483
513
  }
484
514
  // Most workflows pause at a user-controlled artifact boundary. Task's
485
- // plan → loop transition queues the builder/review prompt immediately.
515
+ // plan → reuse → handoff → loop transitions queue the next phase prompt
516
+ // immediately when continueAfterArtifact is true.
486
517
  const queueNextPhase = controller.config.continueAfterArtifact === true && eff.kind === "advanced";
487
518
  applyControllerEffect(controller, ctx, eff, { queuePrompt: queueNextPhase });
488
519
  if (eff.kind === "blocked" && eff.reason) {
@@ -519,13 +550,13 @@ export function createWorkflowExtension(
519
550
  return function workflowExtension(pi: ExtensionAPI): void {
520
551
  replaceControllerFor(pi, config.mode);
521
552
  const deps = makeDeps(pi, config);
522
- // ponytail: default skip rule — reuse is skipped on empty projects (no git
523
- // commits). Shared by every mode today; a mode that wants a different rule
524
- // supplies its own skipRules in config and we respect them as-is.
553
+ // ponytail: default skip rule — reuse is skipped when the project has no
554
+ // git history. Shared by every mode today; a mode that wants a different
555
+ // rule supplies its own skipRules in config and we respect them as-is.
525
556
  const skipRules = config.skipRules ?? [
526
557
  {
527
558
  phase: "reuse",
528
- shouldSkip: async () => isEmptyProject(pi),
559
+ shouldSkip: async () => hasNoGitHistory(pi),
529
560
  },
530
561
  ];
531
562
  const sm = new WorkflowStateMachine({ ...config, skipRules });
@@ -4,11 +4,17 @@ import type {
4
4
  WorkflowConfig,
5
5
  WorkflowModeRegistration,
6
6
  } from "../state-machine.ts";
7
- import { loopCompleteValidator, planValidator } from "../validators.ts";
7
+ import {
8
+ handoffValidator,
9
+ loopCompleteValidator,
10
+ planValidator,
11
+ } from "../validators.ts";
8
12
 
9
- // ponytail: task workflow is only plan → build↔review; the loop's clean marker
10
- // makes it terminal-ready, and the explicit end tool is the only completion.
11
- const phases: Phase[] = ["plan", "loop"];
13
+ // ponytail: task workflow is now plan → reuse → handoff → loop. Each parent-
14
+ // owned artifact (plan.md, reuse.md, handoff.md) advances automatically, and
15
+ // the loop's clean marker makes the workflow terminal-ready. The explicit end
16
+ // tool is the only completion.
17
+ const phases: Phase[] = ["plan", "reuse", "handoff", "loop"];
12
18
 
13
19
  const prompts: Partial<Record<Phase, (ctx: PromptContext) => string>> = {
14
20
  plan: ({ artifactDir, userPrompt }) =>
@@ -24,12 +30,38 @@ Call the subagent tool with { agent: "architect", task: "...", output: false } (
24
30
  Inspect that result, verify it ends with exactly one machine-readable line on its own:
25
31
  WORKFLOW_PLAN_STATUS: ready
26
32
  Then immediately call write_workflow_artifact with the complete validated plan as content. Only that parent tool call writes ${artifactDir}/plan.md and advances the workflow.`,
33
+ reuse: ({ artifactDir }) =>
34
+ `You are in the REUSE phase of a TASK workflow (optional).
35
+
36
+ The adapter automatically skips this phase when the project has no git history, so codebase exploration is only useful when commits already exist. Decide whether exploration is still useful; skip if the task creates something wholly new or there is clearly nothing to reuse.
37
+
38
+ If unsure, ask the user one focused question: "Should I explore the existing codebase for reusable patterns before implementing?" Then follow their answer.
39
+
40
+ If YES (or user confirms): call the subagent tool with { agent: "explorer", task: "...", output: false } (do NOT pass a model parameter). Craft the task from ${artifactDir}/plan.md pointing it at relevant areas, patterns, and dependencies. It must return the reuse findings inline; do not tell it to write any artifact. Immediately call write_workflow_artifact with those findings as content for ${artifactDir}/reuse.md.
41
+
42
+ If NO: call write_workflow_artifact with a brief skip note explaining why.
43
+
44
+ The workflow advances once ${artifactDir}/reuse.md exists.`,
45
+ handoff: ({ artifactDir }) =>
46
+ `You are in the HANDOFF phase of a TASK workflow.
47
+
48
+ Compile a self-contained handoff document so loop-phase sub-agents understand full project context without re-grilling.
49
+
50
+ Draw from all prior phases:
51
+ - ${artifactDir}/plan.md
52
+ - ${artifactDir}/reuse.md
53
+
54
+ Call the subagent tool with { agent: "recapper", task: "...", output: false } (do NOT pass a model parameter). Craft the task pointing the recapper at both artifact files and telling it what the task is about. It must return the complete handoff inline; do not tell it to write any artifact.
55
+
56
+ Inspect that result, verify it ends with exactly one machine-readable line on its own:
57
+ WORKFLOW_HANDOFF_STATUS: ready
58
+ Then immediately call write_workflow_artifact with the complete validated handoff as content. Only that parent tool call writes ${artifactDir}/handoff.md and advances the workflow.`,
27
59
  loop: ({ artifactDir, loopMaxIterations }) =>
28
60
  `You are in the LOOP (orchestration) phase of a TASK workflow. The workflow ENGINE owns the implement→review loop — you do NOT track iterations or decide when the loop is clean.
29
61
 
30
- Use read to inspect ${artifactDir}/plan.md. Using that context, GENERATE YOUR OWN delegation prompt:
62
+ Use read to inspect ${artifactDir}/plan.md, ${artifactDir}/reuse.md, and ${artifactDir}/handoff.md. Using that context, GENERATE YOUR OWN delegation prompt:
31
63
 
32
- Call the subagent tool with { agent: "builder", task: "...", output: false } (do NOT pass a model parameter). Give it the plan context, tailored to this task. Instruct it to implement every task in plan.md in order. All code changes go in the workspace, never in ${artifactDir}. The builder must return its completion summary inline.
64
+ Call the subagent tool with { agent: "builder", task: "...", output: false } (do NOT pass a model parameter). Give it plan + reuse + handoff context, tailored to this task. Instruct it to implement every task in plan.md in order. All code changes go in the workspace, never in ${artifactDir}. The builder must return its completion summary inline.
33
65
 
34
66
  After the builder returns, call the subagent tool with { agent: "commentator", task: "...", output: false }. Craft the review task YOURSELF. The commentator must return its review inline. Each commentator review MUST end with exactly one machine-readable line:
35
67
  WORKFLOW_REVIEW_STATUS: clean
@@ -44,19 +76,21 @@ const config: WorkflowConfig = {
44
76
  phases,
45
77
  phaseArtifacts: {
46
78
  plan: "plan.md",
79
+ reuse: "reuse.md",
80
+ handoff: "handoff.md",
47
81
  loop: "loop-complete.md",
48
82
  },
49
83
  prompts,
50
84
  skipRules: [],
51
85
  artifactValidators: {
52
86
  plan: planValidator,
87
+ handoff: handoffValidator,
53
88
  loop: loopCompleteValidator,
54
89
  },
55
90
  closeValidators: { "loop-complete.md": loopCompleteValidator },
56
91
  closeArtifacts: ["loop-complete.md"],
57
92
  loopMaxIterations: 3,
58
- // Task is plan → build. Continue directly into the loop when architect
59
- // finishes, rather than requiring a second /workflow-task interaction.
93
+ // Each parent-owned artifact automatically queues the next phase prompt.
60
94
  continueAfterArtifact: true,
61
95
  statusKey: "task",
62
96
  entryType: "task-phase",
@@ -66,7 +100,8 @@ const config: WorkflowConfig = {
66
100
  export const taskMode: WorkflowModeRegistration = {
67
101
  config,
68
102
  commandName: "workflow-task",
69
- commandDescription: "Run the task workflow (plan → build↔review loop)",
103
+ commandDescription:
104
+ "Run the task workflow (plan → reuse → handoff → build↔review loop)",
70
105
  toolNames: {
71
106
  start: "start_task_workflow",
72
107
  resume: "resume_task_workflow",
package/docs/workflows.md CHANGED
@@ -120,17 +120,18 @@ That's it. The loader picks it up at boot (`package.json` loads only `./extensio
120
120
 
121
121
  ## Built-in modes
122
122
 
123
- ### `task` — rapid plan + build/review loop
123
+ ### `task` — plan → codebase exploration → handoff → build/review loop
124
124
 
125
- The simplest workflow: an architect subagent produces a validated `plan.md`, then a builder↔commentator review loop runs (max 3 blocking rounds). A clean review makes the workflow terminal-ready; `end_task_workflow` completes it.
125
+ Task now follows the same phase shape as the other modes, minus grilling/research/audit: an architect subagent produces a validated `plan.md`, an optional explorer subagent produces `reuse.md`, a recapper subagent produces a validated `handoff.md`, and then a builder↔commentator review loop runs (max 3 blocking rounds). A clean review makes the workflow terminal-ready; `end_task_workflow` completes it.
126
126
 
127
- Lifecycle: `plan → loop (build ↔ review) → terminal-ready → end_task_workflow`
127
+ Lifecycle: `plan → reuse → handoff → loop (build ↔ review) → terminal-ready → end_task_workflow`
128
128
 
129
129
  - `/workflow-task <goal>` — start a new run
130
130
  - `/workflow-task resume` — list and resume active runs
131
131
  - `/workflow-task help` — show the lifecycle
132
- - Its ready plan automatically queues the builder↔review loop
133
- - No grilling, research, reuse, or handoff phases
132
+ - Valid `plan.md`, `reuse.md`, and `handoff.md` each automatically queue the next phase prompt (the workflow does not pause at those boundaries)
133
+ - No grilling, research, or audit phases
134
+ - `reuse.md` is optional; it is skipped automatically when the project has no git history
134
135
 
135
136
  ## Config reference
136
137
 
@@ -141,11 +142,11 @@ Lifecycle: `plan → loop (build ↔ review) → terminal-ready → end_task_wor
141
142
  | `phaseArtifacts` | `Partial<Record<Phase, string>>` | The artifact file each phase must produce before advancing. Omit a phase to skip its gate. |
142
143
  | `prompts` | `Partial<Record<Phase, (ctx) => string>>` | Prompt generator per phase. `ctx = { artifactDir, userPrompt }`. |
143
144
  | `closeArtifacts` | `string[]` | Files that must exist before `end()` succeeds. Config-owned, no built-in default. |
144
- | `skipRules?` | `{ phase, shouldSkip }[]` | Optional per-phase skip rules. `shouldSkip` is a boolean predicate; when true the engine skips to the next phase. Omit to use the adapter's default (skip `reuse` on empty projects). |
145
+ | `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). |
145
146
  | `statusKey` | `string` | Footer status key. |
146
147
  | `entryType` | `string` | Session-history custom-type. It stores a pointer only; `workflow.json` is canonical. |
147
148
  | `footerLabel` | `string` | Label shown in the footer (`● label · step/total phase`). |
148
- | `continueAfterArtifact?` | `boolean` | Queue the next phase prompt after the parent writes a valid artifact. `task` enables this for plan → build. |
149
+ | `continueAfterArtifact?` | `boolean` | Queue the next phase prompt after the parent writes a valid artifact. `task` enables this at every parent-owned artifact boundary (plan, reuse, handoff) so the workflow flows automatically into the loop. |
149
150
 
150
151
  ### Adapter options
151
152
 
@@ -169,7 +170,7 @@ Runs are **never** auto-resumed on session start. At most one run can be attache
169
170
  - `/workflow-prototype resume`, `/workflow-quick resume`, or `/workflow-task resume` lists active runs (and offers a UI picker when available).
170
171
  - `/workflow-prototype help`, `/workflow-quick help`, or `/workflow-task help` shows the start, resume, continue, and explicit-completion lifecycle.
171
172
 
172
- Resume validates the selected file is under the artifacts base, belongs to that mode, is active, and matches its containing directory. It reconciles the current expected artifact once before emitting the current prompt, covering a crash after `write_workflow_artifact` writes the file but before the phase-state write. Artifact writes do not inject the next phase prompt or launch the next subagent; they terminate the parent turn and wait for the user to continue. A mode can opt out of that pause after a parent artifact write; `task` does so after `plan.md` so the build loop starts immediately. Corrupt records are skipped during discovery.
173
+ Resume validates the selected file is under the artifacts base, belongs to that mode, is active, and matches its containing directory. It reconciles the current expected artifact once before emitting the current prompt, covering a crash after `write_workflow_artifact` writes the file but before the phase-state write. Artifact writes do not inject the next phase prompt or launch the next subagent; they terminate the parent turn and wait for the user to continue. A mode can opt out of that pause after a parent artifact write; `task` does so at every parent-owned artifact boundary (`plan.md`, `reuse.md`, `handoff.md`) so the build loop starts immediately after a valid handoff. Corrupt records are skipped during discovery.
173
174
 
174
175
  A valid terminal artifact makes a workflow **terminal-ready**; it does not complete the run. Call the mode-specific `end_*_workflow` tool to write `status: "completed"`, append the done entry, and terminate. This is the only completion path.
175
176
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selesai/code",
3
- "version": "0.5.9",
3
+ "version": "0.5.11",
4
4
  "description": "Selesai coding agent",
5
5
  "type": "module",
6
6
  "piConfig": {