@hcflabs/pi-subagent 0.0.0-stage → 27.10.8

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.
Files changed (66) hide show
  1. package/CONTEXT.md +77 -0
  2. package/LICENSE +22 -0
  3. package/README.md +282 -2
  4. package/dist/composition.d.ts +11 -0
  5. package/dist/composition.js +104 -0
  6. package/dist/direct-herdr.d.ts +71 -0
  7. package/dist/direct-herdr.js +527 -0
  8. package/dist/discard.d.ts +51 -0
  9. package/dist/discard.js +218 -0
  10. package/dist/display-text.d.ts +5 -0
  11. package/dist/display-text.js +9 -0
  12. package/dist/ephemeral.d.ts +79 -0
  13. package/dist/ephemeral.js +916 -0
  14. package/dist/git-process.d.ts +8 -0
  15. package/dist/git-process.js +25 -0
  16. package/dist/git-runtime.d.ts +115 -0
  17. package/dist/git-runtime.js +489 -0
  18. package/dist/herdr-runtime.d.ts +147 -0
  19. package/dist/herdr-runtime.js +1366 -0
  20. package/dist/index.d.ts +170 -0
  21. package/dist/index.js +589 -0
  22. package/dist/integration-git.d.ts +63 -0
  23. package/dist/integration-git.js +504 -0
  24. package/dist/launch-runtime.d.ts +54 -0
  25. package/dist/launch-runtime.js +178 -0
  26. package/dist/mcp-role.d.ts +12 -0
  27. package/dist/mcp-role.js +152 -0
  28. package/dist/process-lease.d.ts +5 -0
  29. package/dist/process-lease.js +43 -0
  30. package/dist/process.d.ts +9 -0
  31. package/dist/process.js +11 -0
  32. package/dist/prune.d.ts +78 -0
  33. package/dist/prune.js +205 -0
  34. package/dist/review-evidence.d.ts +17 -0
  35. package/dist/review-evidence.js +221 -0
  36. package/dist/runner.d.ts +431 -0
  37. package/dist/runner.js +3380 -0
  38. package/dist/schema.d.ts +2560 -0
  39. package/dist/schema.js +1365 -0
  40. package/dist/store.d.ts +66 -0
  41. package/dist/store.js +394 -0
  42. package/dist/worktree.d.ts +65 -0
  43. package/dist/worktree.js +288 -0
  44. package/docs/orchestration-architecture.html +138 -0
  45. package/docs/orchestration-architecture.svg +135 -0
  46. package/docs/orchestration.md +577 -0
  47. package/docs/worker-lifecycle.html +97 -0
  48. package/docs/worker-lifecycle.svg +86 -0
  49. package/example.png +0 -0
  50. package/examples/roles/implementer.md +25 -0
  51. package/examples/roles/reviewer.md +25 -0
  52. package/examples/roles/scout.md +25 -0
  53. package/extensions/admission.ts +82 -0
  54. package/extensions/config.ts +19 -0
  55. package/extensions/git-read.ts +126 -0
  56. package/extensions/isolated.ts +945 -0
  57. package/extensions/model-class-policy.ts +1 -0
  58. package/extensions/result-transport.ts +236 -0
  59. package/extensions/role-mcp.ts +14 -0
  60. package/extensions/role-tools.ts +178 -0
  61. package/extensions/subagent-command.ts +699 -0
  62. package/extensions/subagent.ts +958 -0
  63. package/extensions/workflow.ts +336 -0
  64. package/package.json +84 -4
  65. package/prompts/review.md +20 -0
  66. package/skills/pi-subagent/SKILL.md +41 -0
