@selesai/code 0.5.10 → 0.5.12
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.
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type ExtensionAPI,
|
|
12
12
|
withFileMutationQueue,
|
|
13
13
|
} from "@selesai/code";
|
|
14
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
14
15
|
import { load } from "cheerio";
|
|
15
16
|
import { Type } from "typebox";
|
|
16
17
|
|
|
@@ -165,6 +166,11 @@ export default function grepAppExtension(pi: ExtensionAPI): void {
|
|
|
165
166
|
details: { total: response.hits.total, page, resultCount: response.hits.hits.length, fullOutputPath: output.fullOutputPath },
|
|
166
167
|
};
|
|
167
168
|
},
|
|
169
|
+
renderResult(result, _options, theme, context) {
|
|
170
|
+
if (context.isError) return new Text(theme.fg("error", "✗ grep.app search failed"), 0, 0);
|
|
171
|
+
const details = result.details as { total?: number; page?: number; resultCount?: number } | undefined;
|
|
172
|
+
return new Text(theme.fg("success", `✓ grep.app: ${details?.resultCount ?? 0} results · page ${details?.page ?? 1} · ${details?.total ?? 0} total matches`), 0, 0);
|
|
173
|
+
},
|
|
168
174
|
});
|
|
169
175
|
|
|
170
176
|
pi.registerTool({
|
|
@@ -196,5 +202,10 @@ export default function grepAppExtension(pi: ExtensionAPI): void {
|
|
|
196
202
|
details: { repo: params.repo, path: params.path, ref: params.ref ?? "HEAD", startLine, endLine, totalLines: allLines.length, fullOutputPath: output.fullOutputPath },
|
|
197
203
|
};
|
|
198
204
|
},
|
|
205
|
+
renderResult(result, _options, theme, context) {
|
|
206
|
+
if (context.isError) return new Text(theme.fg("error", "✗ GitHub file fetch failed"), 0, 0);
|
|
207
|
+
const details = result.details as { repo?: string; path?: string; startLine?: number; endLine?: number } | undefined;
|
|
208
|
+
return new Text(theme.fg("success", `✓ GitHub: ${details?.repo ?? "?"}/${details?.path ?? "?"} · lines ${details?.startLine ?? 1}-${details?.endLine ?? "?"}`), 0, 0);
|
|
209
|
+
},
|
|
199
210
|
});
|
|
200
211
|
}
|
|
@@ -226,8 +226,38 @@ async function resumeController(
|
|
|
226
226
|
try {
|
|
227
227
|
await persistAfter(controller, before);
|
|
228
228
|
const reconcileBefore = checkpoint(controller);
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
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
|
|
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
|
|
523
|
-
//
|
|
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 () =>
|
|
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 {
|
|
7
|
+
import {
|
|
8
|
+
handoffValidator,
|
|
9
|
+
loopCompleteValidator,
|
|
10
|
+
planValidator,
|
|
11
|
+
} from "../validators.ts";
|
|
8
12
|
|
|
9
|
-
// ponytail: task workflow is
|
|
10
|
-
//
|
|
11
|
-
|
|
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
|
|
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
|
-
//
|
|
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:
|
|
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` —
|
|
123
|
+
### `task` — plan → codebase exploration → handoff → build/review loop
|
|
124
124
|
|
|
125
|
-
|
|
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
|
-
-
|
|
133
|
-
- No grilling, research,
|
|
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`
|
|
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
|
|
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
|
|
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
|
|