@henryqw/pi-subagent 4.1.2 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +16 -10
- package/README.md +33 -12
- package/dist/ephemeral.js +158 -55
- package/dist/index.d.ts +10 -5
- package/dist/index.js +18 -27
- package/dist/review-evidence.d.ts +17 -0
- package/dist/review-evidence.js +229 -0
- package/dist/worktree.d.ts +16 -0
- package/dist/worktree.js +49 -31
- package/docs/adr/001-composable-ephemeral-execution.md +11 -5
- package/docs/adr/002-package-owned-delegate-flow-orchestration.md +22 -0
- package/docs/orchestration.md +48 -19
- package/examples/roles/implementer.md +6 -4
- package/examples/roles/reviewer.md +8 -3
- package/examples/roles/scout.md +2 -0
- package/examples/roles/synthesizer.md +2 -0
- package/extensions/delegate-flow.ts +889 -0
- package/extensions/delegation.ts +9 -0
- package/extensions/result-transport.ts +20 -9
- package/extensions/role-tools.ts +11 -6
- package/extensions/subagent.ts +117 -44
- package/package.json +2 -2
- package/skills/pi-subagent-delegated-development/SKILL.md +15 -37
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { isAbsolute, join } from "node:path";
|
|
5
|
+
import { inspectIndexFlags } from "./worktree.js";
|
|
6
|
+
const GIT_TIMEOUT_MS = 30_000;
|
|
7
|
+
export const REVIEW_MAX_PATHS = 1_000;
|
|
8
|
+
export const REVIEW_MAX_PATCH_BYTES = 512 * 1024;
|
|
9
|
+
const STDERR_LIMIT = 200;
|
|
10
|
+
const git = (args, cwd, signal) => new Promise((resolve) => {
|
|
11
|
+
execFile("git", ["--no-pager", ...args], { cwd, signal, timeout: GIT_TIMEOUT_MS }, (error, stdout, stderr) => {
|
|
12
|
+
resolve({
|
|
13
|
+
code: error ? (typeof error.code === "number" ? error.code : -1) : 0,
|
|
14
|
+
stdout: String(stdout),
|
|
15
|
+
stderr: String(stderr).slice(0, STDERR_LIMIT),
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
function failure(args, result) {
|
|
20
|
+
const detail = result.stderr.trim();
|
|
21
|
+
return new Error(`git ${args.join(" ")} failed with exit ${result.code}${detail ? `: ${detail}` : ""}`);
|
|
22
|
+
}
|
|
23
|
+
async function runGit(args, cwd, signal) {
|
|
24
|
+
signal?.throwIfAborted();
|
|
25
|
+
const result = await git(args, cwd, signal);
|
|
26
|
+
signal?.throwIfAborted();
|
|
27
|
+
if (result.code !== 0)
|
|
28
|
+
throw failure(args, result);
|
|
29
|
+
return result.stdout;
|
|
30
|
+
}
|
|
31
|
+
function line(value, field) {
|
|
32
|
+
const result = value.replace(/\r?\n$/, "");
|
|
33
|
+
if (!result || /[\r\n\0]/.test(result))
|
|
34
|
+
throw new Error(`Git returned malformed ${field}.`);
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
function oid(value, field) {
|
|
38
|
+
const result = line(value, field);
|
|
39
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(result))
|
|
40
|
+
throw new Error(`Git returned invalid ${field}.`);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
function requestText(value, field) {
|
|
44
|
+
if (typeof value !== "string" || !value || value.includes("\0"))
|
|
45
|
+
throw new TypeError(`${field} must be non-empty text without NUL bytes.`);
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
async function resolveCommit(reference, cwd, signal) {
|
|
49
|
+
requestText(reference, "review ref");
|
|
50
|
+
return oid(await runGit(["rev-parse", "--verify", "--end-of-options", `${reference}^{commit}`], cwd, signal), `commit for ${reference}`);
|
|
51
|
+
}
|
|
52
|
+
function validatePath(path) {
|
|
53
|
+
if (!path || path.includes("\0") || path.includes("�") || isAbsolute(path) || path.split(/[\\/]/).some((part) => !part || part === "." || part === "..")) {
|
|
54
|
+
throw new Error(`Git returned malformed or unsupported path: ${JSON.stringify(path)}`);
|
|
55
|
+
}
|
|
56
|
+
return path;
|
|
57
|
+
}
|
|
58
|
+
function parsePaths(value) {
|
|
59
|
+
if (!value)
|
|
60
|
+
return [];
|
|
61
|
+
if (!value.endsWith("\0") || value.includes("�"))
|
|
62
|
+
throw new Error("Git returned malformed changed paths.");
|
|
63
|
+
const paths = value.slice(0, -1).split("\0");
|
|
64
|
+
if (paths.length > REVIEW_MAX_PATHS)
|
|
65
|
+
throw new Error(`Review evidence exceeds ${REVIEW_MAX_PATHS} paths; split the review.`);
|
|
66
|
+
return paths.map(validatePath);
|
|
67
|
+
}
|
|
68
|
+
function assertSupportedChanges(value) {
|
|
69
|
+
if (!value)
|
|
70
|
+
return;
|
|
71
|
+
if (!value.endsWith("\0") || value.includes("�"))
|
|
72
|
+
throw new Error("Git returned malformed raw diff.");
|
|
73
|
+
const fields = value.slice(0, -1).split("\0");
|
|
74
|
+
for (let index = 0; index < fields.length;) {
|
|
75
|
+
const header = fields[index++];
|
|
76
|
+
const match = /^:(\d{6}) (\d{6}) [0-9a-f]+ [0-9a-f]+ ([A-Z][0-9]*)$/.exec(header);
|
|
77
|
+
if (!match)
|
|
78
|
+
throw new Error("Git returned malformed raw diff.");
|
|
79
|
+
for (const mode of [match[1], match[2]]) {
|
|
80
|
+
if (mode === "160000")
|
|
81
|
+
throw new Error("Review evidence rejects changed gitlinks.");
|
|
82
|
+
if (!new Set(["000000", "100644", "100755", "120000"]).has(mode)) {
|
|
83
|
+
throw new Error(`Review evidence rejects unsupported file mode: ${mode}.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const paths = /^[RC]/.test(match[3]) ? 2 : 1;
|
|
87
|
+
for (let count = 0; count < paths; count++) {
|
|
88
|
+
if (fields[index] === undefined)
|
|
89
|
+
throw new Error("Git returned malformed raw diff paths.");
|
|
90
|
+
validatePath(fields[index++]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function registeredWorktrees(value) {
|
|
95
|
+
if (!value.endsWith("\0") || value.includes("�"))
|
|
96
|
+
throw new Error("Git returned malformed worktree list.");
|
|
97
|
+
const result = [];
|
|
98
|
+
let record = {};
|
|
99
|
+
for (const field of [...value.slice(0, -1).split("\0"), ""]) {
|
|
100
|
+
if (!field) {
|
|
101
|
+
if (Object.keys(record).length)
|
|
102
|
+
result.push(record);
|
|
103
|
+
record = {};
|
|
104
|
+
}
|
|
105
|
+
else if (field.startsWith("worktree "))
|
|
106
|
+
record.path = field.slice("worktree ".length);
|
|
107
|
+
else if (field.startsWith("HEAD "))
|
|
108
|
+
record.head = oid(`${field.slice("HEAD ".length)}\n`, "worktree HEAD");
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
async function assertCleanRegisteredWorktree(worktree, tip, signal) {
|
|
113
|
+
const root = await fs.realpath(line(await runGit(["rev-parse", "--show-toplevel"], worktree, signal), "worktree root"));
|
|
114
|
+
if (root !== worktree)
|
|
115
|
+
throw new Error("Review evidence requires a registered worktree root.");
|
|
116
|
+
const registrations = registeredWorktrees(await runGit(["worktree", "list", "--porcelain", "-z"], worktree, signal));
|
|
117
|
+
const registered = await Promise.all(registrations
|
|
118
|
+
.filter((entry) => entry.path === worktree)
|
|
119
|
+
.map(async (entry) => ({ ...entry, path: await fs.realpath(entry.path) })));
|
|
120
|
+
if (!registered.some((entry) => entry.head === tip)) {
|
|
121
|
+
throw new Error("Review evidence worktree is not registered at the requested tip.");
|
|
122
|
+
}
|
|
123
|
+
if (oid(await runGit(["rev-parse", "HEAD"], worktree, signal), "worktree HEAD") !== tip) {
|
|
124
|
+
throw new Error("Review evidence worktree moved from the requested tip.");
|
|
125
|
+
}
|
|
126
|
+
if (await runGit(["status", "--porcelain=v1", "--untracked-files=all", "--ignore-submodules=none"], worktree, signal)) {
|
|
127
|
+
throw new Error("Review evidence worktree is not clean.");
|
|
128
|
+
}
|
|
129
|
+
const flags = await inspectIndexFlags(worktree, git, signal);
|
|
130
|
+
if (flags.failure)
|
|
131
|
+
throw new Error(`Review evidence index inspection failed: ${flags.failure}`);
|
|
132
|
+
if (flags.hidden)
|
|
133
|
+
throw new Error("Review evidence rejects assume-unchanged or skip-worktree index entries.");
|
|
134
|
+
}
|
|
135
|
+
async function streamPatch(base, tip, worktree, path, signal) {
|
|
136
|
+
const file = await fs.open(path, "wx", 0o600);
|
|
137
|
+
try {
|
|
138
|
+
await fs.chmod(path, 0o600);
|
|
139
|
+
signal?.throwIfAborted();
|
|
140
|
+
const args = ["--no-pager", "diff", "--no-ext-diff", "--no-textconv", "--ignore-submodules=none", "--binary", base, tip];
|
|
141
|
+
const child = spawn("git", args, { cwd: worktree, signal, timeout: GIT_TIMEOUT_MS, stdio: ["ignore", "pipe", "pipe"] });
|
|
142
|
+
let stderr = Buffer.alloc(0);
|
|
143
|
+
let childError;
|
|
144
|
+
child.stderr.on("data", (chunk) => {
|
|
145
|
+
if (stderr.length < STDERR_LIMIT)
|
|
146
|
+
stderr = Buffer.concat([stderr, chunk.subarray(0, STDERR_LIMIT - stderr.length)]);
|
|
147
|
+
});
|
|
148
|
+
child.once("error", (error) => { childError = error; });
|
|
149
|
+
const closed = new Promise((resolve) => child.once("close", resolve));
|
|
150
|
+
let bytes = 0;
|
|
151
|
+
let overflow = false;
|
|
152
|
+
let outputError;
|
|
153
|
+
try {
|
|
154
|
+
for await (const chunk of child.stdout) {
|
|
155
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
156
|
+
const available = REVIEW_MAX_PATCH_BYTES - bytes;
|
|
157
|
+
if (data.length > available) {
|
|
158
|
+
if (available > 0)
|
|
159
|
+
await file.writeFile(data.subarray(0, available));
|
|
160
|
+
bytes += Math.max(available, 0);
|
|
161
|
+
overflow = true;
|
|
162
|
+
child.kill();
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
await file.writeFile(data);
|
|
166
|
+
bytes += data.length;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
outputError = error;
|
|
171
|
+
child.kill();
|
|
172
|
+
}
|
|
173
|
+
const code = await closed;
|
|
174
|
+
signal?.throwIfAborted();
|
|
175
|
+
if (overflow)
|
|
176
|
+
throw new Error(`Review evidence exceeds ${REVIEW_MAX_PATCH_BYTES} bytes; split the review.`);
|
|
177
|
+
if (outputError)
|
|
178
|
+
throw outputError;
|
|
179
|
+
if (childError)
|
|
180
|
+
throw childError;
|
|
181
|
+
if (code !== 0)
|
|
182
|
+
throw failure(args, { code: code ?? -1, stdout: "", stderr: stderr.toString() });
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
await file.close();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function makeEvidenceDirectory() {
|
|
189
|
+
const directory = await fs.mkdtemp(join(tmpdir(), "pi-subagent-review-"));
|
|
190
|
+
try {
|
|
191
|
+
await fs.chmod(directory, 0o700);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
let cleanup;
|
|
198
|
+
return {
|
|
199
|
+
directory,
|
|
200
|
+
cleanup: () => cleanup ??= fs.rm(directory, { recursive: true, force: true }),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** Prepares the one exact private patch Flow supplies to its Reviewer. */
|
|
204
|
+
export async function prepareExactReviewEvidence(request, signal) {
|
|
205
|
+
const requestedWorktree = requestText(request.worktree, "review worktree");
|
|
206
|
+
const worktree = await fs.realpath(requestedWorktree);
|
|
207
|
+
const [base, tip] = await Promise.all([
|
|
208
|
+
resolveCommit(request.base, worktree, signal),
|
|
209
|
+
resolveCommit(request.tip, worktree, signal),
|
|
210
|
+
]);
|
|
211
|
+
await assertCleanRegisteredWorktree(worktree, tip, signal);
|
|
212
|
+
const changedPaths = parsePaths(await runGit([
|
|
213
|
+
"diff", "--no-ext-diff", "--no-textconv", "--ignore-submodules=none", "--name-only", "-z", base, tip,
|
|
214
|
+
], worktree, signal));
|
|
215
|
+
assertSupportedChanges(await runGit([
|
|
216
|
+
"diff", "--no-ext-diff", "--no-textconv", "--ignore-submodules=none", "--raw", "-z", base, tip,
|
|
217
|
+
], worktree, signal));
|
|
218
|
+
const owned = await makeEvidenceDirectory();
|
|
219
|
+
const patchPath = join(owned.directory, "review.patch");
|
|
220
|
+
try {
|
|
221
|
+
await streamPatch(base, tip, worktree, patchPath, signal);
|
|
222
|
+
await assertCleanRegisteredWorktree(worktree, tip, signal);
|
|
223
|
+
return { base, tip, worktree, changedPaths, patchPath, cleanup: owned.cleanup };
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
await owned.cleanup();
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
package/dist/worktree.d.ts
CHANGED
|
@@ -19,6 +19,11 @@ export type GitRunner = (args: string[], cwd: string, signal?: AbortSignal) => P
|
|
|
19
19
|
stdout: string;
|
|
20
20
|
stderr: string;
|
|
21
21
|
}>;
|
|
22
|
+
export declare class WorktreeSetupError extends Error {
|
|
23
|
+
name: string;
|
|
24
|
+
readonly worktree: WorktreeInfo;
|
|
25
|
+
constructor(message: string, worktree: WorktreeInfo);
|
|
26
|
+
}
|
|
22
27
|
/**
|
|
23
28
|
* Creates one worktree per child from parent HEAD. Returns undefined only when
|
|
24
29
|
* the workspace is not a git repository or HEAD is unborn — callers degrade
|
|
@@ -27,6 +32,17 @@ export type GitRunner = (args: string[], cwd: string, signal?: AbortSignal) => P
|
|
|
27
32
|
* role never silently loses its isolation.
|
|
28
33
|
*/
|
|
29
34
|
export declare function createChildWorktree(cwd: string, childId: string, run?: GitRunner, signal?: AbortSignal): Promise<WorktreeInfo | undefined>;
|
|
35
|
+
export interface WorktreeDirtyInspection {
|
|
36
|
+
dirty: boolean;
|
|
37
|
+
failure?: string;
|
|
38
|
+
initializedSubmodules?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function inspectIndexFlags(cwd: string, run?: GitRunner, signal?: AbortSignal): Promise<{
|
|
41
|
+
hidden: boolean;
|
|
42
|
+
failure?: string;
|
|
43
|
+
}>;
|
|
44
|
+
/** Proves a worktree has no tracked, untracked, ignored, index-hidden, or nested submodule work. */
|
|
45
|
+
export declare function inspectWorktreeDirty(cwd: string, run?: GitRunner): Promise<WorktreeDirtyInspection>;
|
|
30
46
|
/**
|
|
31
47
|
* Inspects and possibly prunes a child worktree after it finishes. Commit count
|
|
32
48
|
* reads the dedicated branch and refuses to prune when checkout HEAD no longer
|
package/dist/worktree.js
CHANGED
|
@@ -6,8 +6,13 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
6
6
|
const GIT_TIMEOUT_MS = 30_000;
|
|
7
7
|
const WORKTREES_DIRNAME = ".worktrees";
|
|
8
8
|
const BRANCH_NAMESPACE = "pi-subagent";
|
|
9
|
-
class WorktreeSetupError extends Error {
|
|
9
|
+
export class WorktreeSetupError extends Error {
|
|
10
10
|
name = "WorktreeSetupError";
|
|
11
|
+
worktree;
|
|
12
|
+
constructor(message, worktree) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.worktree = worktree;
|
|
15
|
+
}
|
|
11
16
|
}
|
|
12
17
|
/** Runs git, capturing output; never throws on non-zero exit or spawn failure. */
|
|
13
18
|
const runGit = (args, cwd, signal) => new Promise((resolve) => {
|
|
@@ -54,7 +59,7 @@ async function ensureLocalExclude(gitDir) {
|
|
|
54
59
|
await appendFile(exclude, `${existing && !existing.endsWith("\n") ? "\n" : ""}${entry}\n`);
|
|
55
60
|
}
|
|
56
61
|
catch (error) {
|
|
57
|
-
throw new
|
|
62
|
+
throw new Error(`Could not update ${exclude}: ${error instanceof Error ? error.message : String(error)}`);
|
|
58
63
|
}
|
|
59
64
|
}
|
|
60
65
|
/**
|
|
@@ -137,11 +142,12 @@ export async function createChildWorktree(cwd, childId, run = runGit, signal) {
|
|
|
137
142
|
}
|
|
138
143
|
await ensureLocalExclude(gitDir);
|
|
139
144
|
signal?.throwIfAborted();
|
|
145
|
+
const worktree = { path, cwd: join(path, relativeCwd), branch, repoRoot: stableRepoRoot, baseCommit };
|
|
140
146
|
const added = await run(["worktree", "add", path, "-b", branch, baseCommit], repoRoot, signal);
|
|
141
147
|
if (added.code !== 0) {
|
|
142
|
-
throw new WorktreeSetupError(`git worktree add failed
|
|
148
|
+
throw new WorktreeSetupError(`git worktree add failed after attempting path=${JSON.stringify(path)} branch=${JSON.stringify(branch)} base=${baseCommit}: ${added.stderr.trim().slice(0, 200)}`, worktree);
|
|
143
149
|
}
|
|
144
|
-
return
|
|
150
|
+
return worktree;
|
|
145
151
|
}
|
|
146
152
|
/** Flags a payload whose state could not be measured (#88113): unmeasured is not zero. */
|
|
147
153
|
function markUnproven(payload, reason, unmeasured = "commits/dirty") {
|
|
@@ -151,6 +157,12 @@ function markUnproven(payload, reason, unmeasured = "commits/dirty") {
|
|
|
151
157
|
+ `Any remaining worktree or branch was preserved — inspect ${payload.path} (branch ${payload.branch}) before assuming no work.`;
|
|
152
158
|
return payload;
|
|
153
159
|
}
|
|
160
|
+
export async function inspectIndexFlags(cwd, run = runGit, signal) {
|
|
161
|
+
const flags = await run(["ls-files", "-v", "-z"], cwd, signal);
|
|
162
|
+
if (flags.code !== 0)
|
|
163
|
+
return { hidden: false, failure: `ls-files exit ${flags.code}: ${flags.stderr.trim().slice(0, 200)}` };
|
|
164
|
+
return { hidden: flags.stdout.split("\0").some((entry) => /^(?:[a-z]|S) /.test(entry)) };
|
|
165
|
+
}
|
|
154
166
|
async function inspectDirty(run, cwd) {
|
|
155
167
|
const refreshed = await run(["update-index", "--really-refresh"], cwd);
|
|
156
168
|
if (refreshed.code !== 0 && refreshed.code !== 1) {
|
|
@@ -161,14 +173,40 @@ async function inspectDirty(run, cwd) {
|
|
|
161
173
|
return { dirty: false, failure: `status exit ${status.code}: ${status.stderr.trim().slice(0, 200)}` };
|
|
162
174
|
if (refreshed.code === 1 || status.stdout.trim())
|
|
163
175
|
return { dirty: true };
|
|
164
|
-
const flags = await
|
|
165
|
-
if (flags.
|
|
166
|
-
return { dirty: false, failure:
|
|
167
|
-
if (flags.
|
|
176
|
+
const flags = await inspectIndexFlags(cwd, run);
|
|
177
|
+
if (flags.failure)
|
|
178
|
+
return { dirty: false, failure: flags.failure };
|
|
179
|
+
if (flags.hidden)
|
|
168
180
|
return { dirty: false, failure: "assume-unchanged or skip-worktree index entries remain" };
|
|
169
|
-
}
|
|
170
181
|
return { dirty: false };
|
|
171
182
|
}
|
|
183
|
+
/** Proves a worktree has no tracked, untracked, ignored, index-hidden, or nested submodule work. */
|
|
184
|
+
export async function inspectWorktreeDirty(cwd, run = runGit) {
|
|
185
|
+
const root = await inspectDirty(run, cwd);
|
|
186
|
+
if (root.dirty || root.failure)
|
|
187
|
+
return root;
|
|
188
|
+
const modules = await run(["submodule", "status", "--recursive"], cwd);
|
|
189
|
+
if (modules.code !== 0)
|
|
190
|
+
return { dirty: false, failure: `submodule list exit ${modules.code}: ${modules.stderr.trim().slice(0, 200)}` };
|
|
191
|
+
const initializedSubmodules = modules.stdout.split("\n").some((line) => line && !line.startsWith("-"));
|
|
192
|
+
if (initializedSubmodules) {
|
|
193
|
+
const listed = await run(["submodule", "foreach", "--recursive", "--quiet", "printf '%s\\0' \"$PWD\""], cwd);
|
|
194
|
+
if (listed.code !== 0)
|
|
195
|
+
return { dirty: false, failure: `submodule list exit ${listed.code}: ${listed.stderr.trim().slice(0, 200)}` };
|
|
196
|
+
const paths = listed.stdout.split("\0").filter(Boolean);
|
|
197
|
+
if (!paths.length)
|
|
198
|
+
return { dirty: false, failure: "initialized submodule paths unavailable" };
|
|
199
|
+
for (const path of paths) {
|
|
200
|
+
const nested = await inspectDirty(run, path);
|
|
201
|
+
if (nested.failure)
|
|
202
|
+
return { dirty: false, failure: `submodule ${path}: ${nested.failure}` };
|
|
203
|
+
if (nested.dirty)
|
|
204
|
+
return { dirty: true };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const rechecked = await inspectDirty(run, cwd);
|
|
208
|
+
return initializedSubmodules ? { ...rechecked, initializedSubmodules } : rechecked;
|
|
209
|
+
}
|
|
172
210
|
/**
|
|
173
211
|
* Inspects and possibly prunes a child worktree after it finishes. Commit count
|
|
174
212
|
* reads the dedicated branch and refuses to prune when checkout HEAD no longer
|
|
@@ -205,34 +243,14 @@ export async function finalizeChildWorktree(info, run = runGit) {
|
|
|
205
243
|
if (head.code !== 0 || head.stdout.trim() !== `refs/heads/${info.branch}`) {
|
|
206
244
|
return markUnproven(payload, "HEAD is detached, switched, or unreadable", "checked-out commits");
|
|
207
245
|
}
|
|
208
|
-
const
|
|
209
|
-
if (modules.code !== 0)
|
|
210
|
-
return markUnproven(payload, `submodule list exit ${modules.code}: ${modules.stderr.trim().slice(0, 200)}`, "dirty");
|
|
211
|
-
forceRemove = modules.stdout.split("\n").some((line) => line && !line.startsWith("-"));
|
|
212
|
-
if (forceRemove) {
|
|
213
|
-
const listed = await run(["submodule", "foreach", "--recursive", "--quiet", "printf '%s\\0' \"$PWD\""], info.path);
|
|
214
|
-
if (listed.code !== 0)
|
|
215
|
-
return markUnproven(payload, `submodule list exit ${listed.code}: ${listed.stderr.trim().slice(0, 200)}`, "dirty");
|
|
216
|
-
const paths = listed.stdout.split("\0").filter(Boolean);
|
|
217
|
-
if (!paths.length)
|
|
218
|
-
return markUnproven(payload, "initialized submodule paths unavailable", "dirty");
|
|
219
|
-
for (const path of paths) {
|
|
220
|
-
const nested = await inspectDirty(run, path);
|
|
221
|
-
if (nested.failure)
|
|
222
|
-
return markUnproven(payload, `submodule ${path}: ${nested.failure}`, "dirty");
|
|
223
|
-
if (nested.dirty) {
|
|
224
|
-
payload.dirty = true;
|
|
225
|
-
return payload;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
const rechecked = await inspectDirty(run, info.path);
|
|
246
|
+
const rechecked = await inspectWorktreeDirty(info.path, run);
|
|
230
247
|
if (rechecked.failure)
|
|
231
248
|
return markUnproven(payload, `final ${rechecked.failure}`, "dirty");
|
|
232
249
|
if (rechecked.dirty) {
|
|
233
250
|
payload.dirty = true;
|
|
234
251
|
return payload;
|
|
235
252
|
}
|
|
253
|
+
forceRemove = Boolean(rechecked.initializedSubmodules);
|
|
236
254
|
}
|
|
237
255
|
else if (payload.commits > 0)
|
|
238
256
|
return payload;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# Compose workflows outside the ephemeral executor
|
|
1
|
+
# Compose generic workflows outside the ephemeral executor
|
|
2
2
|
|
|
3
3
|
## Decision
|
|
4
4
|
|
|
5
|
-
The public task executor is an execution mechanism: it receives a prepared Pi Launch, runs one bounded Delegated Task, and returns the result. `single`, `parallel`,
|
|
5
|
+
The public task executor is an execution mechanism: it receives a prepared Pi Launch, runs one bounded Delegated Task, and returns the result. Main plans and orchestrates; `delegate_task` selects its flat `single`, `parallel`, or `chain` tool policy, while those modes are not executor primitives.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Generic callers compose workflows with JavaScript. Fan-out and fan-in use promises and collections; sequencing uses ordinary control flow; review loops use explicit caller-owned bounds. The package does not define a recursive workflow AST.
|
|
8
8
|
|
|
9
9
|
Resource Policy is split at launch preparation:
|
|
10
10
|
|
|
@@ -12,8 +12,14 @@ Resource Policy is split at launch preparation:
|
|
|
12
12
|
- Caller may add explicit tools, extensions, and environment through `createRoleLaunch`.
|
|
13
13
|
- The executor receives the resulting Pi Launch and does not discover resources.
|
|
14
14
|
|
|
15
|
-
Built-in `implementer` and `reviewer` Roles ship
|
|
15
|
+
Built-in `implementer` and `reviewer` Roles ship as Markdown in `examples/roles/` and use the same parser as user Roles. For generic delegation, a same-named user Role explicitly overrides a built-in. The package does not install, copy, or write user configuration.
|
|
16
|
+
|
|
17
|
+
Main populates direct `model` and `thinking` only for explicit user overrides; otherwise it chooses only `modelClass` (`fast` normally, `balanced` upfront for obvious complexity). This is tool policy, not executor provenance tracking or runtime enforcement.
|
|
18
|
+
|
|
19
|
+
## Scope boundary
|
|
20
|
+
|
|
21
|
+
`delegate_flow` is a fixed package-owned Git workflow, documented in [ADR 002](./002-package-owned-delegate-flow-orchestration.md). It reuses the prepared-child runner but is not a general executor workflow primitive: it has its own fixed unit, worktree, validation, review, integration, and cleanup contract. `delegate_task` and library callers remain generic.
|
|
16
22
|
|
|
17
23
|
## Consequences
|
|
18
24
|
|
|
19
|
-
The executor remains a stable mechanism while callers own
|
|
25
|
+
The executor remains a stable mechanism while generic callers own semantic protocols, shared workspace/state, retry decisions, and bounds. The package-owned Flow removes only its repeated deterministic Git mechanics; it does not turn the executor into a workflow language.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Package-owned `delegate_flow` orchestration
|
|
2
|
+
|
|
3
|
+
## Decision
|
|
4
|
+
|
|
5
|
+
`delegate_task` remains the generic bounded Role tool described by [ADR 001](./001-composable-ephemeral-execution.md). Main plans and orchestrates; `delegate_flow` is a separate package-owned Git workflow with this fixed interface:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
delegate_flow({ units: [{ id, task, modelClass?, validation: [{ command, args }], review? }] });
|
|
9
|
+
delegate_flow_continue({ guidance, modelClass? });
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
A Flow accepts 1–8 independent units with unique IDs. `modelClass` is optional and otherwise uses the shared `pi-subagent/delegateTask` assignment; its class resolves through the existing `pi-task-models` profile model-and-thinking route for the unit's Implementer and, when applicable, Reviewer. `review` is optional non-empty text for the explicit judgment that declared validation cannot establish.
|
|
13
|
+
|
|
14
|
+
Only one memory-only Flow may be active. At start it resolves/freezes the effective `implementer` Role, including a same-named user override. It resolves/freezes the effective `reviewer` Role only when at least one requested unit has `review`. It requires a clean committed attached Main branch and creates one Unit Worktree per unit before launching Implementers in parallel. All started Implementers settle; Flow then processes units in declared order.
|
|
15
|
+
|
|
16
|
+
For each unit, Flow verifies Main, rebases the Unit Worktree in place when earlier Flow units advanced Main, inspects committed Git state, and runs declared validation. Validation is authoritative for objective verification. Without `review`, Flow skips exact evidence and Reviewer launch, then fast-forwards the exact validated tip through the existing guarded `git merge --ff-only` path. With `review`, it gives the Reviewer the exact `{base, tip, patchPath}` packet in that same worktree; only trimmed output exactly equal to `PASS` permits the same full-OID fast-forward. Cleanup uses non-forced worktree removal and branch deletion; cleanup refusal does not undo integration and returns completion with retained-work warnings.
|
|
17
|
+
|
|
18
|
+
If rebase drops all unit commits, `base === tip` is a no-op. Flow validates the state, skips Reviewer and merge, then cleans up ordinarily. Implementer failure, dirty or missing committed work, validation failure, and reviewer findings block the first affected declared unit. `delegate_flow_continue({ guidance, modelClass? })` launches a fresh ephemeral child with the frozen Implementer Role in that same Unit Worktree once, with original requirements, authoritative validation, previous block evidence, and Main guidance. Omitted continuation class retains the Unit's current class; a supplied class replaces it for that one repair, including any subsequent Reviewer launch. A second block is terminal. Failed rebase, evidence/Reviewer, and other infrastructure failures retain worktrees. A reported fast-forward failure completes with its diagnostic as a warning only when Main is clean at the exact integrated tip; otherwise it is terminal. Earlier integrations are never rolled back.
|
|
19
|
+
|
|
20
|
+
## Consequences
|
|
21
|
+
|
|
22
|
+
Flow owns narrow deterministic Git mechanics while allowing user-owned Implementer and conditional Reviewer policy through same-named Role overrides. Overrides do not change validation authority, exact review protocol, approval, integration, or cleanup. Flow has no dependency graph, saved recovery, automatic retry, aggregate review, post-merge validation, Planner Role, or generic-role restriction. Units that overlap files, APIs, schemas, generated output, package metadata, lockfiles, or invariants must be combined or sequenced outside Flow.
|
package/docs/orchestration.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Orchestration and package-author API
|
|
2
2
|
|
|
3
|
-
`pi-subagent` separates
|
|
3
|
+
`pi-subagent` separates generic delegation from its execution mechanism:
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
6
|
Role (built-in or user override) + latest Pi registries ── resolveRoleLaunch ──> PiLaunch
|
|
@@ -10,7 +10,9 @@ caller-owned task, cwd, signal ────────────────
|
|
|
10
10
|
active-Pi ephemeral executor
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Main plans and orchestrates. `delegate_task` owns its flat single/parallel/chain policy, while the public executor runs one prepared delegation. Downstream packages compose their own workflows with ordinary JavaScript and own semantic protocols, shared workspace/state, retry decisions, and bounds. There is no recursive workflow AST.
|
|
14
|
+
|
|
15
|
+
`delegate_flow` is the exception: it is a fixed package-owned Git workflow, not an executor primitive or general workflow language. It uses the effective `implementer` Role and, only for explicit judgment review, the effective `reviewer` Role through the same prepared-child runner; its contract is below.
|
|
14
16
|
|
|
15
17
|
## Frozen `delegate_task` contract
|
|
16
18
|
|
|
@@ -65,11 +67,11 @@ Single mode puts one delegation's fields at the top level.
|
|
|
65
67
|
| --- | --- | --- |
|
|
66
68
|
| `role` | yes | Name of a Role in the user's effective `config/pi-subagent` directory or a package-shipped built-in (`implementer`, `reviewer`); a same-named user file overrides the built-in. |
|
|
67
69
|
| `task` | yes | Non-empty bounded task packet. |
|
|
68
|
-
| `model` | no | Designated `provider/modelId`; takes precedence over `modelClass
|
|
69
|
-
| `modelClass` | no | `fast`, `balanced`, `frontier`, or `fav`; omission uses shared task assignment. |
|
|
70
|
-
| `thinking` | no | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`;
|
|
70
|
+
| `model` | no | Designated `provider/modelId`; takes precedence over `modelClass`, and Main supplies it only for an explicit user override. |
|
|
71
|
+
| `modelClass` | no | `fast`, `balanced`, `frontier`, or `fav`; Main normally chooses `fast`, may choose `balanced` upfront for obvious complexity, and omission uses shared task assignment. |
|
|
72
|
+
| `thinking` | no | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`; Main supplies it only for an explicit user override. Route selection skips models that cannot honor it. |
|
|
71
73
|
|
|
72
|
-
Those five fields are the complete delegation object. `tasks`, `chain`, and `background` cannot be nested. Route fallback occurs only before launch; a started child is never retried by this package.
|
|
74
|
+
Those five fields are the complete delegation object. The direct-model/thinking rule is Main-facing policy only: the runtime adds no provenance tracking or enforcement. `tasks`, `chain`, and `background` cannot be nested. Route fallback occurs only before launch; a started child is never retried by this package.
|
|
73
75
|
|
|
74
76
|
### Background, failures, and transport
|
|
75
77
|
|
|
@@ -79,16 +81,43 @@ Any foreground entry failure makes the tool call throw. Parallel mode first sett
|
|
|
79
81
|
|
|
80
82
|
All Main-visible text for one tool call shares one aggregate 50 KiB UTF-8 transport cap, including child output, sibling failures, and worktree/recovery evidence. Parallel execution does not multiply the cap by its entry count. Truncation is explicit; internal bookkeeping is not made visible by bypassing the cap.
|
|
81
83
|
|
|
84
|
+
## `delegate_flow`
|
|
85
|
+
|
|
86
|
+
`delegate_flow({ units })` accepts 1–8 units with unique non-empty `id` and `task`, one or more direct `{command, args}` validation commands, optional `modelClass`, and optional non-empty `review` text. `delegate_flow_continue({ guidance, modelClass? })` is available only for the one blocked unit of the active Flow.
|
|
87
|
+
|
|
88
|
+
A Flow is memory-only and permits one active Flow. At start it always resolves/freezes the effective `implementer` Role, including a same-named user override. It resolves/freezes the effective `reviewer` only if at least one requested unit declares `review`. Omitted unit classes use the shared `pi-subagent/delegateTask` assignment; a selected class resolves through its existing `pi-task-models` profile model-and-thinking route for the unit's Implementer and, when applicable, Reviewer. It requires clean committed Git Main and creates every Unit Worktree before launching work; setup failure launches no Implementer. Each unit gets exactly one worktree and one Implementer. Implementers run in parallel and all settle. Flow then processes units in declared order:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
Implementers (parallel, one Unit Worktree each)
|
|
92
|
+
│ all settle
|
|
93
|
+
v
|
|
94
|
+
for each declared unit:
|
|
95
|
+
rebase in its Unit Worktree when earlier units advanced Main
|
|
96
|
+
inspect committed state; run declared validation (objective authority)
|
|
97
|
+
├─ no review: git merge --ff-only <exact validated tip>
|
|
98
|
+
└─ review: Reviewer receives exact {base, tip, patchPath}
|
|
99
|
+
exact PASS → git merge --ff-only <full reviewed OID>
|
|
100
|
+
git worktree remove; git branch -d
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Flow derives identity from Git, not child output. Add `review` only for an explicit judgment criterion that automated validation cannot establish; it is not a second generic verification pass. The Reviewer reads the exact patch as authoritative and may use the same worktree only for referenced context. A full-OID fast-forward is the only integration path. Cleanup is non-forced; after a successful integration, cleanup refusal returns `completed` with a retained path/branch warning.
|
|
104
|
+
|
|
105
|
+
If rebase drops all unit commits, `base === tip` is a no-op: Flow validates current state, skips Reviewer and merge, then cleans up ordinarily. Implementer failure, dirty or missing committed work, validation failure, or reviewer findings block the first affected declared unit. `delegate_flow_continue({ guidance, modelClass? })` reruns the Flow's frozen Implementer Role in that same worktree once, then repeats derivation, validation, and conditional review with fresh exact evidence. Omitting continuation `modelClass` retains the blocked unit's current class; providing it replaces that class for the one repair. A second block is terminal. A failed rebase is aborted and terminates as an infrastructure failure with Git diagnostics; other infrastructure failures are terminal. A reported fast-forward failure completes with its diagnostic as a warning only when Main is clean at the exact integrated tip; otherwise it is terminal. Terminal outcomes retain worktrees for Main to reslice. Earlier integrated units are never rolled back.
|
|
106
|
+
|
|
107
|
+
Flow has no dependency graph, saved state, automatic retry, aggregate review, or post-merge validation. Use it only for commuting changes; combine or sequence units that overlap files, APIs, schemas, generated output, package metadata, lockfiles, or invariants.
|
|
108
|
+
|
|
109
|
+
`delegate_task` remains generic: its optional worktree isolation, non-Git behavior, and direct plan/file review are unchanged. Flow uses package-shipped Roles as defaults while retaining same-named user Role overrides; the Reviewer is needed only for a requested review criterion.
|
|
110
|
+
|
|
82
111
|
## Per-delegation resources and isolation
|
|
83
112
|
|
|
84
|
-
|
|
113
|
+
For `delegate_task`, every single entry, parallel sibling, and chain step independently:
|
|
85
114
|
|
|
86
115
|
1. loads its selected Role;
|
|
87
116
|
2. resolves its route and named Skills from the latest effective Pi context after receiving an executor permit;
|
|
88
117
|
3. creates its Role launch policy; and
|
|
89
118
|
4. when the Role requests `isolation: worktree`, creates a worktree identified by the tool call, mode, and input index.
|
|
90
119
|
|
|
91
|
-
Separate deterministic identities produce separate
|
|
120
|
+
Separate deterministic identities produce separate worktree paths and branches. Parallel siblings cannot collide, and a chain does not base one step's worktree on the preceding step's branch. `{previous}` passes text only. There is no implicit shared worktree or hidden workflow state.
|
|
92
121
|
|
|
93
122
|
A worktree starts from Main's current `HEAD`. Clean worktrees with no child commits are pruned; committed, dirty, switched, unmeasurable, or otherwise recoverable work is preserved and reported. Non-git directories and repositories with an unborn `HEAD` use Main's working directory. Git submodules reject worktree isolation, and setup failure in a real repository throws rather than silently sharing Main's checkout. This generic fallback remains unchanged: the bundled delegated-development Skill separately refuses to begin without a committed Git `HEAD`.
|
|
94
123
|
|
|
@@ -98,17 +127,17 @@ If steps must share files, make that an explicit caller decision: use an intenti
|
|
|
98
127
|
|
|
99
128
|
Every Role launch centrally prepends this child identity contract to its system instructions before the Role prompt: the child is a delegated Pi Subagent, not Main; it executes its assigned Role and task directly; Main-only delegation rules do not apply; recursive delegation is unavailable and it must not seek or invoke delegation tools.
|
|
100
129
|
|
|
101
|
-
A Role file
|
|
130
|
+
A Role file requires:
|
|
102
131
|
|
|
103
|
-
- base tools
|
|
104
|
-
- explicit extension paths or package sources;
|
|
105
|
-
- additional effective Pi Skill names;
|
|
132
|
+
- base tools: a YAML array; `tools: []` activates no base built-ins, while trusted selected extension tools still activate;
|
|
133
|
+
- explicit extension paths or package sources: a YAML array; `extensions: []` selects no Role extension bundle;
|
|
134
|
+
- additional effective Pi Skill names: a YAML array; `skills: []` selects no separately named Role Skills, while trusted selected extension Skills still load;
|
|
106
135
|
- system instructions; and
|
|
107
136
|
- optional `isolation: worktree` for the tool layer.
|
|
108
137
|
|
|
109
|
-
At launch, a package caller may add `tools`, `extensions`, and `env
|
|
138
|
+
Every launch installs the Role tool policy. At launch, a package caller may add `tools`, `extensions`, and `env`; caller tools are unioned into the Role base list and loaded extension tools activate in every case. Caller `env` adds to or overrides the active Pi process environment for the child.
|
|
110
139
|
|
|
111
|
-
Children start with ambient extension and Skill discovery disabled. Only explicit Role/caller extensions, explicitly resolved Skill paths, resources supplied by those extension packages, and any required internal tool-policy or Codex adapter load. Loaded extension tools activate even when the Role base list is empty. Child-inappropriate parent tools are always excluded: `delegate_task` and `ask_question`.
|
|
140
|
+
Children start with ambient extension and Skill discovery disabled. Only explicit Role/caller extensions, explicitly resolved Skill paths, resources supplied by those extension packages, and any required internal tool-policy or Codex adapter load. Loaded extension tools activate even when the Role base list is empty. Child-inappropriate parent tools are always excluded: `delegate_task`, `delegate_flow`, `delegate_flow_continue`, and `ask_question`. Explicit Role/caller tool names are verified against the final filtered active child registry after every explicit provider extension completes `session_start`; unavailable names fail before the first model turn and identify the missing names with provider-extension guidance.
|
|
112
141
|
|
|
113
142
|
Role Skill names resolve through Main's effective Pi Skill registry at launch. Missing names are returned in `ResolvedRoleLaunch.missingSkills`; `delegate_task` warns and skips them. Library callers must surface that warning themselves. Missing Skills do not block launch.
|
|
114
143
|
|
|
@@ -125,7 +154,7 @@ The package root exports the following mechanism-level APIs:
|
|
|
125
154
|
| `createEphemeralSubagentExecutor(options)` | Queue and run one prepared no-session child per `run`. |
|
|
126
155
|
| `createChildWorktree` / `finalizeChildWorktree` | Optional caller-managed worktree lifecycle. |
|
|
127
156
|
|
|
128
|
-
A loaded `Role` contains `name`, `description`,
|
|
157
|
+
A loaded `Role` contains `name`, `description`, required normalized `tools`, `extensions`, and `skills` arrays, optional `isolation`, and `systemPrompt`. `resolveRoleLaunch` accepts `role`, `taskId`, and optional caller `agentDir`, `extensions`, `tools`, and `env`. Its result is a `PiLaunch` (`{ env, args }`) plus the selected `model`, `thinkingLevel`, and `missingSkills`.
|
|
129
158
|
|
|
130
159
|
`createEphemeralSubagentExecutor` requires:
|
|
131
160
|
|
|
@@ -138,7 +167,7 @@ const executorOptions = {
|
|
|
138
167
|
|
|
139
168
|
Concurrency is FIFO. `run` accepts optional `signal`, `onUpdate(text)`, and `onTokens(number)` callbacks plus required `prepare()`. A queued run receives its permit before `prepare` executes, so resource and route resolution can use the latest Pi state. Queued time does not consume child timeout. `maxConcurrency`, `idleMs`, and `maxMs` must be positive; `maxMs` must exceed `idleMs`.
|
|
140
169
|
|
|
141
|
-
The executor is **active-Pi-only**. It reuses the currently running Pi invocation and does not locate or support a standalone Node.js Pi installation.
|
|
170
|
+
The executor is **active-Pi-only**. It reuses the currently running Pi invocation and does not locate or support a standalone Node.js Pi installation. Once direct Pi exits, stdout/stderr drain normally until EOF; an escaped descendant retaining either stream is cut off after short output inactivity or a one-second hard deadline so it cannot retain the FIFO permit.
|
|
142
171
|
|
|
143
172
|
### Prepare after the permit
|
|
144
173
|
|
|
@@ -328,7 +357,7 @@ The package ships two working built-in Roles, validated by the same parser as us
|
|
|
328
357
|
| Built-in | Behavior |
|
|
329
358
|
| --- | --- |
|
|
330
359
|
| `implementer` | Focused implementation requesting `isolation: worktree`; commits scoped changes locally, never pushes or opens PRs without authorization. Non-Git or unborn-`HEAD` contexts may use Main's cwd. |
|
|
331
|
-
| `reviewer` | Read-only correctness review
|
|
360
|
+
| `reviewer` | Read-only correctness review of supplied plans/files, or—when a Flow unit declares `review`—Flow's exact `{base, tip, patchPath}` packet in its Unit Worktree; never edits or commits. |
|
|
332
361
|
|
|
333
362
|
A same-named Markdown file in `config/pi-subagent/` explicitly overrides the built-in default.
|
|
334
363
|
|
|
@@ -348,6 +377,6 @@ cp <package-install-dir>/examples/roles/scout.md ~/.pi/agent/config/pi-subagent/
|
|
|
348
377
|
|
|
349
378
|
The package never creates, copies, updates, or removes files in `~/.pi/agent/config/pi-subagent/`. Once copied, the files and their names are entirely user-owned.
|
|
350
379
|
|
|
351
|
-
The bundled [`pi-subagent-delegated-development`](../skills/pi-subagent-delegated-development/SKILL.md) Skill is Main-side
|
|
380
|
+
The bundled [`pi-subagent-delegated-development`](../skills/pi-subagent-delegated-development/SKILL.md) Skill is Main-side planner/orchestrator policy only. `delegate_flow` owns its fixed Git mechanics and objective validation authority; the Skill defines no runtime code or configuration. `delegate_task` remains the generic flat single/parallel/chain mechanism.
|
|
352
381
|
|
|
353
|
-
See
|
|
382
|
+
See [ADR 001](./adr/001-composable-ephemeral-execution.md) for the executor boundary and [ADR 002](./adr/002-package-owned-delegate-flow-orchestration.md) for Flow.
|
|
@@ -9,15 +9,17 @@ tools:
|
|
|
9
9
|
- grep
|
|
10
10
|
- find
|
|
11
11
|
- ls
|
|
12
|
+
extensions: []
|
|
13
|
+
skills: []
|
|
12
14
|
isolation: worktree
|
|
13
15
|
---
|
|
14
16
|
|
|
15
17
|
Implement one bounded task.
|
|
16
18
|
|
|
17
|
-
Read applicable repository instructions and domain context first. Inspect the existing flow and its callers before editing. Work only in explicitly assigned files
|
|
19
|
+
Read applicable repository instructions and domain context first. Inspect the existing flow and its callers before editing. Work only in the assigned cwd and explicitly assigned files; preserve unrelated changes. Fix the root cause with the smallest complete diff, reusing existing patterns and dependencies.
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
For ordinary delegation, run the focused validation needed to establish that the change is correct. When a Flow packet declares an authoritative validation gate, treat that gate as the final validation: run only narrow development checks needed while implementing, and do not duplicate the declared gate. Do not access credentials, use the network, generate artifacts, or broaden scope unless the task explicitly requires it. Never invoke external LLM APIs, SDKs, agent harnesses, or model CLIs; deterministic developer tools remain allowed.
|
|
20
22
|
|
|
21
|
-
Commit completed scoped changes locally unless the task forbids it. Never push or open pull requests without explicit authorization.
|
|
23
|
+
Commit completed scoped changes locally unless the task forbids it. Never create or manage another worktree. Never push or open pull requests without explicit authorization.
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
For ordinary delegation, report the completed change, validation, and remaining risks. For Flow, return the retained assigned cwd, branch, base commit, tip commit, changed files from the base-to-tip committed diff, clean `git status --porcelain=v1 --untracked-files=all` result, and validation results. Do not remove the retained worktree or task branch; Main cleans them only after successful integration and validation.
|