@@ -0,0 +1,288 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, lstatSync } from "node:fs";
3
+ import { appendFile, mkdir, readFile } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { runGit } from "./git-process.js";
6
+ export function isMissing(error) {
7
+ return Boolean(error && typeof error === "object" && error.code === "ENOENT");
8
+ }
9
+ export function isWithin(root, candidate) {
10
+ const fromRoot = relative(root, candidate);
11
+ return fromRoot === "" || (fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot));
12
+ }
13
+ export const WORKTREES_DIRNAME = ".worktrees";
14
+ const BRANCH_NAMESPACE = "pi-subagent";
15
+ const NAME_COMPONENT_LIMIT = 24;
16
+ const NAME_LIMIT = 72;
17
+ export const READABLE_BRANCH_PATTERN = new RegExp(`^${BRANCH_NAMESPACE}/subagent-[a-z0-9](?:[a-z0-9-]{0,${NAME_LIMIT - 2}}[a-z0-9])?-[0-9a-f]{24}$`);
18
+ /** Keep saved opaque names eligible for exact recovery and cleanup. */
19
+ export const OWNED_BRANCH_PATTERN = new RegExp(`${READABLE_BRANCH_PATTERN.source}|^${BRANCH_NAMESPACE}/subagent-[0-9a-f]{24}$`);
20
+ export class WorktreeSetupError extends Error {
21
+ name = "WorktreeSetupError";
22
+ worktree;
23
+ constructor(message, worktree) {
24
+ super(message);
25
+ this.worktree = worktree;
26
+ }
27
+ }
28
+ const sanitizeShortId = (childId) => createHash("sha256").update(childId).digest("hex").slice(0, 24);
29
+ function nameSlug(parts) {
30
+ return parts.map((part) => part.toLowerCase().replace(/[^a-z0-9]+/g, "-")
31
+ .slice(0, NAME_COMPONENT_LIMIT).replace(/^-+|-+$/g, "") || "task")
32
+ .join("-").slice(0, NAME_LIMIT).replace(/-+$/, "") || "task";
33
+ }
34
+ const stripGitLineEnd = (value) => value.replace(/\r?\n$/, "");
35
+ function hasRepositoryMarker(cwd) {
36
+ for (let directory = resolve(cwd);; directory = dirname(directory)) {
37
+ try {
38
+ lstatSync(join(directory, ".git"));
39
+ return true;
40
+ }
41
+ catch (error) {
42
+ if (!error || typeof error !== "object" || !("code" in error) || !["ENOENT", "ENOTDIR"].includes(String(error.code)))
43
+ return true;
44
+ }
45
+ if (dirname(directory) === directory)
46
+ return false;
47
+ }
48
+ }
49
+ /** Keeps ".worktrees/" out of git status via the common repository's exclude file; never dirties any checkout. */
50
+ async function ensureLocalExclude(gitDir) {
51
+ const entry = `/${WORKTREES_DIRNAME}/`;
52
+ const exclude = join(gitDir, "info", "exclude");
53
+ try {
54
+ await mkdir(join(gitDir, "info"), { recursive: true });
55
+ let existing = "";
56
+ try {
57
+ existing = await readFile(exclude, "utf8");
58
+ }
59
+ catch (error) {
60
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT")
61
+ throw error;
62
+ }
63
+ if (existing.split("\n").some((line) => line.trim() === entry))
64
+ return;
65
+ await appendFile(exclude, `${existing && !existing.endsWith("\n") ? "\n" : ""}${entry}\n`);
66
+ }
67
+ catch (error) {
68
+ throw new Error(`Could not update ${exclude}: ${error instanceof Error ? error.message : String(error)}`);
69
+ }
70
+ }
71
+ /**
72
+ * Creates one worktree per child from parent HEAD. Returns undefined only when
73
+ * the workspace is not a git repository or HEAD is unborn — callers degrade
74
+ * silently to the shared working directory. Setup failures in a real git
75
+ * repository (unwritable path, stale branch, git lock) throw so an isolated
76
+ * role never silently loses its isolation.
77
+ */
78
+ export async function createChildWorktree(cwd, childId, run = runGit, signal, onPrepared, nameParts = [childId]) {
79
+ const root = await run(["rev-parse", "--show-toplevel"], cwd, signal);
80
+ signal?.throwIfAborted();
81
+ if (root.code === -1)
82
+ throw new Error(`git rev-parse failed (${root.stderr.trim().slice(0, 200)})`);
83
+ if (root.code !== 0) {
84
+ signal?.throwIfAborted();
85
+ const repository = await run(["-c", "safe.directory=*", "rev-parse", "--show-toplevel"], cwd, signal);
86
+ signal?.throwIfAborted();
87
+ if (repository.code === -1)
88
+ throw new Error(`git rev-parse failed (${repository.stderr.trim().slice(0, 200)})`);
89
+ if (repository.code !== 0 && !hasRepositoryMarker(cwd) && !process.env.GIT_DIR && !process.env.GIT_WORK_TREE)
90
+ return undefined;
91
+ throw new Error(`git rev-parse failed (${root.stderr.trim().slice(0, 200)})`); // dubious ownership, timeout, …
92
+ }
93
+ const repoRoot = stripGitLineEnd(root.stdout);
94
+ if (!repoRoot)
95
+ return undefined;
96
+ const prefix = await run(["rev-parse", "--show-prefix"], cwd, signal);
97
+ if (prefix.code !== 0)
98
+ throw new Error(`git rev-parse --show-prefix failed (${prefix.stderr.trim().slice(0, 200)})`);
99
+ const relativeCwd = stripGitLineEnd(prefix.stdout);
100
+ const base = await run(["rev-parse", "HEAD"], repoRoot, signal);
101
+ signal?.throwIfAborted();
102
+ if (base.code === -1)
103
+ throw new Error(`git rev-parse HEAD failed (${base.stderr.trim().slice(0, 200)})`);
104
+ if (base.code !== 0) {
105
+ signal?.throwIfAborted();
106
+ const head = await run(["symbolic-ref", "--quiet", "HEAD"], repoRoot, signal);
107
+ signal?.throwIfAborted();
108
+ const reference = stripGitLineEnd(head.stdout);
109
+ if (head.code === 0 && reference) {
110
+ const exists = await run(["show-ref", "--verify", "--quiet", reference], repoRoot, signal);
111
+ signal?.throwIfAborted();
112
+ if (exists.code === 1)
113
+ return undefined; // unborn HEAD
114
+ }
115
+ throw new Error(`git rev-parse HEAD failed (${base.stderr.trim().slice(0, 200)})`);
116
+ }
117
+ const baseCommit = base.stdout.trim();
118
+ const superproject = await run(["rev-parse", "--show-superproject-working-tree"], repoRoot, signal);
119
+ if (superproject.code !== 0) {
120
+ throw new Error(`git rev-parse --show-superproject-working-tree failed (${superproject.stderr.trim().slice(0, 200)})`);
121
+ }
122
+ if (stripGitLineEnd(superproject.stdout)) {
123
+ throw new Error("Worktree isolation is unavailable inside Git submodules because parent cleanup can remove their Git metadata.");
124
+ }
125
+ const common = await run(["rev-parse", "--git-common-dir"], repoRoot, signal);
126
+ const rawCommonGitDir = stripGitLineEnd(common.stdout);
127
+ if (common.code !== 0 || !rawCommonGitDir) {
128
+ throw new Error(`git rev-parse --git-common-dir failed (${common.stderr.trim().slice(0, 200)})`);
129
+ }
130
+ const current = await run(["rev-parse", "--git-dir"], repoRoot, signal);
131
+ const rawCurrentGitDir = stripGitLineEnd(current.stdout);
132
+ if (current.code !== 0 || !rawCurrentGitDir) {
133
+ throw new Error(`git rev-parse --git-dir failed (${current.stderr.trim().slice(0, 200)})`);
134
+ }
135
+ const gitDir = isAbsolute(rawCommonGitDir) ? rawCommonGitDir : join(repoRoot, rawCommonGitDir);
136
+ const currentGitDir = isAbsolute(rawCurrentGitDir) ? rawCurrentGitDir : join(repoRoot, rawCurrentGitDir);
137
+ let stableRepoRoot = repoRoot;
138
+ if (currentGitDir !== gitDir) {
139
+ const worktrees = await run(["worktree", "list", "--porcelain", "-z"], repoRoot, signal);
140
+ const primary = worktrees.stdout.split("\0", 1)[0];
141
+ if (worktrees.code !== 0 || !primary?.startsWith("worktree ") || primary.length === "worktree ".length) {
142
+ throw new Error(`git worktree list failed (${worktrees.stderr.trim().slice(0, 200)})`);
143
+ }
144
+ stableRepoRoot = primary.slice("worktree ".length);
145
+ }
146
+ signal?.throwIfAborted();
147
+ const worktreesRoot = join(stableRepoRoot, WORKTREES_DIRNAME);
148
+ const name = `subagent-${nameSlug(nameParts)}-${sanitizeShortId(childId)}`;
149
+ const branch = `${BRANCH_NAMESPACE}/${name}`;
150
+ const path = join(worktreesRoot, name);
151
+ const worktree = { path, cwd: join(path, relativeCwd), branch, repoRoot: stableRepoRoot, baseCommit };
152
+ if (onPrepared)
153
+ await onPrepared(worktree);
154
+ signal?.throwIfAborted();
155
+ try {
156
+ await mkdir(worktreesRoot, { recursive: true });
157
+ }
158
+ catch (error) {
159
+ throw new Error(`Could not create ${worktreesRoot}: ${error instanceof Error ? error.message : String(error)}`);
160
+ }
161
+ await ensureLocalExclude(gitDir);
162
+ signal?.throwIfAborted();
163
+ const added = await run(["worktree", "add", path, "-b", branch, baseCommit], repoRoot, signal);
164
+ if (added.code !== 0) {
165
+ 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);
166
+ }
167
+ return worktree;
168
+ }
169
+ /** Records only measurements that completed before recovery became necessary. */
170
+ function recoveryPayload(info, reason, measurements = {}, action = `Inspect ${info.path} (branch ${info.branch}) before assuming no work.`) {
171
+ const unknown = [
172
+ ...(measurements.commits === undefined ? ["commits"] : []),
173
+ ...(measurements.dirty === undefined ? ["dirty"] : []),
174
+ ].join("/");
175
+ return {
176
+ outcome: "recovery",
177
+ path: info.path,
178
+ branch: info.branch,
179
+ ...(measurements.commits === undefined ? {} : { commits: measurements.commits }),
180
+ ...(measurements.dirty === undefined ? {} : { dirty: measurements.dirty }),
181
+ note: `Worktree recovery required (${reason})${unknown ? `: ${unknown} UNKNOWN` : ""}. ${action}`,
182
+ };
183
+ }
184
+ export async function inspectIndexFlags(cwd, run = runGit, signal) {
185
+ const flags = await run(["ls-files", "-v", "-z"], cwd, signal);
186
+ if (flags.code !== 0)
187
+ return { hidden: false, failure: `ls-files exit ${flags.code}: ${flags.stderr.trim().slice(0, 200)}` };
188
+ return { hidden: flags.stdout.split("\0").some((entry) => /^(?:[a-z]|S) /.test(entry)) };
189
+ }
190
+ async function inspectDirty(run, cwd, includeIgnored = true) {
191
+ const refreshed = await run(["update-index", "--really-refresh"], cwd);
192
+ if (refreshed.code !== 0 && refreshed.code !== 1) {
193
+ return { dirty: false, failure: `update-index exit ${refreshed.code}: ${refreshed.stderr.trim().slice(0, 200)}` };
194
+ }
195
+ const status = await run(["status", "--porcelain", "--untracked-files=all", ...(includeIgnored ? ["--ignored=matching"] : []), "--ignore-submodules=none"], cwd);
196
+ if (status.code !== 0)
197
+ return { dirty: false, failure: `status exit ${status.code}: ${status.stderr.trim().slice(0, 200)}` };
198
+ if (refreshed.code === 1 || status.stdout.trim())
199
+ return { dirty: true };
200
+ const flags = await inspectIndexFlags(cwd, run);
201
+ if (flags.failure)
202
+ return { dirty: false, failure: flags.failure };
203
+ if (flags.hidden)
204
+ return { dirty: false, failure: "assume-unchanged or skip-worktree index entries remain" };
205
+ return { dirty: false };
206
+ }
207
+ /** Proves a worktree has no tracked, untracked, index-hidden, or nested submodule work; cleanup also checks ignored files. */
208
+ export async function inspectWorktreeDirty(cwd, run = runGit, includeIgnored = true) {
209
+ const root = await inspectDirty(run, cwd, includeIgnored);
210
+ if (root.dirty || root.failure)
211
+ return root;
212
+ const modules = await run(["submodule", "status", "--recursive"], cwd);
213
+ if (modules.code !== 0)
214
+ return { dirty: false, failure: `submodule list exit ${modules.code}: ${modules.stderr.trim().slice(0, 200)}` };
215
+ const initializedSubmodules = modules.stdout.split("\n").some((line) => line && !line.startsWith("-"));
216
+ if (initializedSubmodules) {
217
+ const listed = await run(["submodule", "foreach", "--recursive", "--quiet", "printf '%s\\0' \"$PWD\""], cwd);
218
+ if (listed.code !== 0)
219
+ return { dirty: false, failure: `submodule list exit ${listed.code}: ${listed.stderr.trim().slice(0, 200)}` };
220
+ const paths = listed.stdout.split("\0").filter(Boolean);
221
+ if (!paths.length)
222
+ return { dirty: false, failure: "initialized submodule paths unavailable" };
223
+ for (const path of paths) {
224
+ const nested = await inspectDirty(run, path, includeIgnored);
225
+ if (nested.failure)
226
+ return { dirty: false, failure: `submodule ${path}: ${nested.failure}` };
227
+ if (nested.dirty)
228
+ return { dirty: true };
229
+ }
230
+ }
231
+ const rechecked = await inspectDirty(run, cwd, includeIgnored);
232
+ return initializedSubmodules ? { ...rechecked, initializedSubmodules } : rechecked;
233
+ }
234
+ /**
235
+ * Inspects and possibly prunes a child worktree after it finishes. Commit count
236
+ * reads the dedicated branch and refuses to prune when checkout HEAD no longer
237
+ * names it; the clean-tree proof includes untracked, ignored, and submodule
238
+ * changes despite repository config. A worktree with zero branch commits and a
239
+ * clean tree is removed only when every probe succeeds and base_commit was
240
+ * recorded; any uncertainty preserves work and omits unknown measurements.
241
+ */
242
+ export async function finalizeChildWorktree(info, run = runGit) {
243
+ const checkoutExists = existsSync(info.path);
244
+ const gitCwd = checkoutExists ? info.path : info.repoRoot || info.path;
245
+ if (!info.baseCommit)
246
+ return recoveryPayload(info, "no base_commit recorded — commit count unmeasurable");
247
+ const counted = await run(["rev-list", "--count", `${info.baseCommit}..${info.branch}`], gitCwd);
248
+ const commits = Number.parseInt(counted.stdout.trim(), 10);
249
+ const countFailure = counted.code !== 0
250
+ ? `rev-list exit ${counted.code}: ${counted.stderr.trim().slice(0, 200)}`
251
+ : Number.isNaN(commits) ? "rev-list produced non-numeric output" : undefined;
252
+ const inspection = checkoutExists ? await inspectDirty(run, info.path, true) : undefined;
253
+ const measurements = {
254
+ ...(countFailure === undefined ? { commits } : {}),
255
+ ...(inspection?.failure === undefined && inspection !== undefined ? { dirty: inspection.dirty } : {}),
256
+ };
257
+ if (countFailure || inspection?.failure) {
258
+ return recoveryPayload(info, [countFailure, inspection?.failure].filter(Boolean).join("; "), measurements);
259
+ }
260
+ let forceRemove = false;
261
+ if (checkoutExists) {
262
+ const dirty = inspection.dirty;
263
+ if (commits > 0 || dirty)
264
+ return { outcome: "retained", path: info.path, branch: info.branch, commits, dirty };
265
+ const head = await run(["symbolic-ref", "--quiet", "HEAD"], info.path);
266
+ if (head.code !== 0 || head.stdout.trim() !== `refs/heads/${info.branch}`) {
267
+ return recoveryPayload(info, "HEAD is detached, switched, or unreadable", measurements);
268
+ }
269
+ const rechecked = await inspectWorktreeDirty(info.path, run);
270
+ if (rechecked.failure)
271
+ return recoveryPayload(info, `final ${rechecked.failure}`, { commits });
272
+ if (rechecked.dirty)
273
+ return { outcome: "retained", path: info.path, branch: info.branch, commits, dirty: true };
274
+ forceRemove = Boolean(rechecked.initializedSubmodules);
275
+ }
276
+ else if (commits > 0) {
277
+ return recoveryPayload(info, "worktree checkout is missing", { commits });
278
+ }
279
+ const cleanupCwd = info.repoRoot || info.path;
280
+ const removed = await run(["worktree", "remove", ...(forceRemove ? ["--force"] : []), info.path], cleanupCwd);
281
+ if (removed.code !== 0)
282
+ return recoveryPayload(info, `worktree remove exit ${removed.code}: ${removed.stderr.trim().slice(0, 200)}`);
283
+ const deleted = await run(["update-ref", "-d", `refs/heads/${info.branch}`, info.baseCommit], cleanupCwd);
284
+ if (deleted.code !== 0) {
285
+ return recoveryPayload(info, `branch delete exit ${deleted.code}: ${deleted.stderr.trim().slice(0, 200)}`, {}, `Inspect branch ${info.branch}; recreate ${info.path} from it before assuming no work.`);
286
+ }
287
+ return { outcome: "pruned", path: info.path, branch: info.branch };
288
+ }
@@ -0,0 +1,138 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Pi Subagent isolated workflow</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Geist:wght@400;500;600&amp;family=Geist+Mono:wght@400;500;600&amp;display=swap" rel="stylesheet">
8
+ <style>
9
+ * { box-sizing: border-box; }
10
+ body { margin: 0; padding: 32px; background: #f0eee9; color: #101828; font-family: 'Geist', sans-serif; }
11
+ .frame { width: 100%; max-width: 1280px; margin: auto; }
12
+ h1 { margin: 0 0 24px; font: 400 28px 'Instrument Serif', serif; }
13
+ .diagram-scroll { overflow-x: auto; }
14
+ svg { display: block; width: 100%; min-width: 1280px; height: auto; }
15
+ </style>
16
+ </head>
17
+ <body>
18
+ <main class="frame">
19
+ <h1>Stage ready candidates before all workers finish</h1>
20
+ <div class="diagram-scroll">
21
+ <svg width="1280" height="720" viewBox="0 0 1280 720" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="orchestration-architecture-title orchestration-architecture-desc">
22
+ <title id="orchestration-architecture-title">Pi Subagent isolated workflow</title>
23
+ <desc id="orchestration-architecture-desc">Workers commit checked candidates for Main to stage while siblings run, advance dependent waves, validate the combined tip, and explicitly promote into unchanged clean Main.</desc>
24
+ <defs>
25
+ <style>
26
+ .section { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; letter-spacing: .12em; }
27
+ .node { fill: #ffffff; stroke: #101828; stroke-width: 1; }
28
+ .input { fill: #e6e4de; stroke: #6a7282; stroke-width: 1; }
29
+ .focal { fill: #1d4ed8; fill-opacity: .08; stroke: #1d4ed8; stroke-width: 1.2; }
30
+ .optional { fill: #101828; fill-opacity: .02; stroke: #4c5665; stroke-width: 1; stroke-dasharray: 4 3; }
31
+ .tag { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; }
32
+ .name { fill: #101828; font: 600 12px 'Geist', sans-serif; text-anchor: middle; }
33
+ .detail { fill: #4c5665; font: 400 12px 'Geist', sans-serif; text-anchor: middle; }
34
+ .code { fill: #4c5665; font: 400 8px 'Geist Mono', monospace; text-anchor: middle; }
35
+ .edge { fill: none; stroke: #4c5665; stroke-width: 1.2; marker-end: url(#arrow); }
36
+ .branch { fill: none; stroke: #4c5665; stroke-width: 1; stroke-dasharray: 5 4; marker-end: url(#arrow); }
37
+ .approved { fill: none; stroke: #1d4ed8; stroke-width: 1.2; marker-end: url(#arrow-accent); }
38
+ .label-mask { fill: #f0eee9; }
39
+ .label { fill: #4c5665; font: 500 8px 'Geist Mono', monospace; text-anchor: middle; }
40
+ .note-title { fill: #101828; font: 600 12px 'Geist', sans-serif; }
41
+ .note { fill: #4c5665; font: 400 12px 'Geist', sans-serif; }
42
+ .key { fill: #4c5665; font: 400 12px 'Geist', sans-serif; }
43
+ </style>
44
+ <marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
45
+ <polygon points="0 0,8 3,0 6" fill="#4c5665"/>
46
+ </marker>
47
+ <marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
48
+ <polygon points="0 0,8 3,0 6" fill="#1d4ed8"/>
49
+ </marker>
50
+ </defs>
51
+ <rect width="1280" height="720" fill="#f0eee9"/>
52
+ <text class="section" x="40" y="120">CANDIDATE PRODUCTION</text>
53
+ <text class="section" x="40" y="320">MAIN'S EXPLICIT ACTIONS</text>
54
+
55
+ <!-- Draw connectors and their labels before nodes. -->
56
+ <line class="edge" x1="256" y1="208" x2="304" y2="208"/>
57
+ <line class="edge" x1="560" y1="208" x2="608" y2="208"/>
58
+ <line class="edge" x1="880" y1="208" x2="928" y2="208"/>
59
+ <line class="edge" x1="1064" y1="272" x2="1064" y2="344"/>
60
+ <line class="edge" x1="928" y1="408" x2="880" y2="408"/>
61
+ <line class="approved" x1="568" y1="408" x2="472" y2="408"/>
62
+ <line class="branch" x1="1064" y1="472" x2="1064" y2="520"/>
63
+ <path class="branch" d="M1200 564 H1224 Q1232 564 1232 556 V96 Q1232 88 1224 88 H440 Q432 88 432 96 V144"/>
64
+ <rect class="label-mask" x="884" y="388" width="40" height="12" rx="2"/>
65
+ <text class="label" x="904" y="396">SETTLED</text>
66
+ <rect class="label-mask" x="500" y="388" width="40" height="12" rx="2"/>
67
+ <text class="label" x="520" y="396">PASS</text>
68
+ <rect class="label-mask" x="1072" y="488" width="72" height="12" rx="2"/>
69
+ <text class="label" x="1108" y="496">DEPENDENTS</text>
70
+ <rect class="label-mask" x="716" y="68" width="80" height="12" rx="2"/>
71
+ <text class="label" x="756" y="76">STAGED BASE</text>
72
+
73
+ <rect class="input" x="40" y="144" width="216" height="128" rx="6"/>
74
+ <text class="tag" x="56" y="168">MAIN</text>
75
+ <text class="name" x="148" y="200">Start isolated request</text>
76
+ <text class="code" x="148" y="224">delegate_task · mode: isolated</text>
77
+ <text class="detail" x="148" y="248">Keep the durable request ID.</text>
78
+
79
+ <rect class="node" x="304" y="144" width="256" height="128" rx="6"/>
80
+ <text class="tag" x="320" y="168">WORKERS</text>
81
+ <text class="name" x="432" y="200">Commit candidates</text>
82
+ <text class="detail" x="432" y="224">Use owned worktrees.</text>
83
+ <text class="detail" x="432" y="248">No tracked or untracked changes.</text>
84
+
85
+ <rect class="node" x="608" y="144" width="272" height="128" rx="6"/>
86
+ <text class="tag" x="624" y="168">RUNNER</text>
87
+ <text class="name" x="744" y="200">Run preliminary checks</text>
88
+ <text class="detail" x="744" y="224">Include optional task judgment.</text>
89
+ <text class="detail" x="744" y="248">Bind evidence to the exact candidate.</text>
90
+
91
+ <rect class="node" x="928" y="144" width="272" height="128" rx="6"/>
92
+ <text class="tag" x="944" y="168">RUNNER → MAIN</text>
93
+ <text class="name" x="1064" y="200">Send ready follow-up</text>
94
+ <text class="detail" x="1064" y="224">Readiness is not promotion.</text>
95
+ <text class="code" x="1064" y="248">subagent_status</text>
96
+
97
+ <rect class="focal" x="928" y="344" width="272" height="128" rx="6"/>
98
+ <text class="tag" x="944" y="368">MAIN · INTEGRATION CHECKOUT</text>
99
+ <text class="name" x="1064" y="400">Stage exact candidates</text>
100
+ <text class="code" x="1064" y="424">subagent_stage</text>
101
+ <text class="detail" x="1064" y="448">Stage while siblings run.</text>
102
+
103
+ <rect class="node" x="568" y="344" width="312" height="128" rx="6"/>
104
+ <text class="tag" x="584" y="368">MAIN · AFTER THE WAVE SETTLES</text>
105
+ <text class="name" x="724" y="400">Validate the exact combined tip</text>
106
+ <text class="code" x="724" y="424">subagent_integrate validate</text>
107
+ <text class="detail" x="724" y="448">Final checks and optional final judgment</text>
108
+
109
+ <rect class="focal" x="160" y="344" width="312" height="128" rx="6"/>
110
+ <text class="tag" x="176" y="368">MAIN · EXPLICIT PROMOTION</text>
111
+ <text class="name" x="316" y="400">Promote passing work</text>
112
+ <text class="code" x="316" y="424">subagent_integrate promote</text>
113
+ <text class="detail" x="316" y="448">Main must be unchanged and clean.</text>
114
+
115
+ <rect class="optional" x="928" y="520" width="272" height="88" rx="6"/>
116
+ <text class="name" x="1064" y="544">Advance dependent tasks</text>
117
+ <text class="code" x="1064" y="564">subagent_integrate advance</text>
118
+ <text class="detail" x="1064" y="588">Run from the selected staged snapshot.</text>
119
+
120
+ <!-- Safety details remain in the README as text. -->
121
+ <text class="note-title" x="40" y="536">Blocked promotion preserves evidence.</text>
122
+ <text class="note" x="40" y="560">Failures, conflicts, changed identities, or uncertain</text>
123
+ <text class="note" x="40" y="580">outcomes require inspection, not replay.</text>
124
+ <text class="note-title" x="520" y="536">One combined correction</text>
125
+ <text class="note" x="520" y="560">After a definitive combined check or review failure,</text>
126
+ <text class="note" x="520" y="580">commit one correction, record it, then validate again.</text>
127
+
128
+ <line x1="40" y1="640" x2="1240" y2="640" stroke="#101828" stroke-opacity=".141"/>
129
+ <line class="edge" x1="40" y1="672" x2="88" y2="672"/>
130
+ <text class="key" x="104" y="676">Checked sequence</text>
131
+ <line class="branch" x1="304" y1="672" x2="352" y2="672"/>
132
+ <text class="key" x="368" y="676">Dependent wave, if needed</text>
133
+ <text class="key" x="704" y="676">Staging, validation, and promotion require exact, clean Main.</text>
134
+ </svg>
135
+ </div>
136
+ </main>
137
+ </body>
138
+ </html>
@@ -0,0 +1,135 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="1280" height="720" viewBox="0 0 1280 720" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="orchestration-architecture-title orchestration-architecture-desc" id="orchestration-architecture-root">
3
+ <title id="orchestration-architecture-title">Pi Subagent isolated workflow</title>
4
+ <desc id="orchestration-architecture-desc">Workers commit checked candidates for Main to stage while siblings run, advance dependent waves, validate the combined tip, and explicitly promote into unchanged clean Main.</desc>
5
+ <defs>
6
+ <style>@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Geist:wght@400;500;600&amp;family=Geist+Mono:wght@400;500;600&amp;family=Noto+Serif:ital@0;1&amp;family=Noto+Sans+KR:wght@400;500;600&amp;family=Noto+Serif+KR:wght@400&amp;family=Noto+Sans+TC:wght@400;500;600&amp;family=Noto+Serif+TC:wght@400&amp;display=swap');
7
+ #orchestration-architecture-root { color: #101828; font-family: 'Geist', sans-serif; }
8
+ #orchestration-architecture-root .diagram-scroll { overflow-x: auto; }
9
+ #orchestration-architecture-root .section { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; letter-spacing: .12em; }
10
+ #orchestration-architecture-root .node { fill: #ffffff; stroke: #101828; stroke-width: 1; }
11
+ #orchestration-architecture-root .input { fill: #e6e4de; stroke: #6a7282; stroke-width: 1; }
12
+ #orchestration-architecture-root .focal { fill: #1d4ed8; fill-opacity: .08; stroke: #1d4ed8; stroke-width: 1.2; }
13
+ #orchestration-architecture-root .optional { fill: #101828; fill-opacity: .02; stroke: #4c5665; stroke-width: 1; stroke-dasharray: 4 3; }
14
+ #orchestration-architecture-root .tag { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; }
15
+ #orchestration-architecture-root .name { fill: #101828; font: 600 12px 'Geist', sans-serif; text-anchor: middle; }
16
+ #orchestration-architecture-root .detail { fill: #4c5665; font: 400 12px 'Geist', sans-serif; text-anchor: middle; }
17
+ #orchestration-architecture-root .code { fill: #4c5665; font: 400 8px 'Geist Mono', monospace; text-anchor: middle; }
18
+ #orchestration-architecture-root .edge { fill: none; stroke: #4c5665; stroke-width: 1.2; marker-end: url(#orchestration-architecture-arrow); }
19
+ #orchestration-architecture-root .branch { fill: none; stroke: #4c5665; stroke-width: 1; stroke-dasharray: 5 4; marker-end: url(#orchestration-architecture-arrow); }
20
+ #orchestration-architecture-root .approved { fill: none; stroke: #1d4ed8; stroke-width: 1.2; marker-end: url(#orchestration-architecture-arrow-accent); }
21
+ #orchestration-architecture-root .label-mask { fill: #f0eee9; }
22
+ #orchestration-architecture-root .label { fill: #4c5665; font: 500 8px 'Geist Mono', monospace; text-anchor: middle; }
23
+ #orchestration-architecture-root .note-title { fill: #101828; font: 600 12px 'Geist', sans-serif; }
24
+ #orchestration-architecture-root .note { fill: #4c5665; font: 400 12px 'Geist', sans-serif; }
25
+ #orchestration-architecture-root .key { fill: #4c5665; font: 400 12px 'Geist', sans-serif; }</style>
26
+ <style>
27
+ .section { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; letter-spacing: .12em; }
28
+ .node { fill: #ffffff; stroke: #101828; stroke-width: 1; }
29
+ .input { fill: #e6e4de; stroke: #6a7282; stroke-width: 1; }
30
+ .focal { fill: #1d4ed8; fill-opacity: .08; stroke: #1d4ed8; stroke-width: 1.2; }
31
+ .optional { fill: #101828; fill-opacity: .02; stroke: #4c5665; stroke-width: 1; stroke-dasharray: 4 3; }
32
+ .tag { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; }
33
+ .name { fill: #101828; font: 600 12px 'Geist', sans-serif; text-anchor: middle; }
34
+ .detail { fill: #4c5665; font: 400 12px 'Geist', sans-serif; text-anchor: middle; }
35
+ .code { fill: #4c5665; font: 400 8px 'Geist Mono', monospace; text-anchor: middle; }
36
+ .edge { fill: none; stroke: #4c5665; stroke-width: 1.2; marker-end: url(#orchestration-architecture-arrow); }
37
+ .branch { fill: none; stroke: #4c5665; stroke-width: 1; stroke-dasharray: 5 4; marker-end: url(#orchestration-architecture-arrow); }
38
+ .approved { fill: none; stroke: #1d4ed8; stroke-width: 1.2; marker-end: url(#orchestration-architecture-arrow-accent); }
39
+ .label-mask { fill: #f0eee9; }
40
+ .label { fill: #4c5665; font: 500 8px 'Geist Mono', monospace; text-anchor: middle; }
41
+ .note-title { fill: #101828; font: 600 12px 'Geist', sans-serif; }
42
+ .note { fill: #4c5665; font: 400 12px 'Geist', sans-serif; }
43
+ .key { fill: #4c5665; font: 400 12px 'Geist', sans-serif; }
44
+ </style>
45
+ <marker id="orchestration-architecture-arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
46
+ <polygon points="0 0,8 3,0 6" fill="#4c5665"/>
47
+ </marker>
48
+ <marker id="orchestration-architecture-arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
49
+ <polygon points="0 0,8 3,0 6" fill="#1d4ed8"/>
50
+ </marker>
51
+ </defs>
52
+ <rect width="1280" height="720" fill="#f0eee9"/>
53
+ <text class="section" x="40" y="120">CANDIDATE PRODUCTION</text>
54
+ <text class="section" x="40" y="320">MAIN'S EXPLICIT ACTIONS</text>
55
+
56
+ <!-- Draw connectors and their labels before nodes. -->
57
+ <line class="edge" x1="256" y1="208" x2="304" y2="208"/>
58
+ <line class="edge" x1="560" y1="208" x2="608" y2="208"/>
59
+ <line class="edge" x1="880" y1="208" x2="928" y2="208"/>
60
+ <line class="edge" x1="1064" y1="272" x2="1064" y2="344"/>
61
+ <line class="edge" x1="928" y1="408" x2="880" y2="408"/>
62
+ <line class="approved" x1="568" y1="408" x2="472" y2="408"/>
63
+ <line class="branch" x1="1064" y1="472" x2="1064" y2="520"/>
64
+ <path class="branch" d="M1200 564 H1224 Q1232 564 1232 556 V96 Q1232 88 1224 88 H440 Q432 88 432 96 V144"/>
65
+ <rect class="label-mask" x="884" y="388" width="40" height="12" rx="2"/>
66
+ <text class="label" x="904" y="396">SETTLED</text>
67
+ <rect class="label-mask" x="500" y="388" width="40" height="12" rx="2"/>
68
+ <text class="label" x="520" y="396">PASS</text>
69
+ <rect class="label-mask" x="1072" y="488" width="72" height="12" rx="2"/>
70
+ <text class="label" x="1108" y="496">DEPENDENTS</text>
71
+ <rect class="label-mask" x="716" y="68" width="80" height="12" rx="2"/>
72
+ <text class="label" x="756" y="76">STAGED BASE</text>
73
+
74
+ <rect class="input" x="40" y="144" width="216" height="128" rx="6"/>
75
+ <text class="tag" x="56" y="168">MAIN</text>
76
+ <text class="name" x="148" y="200">Start isolated request</text>
77
+ <text class="code" x="148" y="224">delegate_task · mode: isolated</text>
78
+ <text class="detail" x="148" y="248">Keep the durable request ID.</text>
79
+
80
+ <rect class="node" x="304" y="144" width="256" height="128" rx="6"/>
81
+ <text class="tag" x="320" y="168">WORKERS</text>
82
+ <text class="name" x="432" y="200">Commit candidates</text>
83
+ <text class="detail" x="432" y="224">Use owned worktrees.</text>
84
+ <text class="detail" x="432" y="248">No tracked or untracked changes.</text>
85
+
86
+ <rect class="node" x="608" y="144" width="272" height="128" rx="6"/>
87
+ <text class="tag" x="624" y="168">RUNNER</text>
88
+ <text class="name" x="744" y="200">Run preliminary checks</text>
89
+ <text class="detail" x="744" y="224">Include optional task judgment.</text>
90
+ <text class="detail" x="744" y="248">Bind evidence to the exact candidate.</text>
91
+
92
+ <rect class="node" x="928" y="144" width="272" height="128" rx="6"/>
93
+ <text class="tag" x="944" y="168">RUNNER → MAIN</text>
94
+ <text class="name" x="1064" y="200">Send ready follow-up</text>
95
+ <text class="detail" x="1064" y="224">Readiness is not promotion.</text>
96
+ <text class="code" x="1064" y="248">subagent_status</text>
97
+
98
+ <rect class="focal" x="928" y="344" width="272" height="128" rx="6"/>
99
+ <text class="tag" x="944" y="368">MAIN · INTEGRATION CHECKOUT</text>
100
+ <text class="name" x="1064" y="400">Stage exact candidates</text>
101
+ <text class="code" x="1064" y="424">subagent_stage</text>
102
+ <text class="detail" x="1064" y="448">Stage while siblings run.</text>
103
+
104
+ <rect class="node" x="568" y="344" width="312" height="128" rx="6"/>
105
+ <text class="tag" x="584" y="368">MAIN · AFTER THE WAVE SETTLES</text>
106
+ <text class="name" x="724" y="400">Validate the exact combined tip</text>
107
+ <text class="code" x="724" y="424">subagent_integrate validate</text>
108
+ <text class="detail" x="724" y="448">Final checks and optional final judgment</text>
109
+
110
+ <rect class="focal" x="160" y="344" width="312" height="128" rx="6"/>
111
+ <text class="tag" x="176" y="368">MAIN · EXPLICIT PROMOTION</text>
112
+ <text class="name" x="316" y="400">Promote passing work</text>
113
+ <text class="code" x="316" y="424">subagent_integrate promote</text>
114
+ <text class="detail" x="316" y="448">Main must be unchanged and clean.</text>
115
+
116
+ <rect class="optional" x="928" y="520" width="272" height="88" rx="6"/>
117
+ <text class="name" x="1064" y="544">Advance dependent tasks</text>
118
+ <text class="code" x="1064" y="564">subagent_integrate advance</text>
119
+ <text class="detail" x="1064" y="588">Run from the selected staged snapshot.</text>
120
+
121
+ <!-- Safety details remain in the README as text. -->
122
+ <text class="note-title" x="40" y="536">Blocked promotion preserves evidence.</text>
123
+ <text class="note" x="40" y="560">Failures, conflicts, changed identities, or uncertain</text>
124
+ <text class="note" x="40" y="580">outcomes require inspection, not replay.</text>
125
+ <text class="note-title" x="520" y="536">One combined correction</text>
126
+ <text class="note" x="520" y="560">After a definitive combined check or review failure,</text>
127
+ <text class="note" x="520" y="580">commit one correction, record it, then validate again.</text>
128
+
129
+ <line x1="40" y1="640" x2="1240" y2="640" stroke="#101828" stroke-opacity=".141"/>
130
+ <line class="edge" x1="40" y1="672" x2="88" y2="672"/>
131
+ <text class="key" x="104" y="676">Checked sequence</text>
132
+ <line class="branch" x1="304" y1="672" x2="352" y2="672"/>
133
+ <text class="key" x="368" y="676">Dependent wave, if needed</text>
134
+ <text class="key" x="704" y="676">Staging, validation, and promotion require exact, clean Main.</text>
135
+ </svg>