@henryqw/pi-subagent 22.0.1 → 22.2.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/README.md +15 -8
- package/dist/direct-herdr.d.ts +2 -0
- package/dist/direct-herdr.js +49 -0
- package/dist/git-runtime.js +23 -22
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/integration-git.js +4 -2
- package/dist/launch-runtime.d.ts +1 -1
- package/dist/launch-runtime.js +10 -10
- package/dist/runner.d.ts +7 -0
- package/dist/runner.js +87 -13
- package/dist/store.d.ts +3 -0
- package/dist/store.js +8 -3
- package/dist/worktree.d.ts +1 -1
- package/dist/worktree.js +6 -6
- package/docs/worker-lifecycle.html +92 -0
- package/docs/worker-lifecycle.svg +80 -0
- package/extensions/isolated.ts +95 -9
- package/extensions/result-transport.ts +1 -5
- package/extensions/role-tools.ts +8 -2
- package/extensions/subagent-command.ts +15 -6
- package/extensions/subagent.ts +94 -43
- package/extensions/workflow.ts +18 -6
- package/package.json +1 -1
- package/skills/pi-subagent/SKILL.md +3 -3
- package/docs/delegation-routing.html +0 -89
- package/docs/delegation-routing.svg +0 -78
- package/extensions/task-name.ts +0 -28
package/extensions/subagent.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { type Component, Text, type TUI, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { type Component, Text, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
4
|
import { availableTaskModels, loadTaskModelsConfig, modelReference, registerModelTask, resolveAvailableModel, type ResolvedTaskRoute, taskThinkingLevels } from "@henryqw/pi-task-models";
|
|
5
5
|
import { capEphemeralSubagentOutput as capOutput, createEphemeralSubagentExecutor, DELEGATE_TASK, formatDuration, loadRoles, prepareRoleLaunch, ROLE_TOOL_POLICY_FLAG, type EphemeralSubagentTimeout, type Role } from "@henryqw/pi-subagent";
|
|
6
6
|
import { DEFAULT_EXECUTION_POLICY, DEFAULT_TIMEOUT_CONFIG, readSubagentConfig, resolveExecutionPolicy, type EffectiveExecutionPolicy } from "./config.ts";
|
|
@@ -13,10 +13,9 @@ import { DelegateTaskSchema, identifyWorkflowEntries, parseDelegateTask, runFore
|
|
|
13
13
|
import { createDirectHerdr, type DirectHandle, type DirectTab } from "../dist/direct-herdr.js";
|
|
14
14
|
import { materializeTransientLaunch } from "../dist/launch-runtime.js";
|
|
15
15
|
const WIDGET_KEY = "subagent-status";
|
|
16
|
-
const WIDGET_INTERVAL_MS =
|
|
16
|
+
const WIDGET_INTERVAL_MS = 1_000;
|
|
17
17
|
const MAX_WIDGET_ITEMS = 8;
|
|
18
18
|
const MAX_WIDGET_LINES = 6;
|
|
19
|
-
const MAX_WIDGET_GROUP_ROWS = 3;
|
|
20
19
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
21
20
|
|
|
22
21
|
type TimeoutPolicy = EphemeralSubagentTimeout;
|
|
@@ -30,6 +29,7 @@ type WidgetItem = {
|
|
|
30
29
|
startedAt: number;
|
|
31
30
|
status: WidgetStatus;
|
|
32
31
|
finishedAt?: number;
|
|
32
|
+
tokens?: number;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
function roleBadge(role: string): string {
|
|
@@ -55,15 +55,8 @@ function statusLabel(status: WidgetStatus): string {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
if (item.status === "failure") return "Failed";
|
|
61
|
-
if (item.status === "aborted") return "Stopped";
|
|
62
|
-
return "working…";
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function activityMetrics(item: WidgetItem, now: number): string {
|
|
66
|
-
return `${item.model}·${item.thinkingLevel} · ${formatDuration((item.finishedAt ?? now) - item.startedAt)}`;
|
|
58
|
+
function tokenLabel(tokens: number | undefined): string {
|
|
59
|
+
return tokens === undefined ? "— tok" : `${tokens < 1_000 ? tokens : `${(tokens / 1_000).toFixed(1).replace(/\.0$/, "")}k`} tok`;
|
|
67
60
|
}
|
|
68
61
|
|
|
69
62
|
function renderWidgetRows(
|
|
@@ -73,36 +66,26 @@ function renderWidgetRows(
|
|
|
73
66
|
spinnerIndex: number,
|
|
74
67
|
theme: Theme,
|
|
75
68
|
): string[] {
|
|
76
|
-
const ordered = [...items.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
for (const group of groups.values()) {
|
|
91
|
-
const working = group.items.some(({ status }) => status === "working");
|
|
92
|
-
if (working && !visibleWorkingGroups.has(group)) continue;
|
|
93
|
-
const reservedLines = working ? --remainingWorkingGroups * 2 : 0;
|
|
94
|
-
const childCount = Math.min(MAX_WIDGET_GROUP_ROWS, group.items.length, maxVisibleLines - lines.length - reservedLines - 1);
|
|
95
|
-
if (childCount < 1) continue;
|
|
96
|
-
lines.push(truncateToWidth(theme.fg("text", group.name), width));
|
|
97
|
-
for (const item of group.items.slice(0, childCount)) {
|
|
98
|
-
visible.add(item);
|
|
99
|
-
lines.push(truncateToWidth(
|
|
100
|
-
` ${statusGlyph(item.status, spinnerIndex, theme)} ${theme.fg("accent", item.role)} ${theme.fg("text", activityLabel(item))} · ${theme.fg("muted", activityMetrics(item, now))}`,
|
|
101
|
-
width,
|
|
102
|
-
));
|
|
69
|
+
const ordered = [...items].sort((a, b) =>
|
|
70
|
+
(a.status === "failure" ? 0 : a.status === "working" ? 1 : 2)
|
|
71
|
+
- (b.status === "failure" ? 0 : b.status === "working" ? 1 : 2));
|
|
72
|
+
const visible = ordered.slice(0, ordered.length > MAX_WIDGET_LINES ? MAX_WIDGET_LINES - 1 : MAX_WIDGET_LINES);
|
|
73
|
+
const lines = visible.map((item) => {
|
|
74
|
+
const prefix = `${statusGlyph(item.status, spinnerIndex, theme)} ${theme.fg("accent", `D ${item.role}`)} ${item.status === "working" ? "" : `${statusLabel(item.status)} · `}`;
|
|
75
|
+
const metrics = [
|
|
76
|
+
`${item.model}/${item.thinkingLevel}`,
|
|
77
|
+
tokenLabel(item.tokens),
|
|
78
|
+
formatDuration((item.finishedAt ?? now) - item.startedAt),
|
|
79
|
+
];
|
|
80
|
+
while (metrics.length && width - visibleWidth(prefix) - visibleWidth(` · ${metrics.join(" · ")}`) < 8) {
|
|
81
|
+
if (metrics.length === 2) metrics.shift(); // Preserve measured tokens ahead of route details.
|
|
82
|
+
else metrics.pop();
|
|
103
83
|
}
|
|
104
|
-
|
|
105
|
-
|
|
84
|
+
const suffix = metrics.length ? ` · ${theme.fg("muted", metrics.join(" · "))}` : "";
|
|
85
|
+
const name = truncateToWidth(theme.fg("text", item.name), Math.max(0, width - visibleWidth(prefix) - visibleWidth(suffix)));
|
|
86
|
+
return truncateToWidth(`${prefix}${name}${suffix}`, width);
|
|
87
|
+
});
|
|
88
|
+
const hidden = ordered.slice(visible.length);
|
|
106
89
|
if (hidden.length) {
|
|
107
90
|
const counts: Record<WidgetStatus, number> = { working: 0, success: 0, failure: 0, aborted: 0 };
|
|
108
91
|
for (const { status } of hidden) counts[status] += 1;
|
|
@@ -177,6 +160,8 @@ export default function subagentExtension(
|
|
|
177
160
|
].join("\n"), outputPad, 0);
|
|
178
161
|
});
|
|
179
162
|
const widgetItems = new Map<string, WidgetItem>();
|
|
163
|
+
const tokenHandles = new Map<string, DirectHandle>();
|
|
164
|
+
const samplingTokens = new Set<string>();
|
|
180
165
|
const loadedConfig = readSubagentConfig();
|
|
181
166
|
let initialPolicy: EffectiveExecutionPolicy;
|
|
182
167
|
try {
|
|
@@ -243,7 +228,51 @@ export default function subagentExtension(
|
|
|
243
228
|
return [...grouped.values()];
|
|
244
229
|
},
|
|
245
230
|
isolated: (cwd) => isolatedSurface.inventory(cwd),
|
|
246
|
-
|
|
231
|
+
recover: (cwd) => isolatedSurface.recover(cwd),
|
|
232
|
+
async inspectInTab(root, id, ctx, current) {
|
|
233
|
+
const notices = await isolatedSurface.inspect(root, id);
|
|
234
|
+
if (!current()) throw new Error("Session or branch changed before status inspection.");
|
|
235
|
+
const snapshot = notices.join("\n");
|
|
236
|
+
if (Buffer.byteLength(snapshot, "utf8") > 32_000) throw new Error("Status exceeds the 32 KiB inspection limit; use subagent_status for exact evidence.");
|
|
237
|
+
const role = loadRoles().find((candidate) => candidate.name === "scout");
|
|
238
|
+
if (!role || roleCanWrite(role)) throw new Error("Status inspection requires a configured read-only scout Role.");
|
|
239
|
+
const prepared = prepareRoleLaunch(pi, ctx, { role, task: DELEGATE_TASK, modelClass: "fast" });
|
|
240
|
+
const herdr = createDirectHerdr(pi, ctx.cwd, currentPolicy().childIdleMs);
|
|
241
|
+
const controller = new AbortController();
|
|
242
|
+
const taskId = `inspect-${id}-${randomUUID()}`;
|
|
243
|
+
const handles: DirectHandle[] = [];
|
|
244
|
+
const tabs: DirectTabRecord[] = [];
|
|
245
|
+
let resolveSettled!: () => void;
|
|
246
|
+
const settled = new Promise<void>((resolve) => { resolveSettled = resolve; });
|
|
247
|
+
directTasks.set(taskId, { controller, settled, handles, tabs });
|
|
248
|
+
try {
|
|
249
|
+
const transient = await materializeTransientLaunch({ launch: prepared, prompt: prepared.systemPrompt,
|
|
250
|
+
promptArgIndex: prepared.promptArgIndex }, controller.signal);
|
|
251
|
+
let handle: DirectHandle;
|
|
252
|
+
try {
|
|
253
|
+
handle = await herdr.start(
|
|
254
|
+
{ ...prepared, args: [...transient.launch.args] }, `i-${randomUUID().replaceAll("-", "").slice(0, 24)}`,
|
|
255
|
+
`Inspect ${id}`, `Analyze this saved, read-only status snapshot for request ${id}. Summarize blockers and safe next decisions; do not infer missing evidence or mutate resources. The subagent_status tool in Main remains authoritative for exact action identities.\n\n${snapshot}`,
|
|
256
|
+
controller.signal, (tab) => {
|
|
257
|
+
const record = { taskId, entryId: id, ...tab };
|
|
258
|
+
pi.appendEntry(DIRECT_TAB_TYPE, record);
|
|
259
|
+
tabs.push(record);
|
|
260
|
+
});
|
|
261
|
+
} catch (error) {
|
|
262
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}. Inspection Role prompt retained at ${transient.launch.args[prepared.promptArgIndex + 1]} after uncertain start.`, { cause: error });
|
|
263
|
+
}
|
|
264
|
+
handles.push(handle);
|
|
265
|
+
await transient.cleanup();
|
|
266
|
+
if (!current() || controller.signal.aborted) {
|
|
267
|
+
await handle.cancel();
|
|
268
|
+
throw new Error(`Session changed during inspection launch; inspect Herdr tab ${handle.tabId}.`);
|
|
269
|
+
}
|
|
270
|
+
return { tabId: handle.tabId, name: handle.name, sessionFile: handle.sessionFile };
|
|
271
|
+
} finally {
|
|
272
|
+
directTasks.delete(taskId);
|
|
273
|
+
resolveSettled();
|
|
274
|
+
}
|
|
275
|
+
},
|
|
247
276
|
canFollowup: (root, id, task) => isolatedSurface.canFollowup(root, id, task),
|
|
248
277
|
enqueue: (root, id, task, text, current) => isolatedSurface.enqueue(root, id, task, text, current),
|
|
249
278
|
drain: (root, id, task, current) => isolatedSurface.drain(root, id, task, current),
|
|
@@ -252,6 +281,7 @@ export default function subagentExtension(
|
|
|
252
281
|
let widgetInstalled = false;
|
|
253
282
|
let widgetTimer: ReturnType<typeof setInterval> | undefined;
|
|
254
283
|
let spinnerIndex = 0;
|
|
284
|
+
let widgetTicks = 0;
|
|
255
285
|
let activeTui: TUI | undefined;
|
|
256
286
|
const stopWidgetTimer = () => {
|
|
257
287
|
if (!widgetTimer) return;
|
|
@@ -266,6 +296,19 @@ export default function subagentExtension(
|
|
|
266
296
|
widgetTimer = setInterval(() => {
|
|
267
297
|
spinnerIndex = (spinnerIndex + 1) % SPINNER_FRAMES.length;
|
|
268
298
|
requestWidgetRender();
|
|
299
|
+
if (++widgetTicks % 2 !== 0) return;
|
|
300
|
+
for (const [id, handle] of tokenHandles) {
|
|
301
|
+
if (samplingTokens.has(id)) continue;
|
|
302
|
+
samplingTokens.add(id);
|
|
303
|
+
void handle.usageTokens().then((tokens) => {
|
|
304
|
+
const item = widgetItems.get(id);
|
|
305
|
+
if (tokens !== undefined && item?.status === "working" && tokenHandles.get(id) === handle) {
|
|
306
|
+
item.tokens = tokens;
|
|
307
|
+
requestWidgetRender();
|
|
308
|
+
}
|
|
309
|
+
}).catch(() => { /* Usage is optional; never interrupt work for telemetry. */ })
|
|
310
|
+
.finally(() => samplingTokens.delete(id));
|
|
311
|
+
}
|
|
269
312
|
}, WIDGET_INTERVAL_MS);
|
|
270
313
|
widgetTimer.unref();
|
|
271
314
|
};
|
|
@@ -316,6 +359,7 @@ export default function subagentExtension(
|
|
|
316
359
|
const finishWidgetItem = (id: string, status: Exclude<WidgetStatus, "working">) => {
|
|
317
360
|
const item = widgetItems.get(id);
|
|
318
361
|
if (!item) return;
|
|
362
|
+
tokenHandles.delete(id);
|
|
319
363
|
item.status = status;
|
|
320
364
|
item.finishedAt = Date.now();
|
|
321
365
|
if (![...widgetItems.values()].some(({ status }) => status === "working")) stopWidgetTimer();
|
|
@@ -357,6 +401,8 @@ export default function subagentExtension(
|
|
|
357
401
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
358
402
|
stopWidgetTimer();
|
|
359
403
|
widgetItems.clear();
|
|
404
|
+
tokenHandles.clear();
|
|
405
|
+
samplingTokens.clear();
|
|
360
406
|
activeTui = undefined;
|
|
361
407
|
widgetInstalled = false;
|
|
362
408
|
if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
@@ -506,6 +552,7 @@ export default function subagentExtension(
|
|
|
506
552
|
}
|
|
507
553
|
await transient.cleanup();
|
|
508
554
|
handles.push(handle);
|
|
555
|
+
if (widgetItems.has(entry.id)) tokenHandles.set(entry.id, handle);
|
|
509
556
|
return handle;
|
|
510
557
|
};
|
|
511
558
|
// The first tab is verified before returning a handle; subsequent independent
|
|
@@ -538,12 +585,16 @@ export default function subagentExtension(
|
|
|
538
585
|
active++;
|
|
539
586
|
return () => { active--; queue.shift()?.(); };
|
|
540
587
|
};
|
|
541
|
-
await runForegroundWorkflow(
|
|
588
|
+
await runForegroundWorkflow(workflow.mode, entries, async (entry) => {
|
|
542
589
|
const release = await permit();
|
|
543
590
|
try {
|
|
544
591
|
controller.signal.throwIfAborted();
|
|
545
592
|
const handle = entry.index === 0 ? first : await launch(entry, controller.signal);
|
|
546
593
|
const answer = await handle.answer(Math.floor(40 * 1024 / entries.length));
|
|
594
|
+
try {
|
|
595
|
+
const tokens = await handle.usageTokens();
|
|
596
|
+
if (tokens !== undefined && widgetItems.has(entry.id)) widgetItems.get(entry.id)!.tokens = tokens;
|
|
597
|
+
} catch { /* Optional telemetry. */ }
|
|
547
598
|
const base = states.get(entry.id)!;
|
|
548
599
|
states.set(entry.id, { id: base.id, index: base.index, name: base.name, role: base.role,
|
|
549
600
|
...(base.model ? { model: base.model } : {}), ...(base.thinkingLevel ? { thinkingLevel: base.thinkingLevel } : {}),
|
package/extensions/workflow.ts
CHANGED
|
@@ -4,10 +4,17 @@ import { PROFILE_NAMES } from "@henryqw/pi-task-models";
|
|
|
4
4
|
import { Type, type Static } from "typebox";
|
|
5
5
|
import { Check, Errors } from "typebox/value";
|
|
6
6
|
import { ExecuteRequestSchema, parseExecuteRequest, type ExecuteRequest } from "../dist/schema.js";
|
|
7
|
-
import { TaskNameSchema, normalizeTaskName } from "./task-name.ts";
|
|
8
7
|
|
|
9
8
|
export const MAX_WORKFLOW_ENTRIES = 8;
|
|
10
9
|
|
|
10
|
+
const TASK_NAME_MAX_LENGTH = 29;
|
|
11
|
+
const TaskNameSchema = Type.String({
|
|
12
|
+
minLength: 1,
|
|
13
|
+
maxLength: TASK_NAME_MAX_LENGTH,
|
|
14
|
+
description: `Short descriptive task name, about five words and fewer than ${TASK_NAME_MAX_LENGTH + 1} characters; C0/C1 control characters are rejected.`,
|
|
15
|
+
pattern: DISPLAY_TEXT_CONTRACT.pattern,
|
|
16
|
+
});
|
|
17
|
+
|
|
11
18
|
const RoleSchema = Type.String({ minLength: 1, pattern: DISPLAY_TEXT_CONTRACT.pattern, description: "Configured Subagent role name" });
|
|
12
19
|
const TaskSchema = Type.String({ minLength: 1, description: "Bounded task packet" });
|
|
13
20
|
const ModelSchema = Type.String({ minLength: 1, pattern: DISPLAY_TEXT_CONTRACT.pattern, description: "Designated model as provider/modelId; replaces the selected route model" });
|
|
@@ -55,6 +62,12 @@ function text(value: string, path: string): string {
|
|
|
55
62
|
return normalized;
|
|
56
63
|
}
|
|
57
64
|
|
|
65
|
+
function normalizeTaskName(value: string, path: string): string {
|
|
66
|
+
const normalized = value.trim();
|
|
67
|
+
if (!normalized) throw new Error(`${path} must be non-empty text.`);
|
|
68
|
+
return normalized;
|
|
69
|
+
}
|
|
70
|
+
|
|
58
71
|
function normalizeDelegation(value: Static<typeof DelegationSchema>, path: string): Delegation {
|
|
59
72
|
return {
|
|
60
73
|
role: text(value.role, `${path}.role`),
|
|
@@ -118,19 +131,18 @@ async function runEntry(entry: WorkflowEntry, run: DelegationRunner): Promise<st
|
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
export async function runForegroundWorkflow(
|
|
121
|
-
|
|
122
|
-
|
|
134
|
+
mode: WorkflowMode,
|
|
135
|
+
entries: readonly WorkflowEntry[],
|
|
123
136
|
run: DelegationRunner,
|
|
124
137
|
signal?: AbortSignal,
|
|
125
138
|
): Promise<void> {
|
|
126
139
|
signal?.throwIfAborted();
|
|
127
|
-
|
|
128
|
-
if (workflow.mode === "single") {
|
|
140
|
+
if (mode === "single") {
|
|
129
141
|
await runEntry(entries[0]!, run);
|
|
130
142
|
signal?.throwIfAborted();
|
|
131
143
|
return;
|
|
132
144
|
}
|
|
133
|
-
if (
|
|
145
|
+
if (mode === "parallel") {
|
|
134
146
|
await Promise.all(entries.map((entry) => runEntry(entry, run)));
|
|
135
147
|
signal?.throwIfAborted();
|
|
136
148
|
return;
|
package/package.json
CHANGED
|
@@ -26,10 +26,10 @@ Use one compact `role`/`name`/`task` packet, `tasks` for independent packets, or
|
|
|
26
26
|
|
|
27
27
|
Give the request a unique ID and goal. Give each task a unique ID, kind, Role, model class, bounded requirements, deliverable, `dependsOn`, and `contextFrom`. Changesets require focused task checks, and a graph containing changesets requires final checks. The root must have a `package.json` test script; `pnpm test` is added to final checks if missing and runs against the combined tip before Main promotion. Text context may name only completed text tasks and preserves declared order.
|
|
28
28
|
|
|
29
|
-
Every changeset advances automatically after preliminary checks pass. While a locally owned changeset task is actively working, the user may open `/subagent` to queue bounded same-worker revisions or withdraw all still-pending instructions into the native editor and submit one replacement. Cancellation after withdrawal leaves the originals removed; already-claimed instructions cannot be recalled. Each revision must produce a new clean commit and rerun preliminary checks. When no queued revision remains, the runner seals and durably records the exact ready candidate without waiting for
|
|
29
|
+
Every changeset advances automatically after preliminary checks pass. While a locally owned changeset task is actively working, the user may open `/subagent` to queue bounded same-worker revisions or withdraw all still-pending instructions into the native editor and submit one replacement. Cancellation after withdrawal leaves the originals removed; already-claimed instructions cannot be recalled. Each revision must produce a new clean commit and rerun preliminary checks. When no queued revision remains, the runner seals and durably records the exact ready candidate without waiting for sibling workers. There is no guaranteed post-completion editing window, and late follow-ups fail visibly.
|
|
30
30
|
|
|
31
|
-
A ready candidate does not integrate automatically.
|
|
31
|
+
A ready candidate does not integrate automatically. Each checked worker triggers a follow-up while its siblings continue; inspect `subagent_status` and immediately use `subagent_stage` with the exact `generation`, `taskId`, `attempt`, candidate `tip`, and `expectedTip` to select and merge it into the owned integration checkout. Stage and resolve are available during the active wave; wait for the wave to settle before rejecting or revising. Resolve conflicts in the integration checkout and verify with `resolve`; never edit Main to resolve them. For dependents, `subagent_integrate advance` acknowledges a durable background wave, then reports checked candidates as they finish. When selected work is staged and the wave settles, use `subagent_integrate validate` on the exact `combinedTip` for full root checks and optional final judgment; `promote` requires passing evidence and unchanged Main. One Main-authored committed correction to a definitively failed combined validation can be recorded with `correct`, then revalidated. Only proven promotion triggers selected-worker termination and verified cleanup. Failures, ambiguity, conflicts, interruption, child-limit exhaustion, or Main drift retain work and never waive checks or identity guards. The extension never pushes, opens a pull request, publishes, or deploys.
|
|
32
32
|
|
|
33
|
-
Use `subagent_status` to inspect durable state without mutation, including candidate identities, generations, and retained resources. `subagent_resume` handles only the reported retry/verify/finalize continuation; it cannot replace Main's stage/integrate decisions or reset policy/corrections. `subagent_stage reject` or `revise` freezes a generation containing the candidate and invalidates dependent attempts; restage chosen work in a new generation. If Main advances cleanly before promotion, use `subagent_integrate refresh` with exact old/new Main and combined-tip identities; restage immutable candidates into the new generation and rerun combined validation. Dirty or divergent Main blocks refresh. At most two unreleased integration worktrees may coexist. Rejected candidates and superseded generations remain retained until Main explicitly calls `subagent_integrate release` with the exact generation and tip; for a rejected worker include its task ID and attempt after releasing all generations using it. Dirty/conflicted or unproved resources block release without force deletion. Impossible fresh dependent work after rejection needs a new request. `subagent_abort` cannot discard retained candidates or integration generations; it may finish once all are released. Old v4 state is rejected (current state is v5), not migrated. Status, abort, process I/O, termination, and verified post-promotion cleanup keep finite safety budgets.
|
|
33
|
+
After a coordinator interruption in the canonical repository, ask the user to run `/subagent recover` once: it acquires the productive lease, classifies orphaned interruptions, preserves invalid files and retained work, and sends Main one follow-up with blockers and next actions. A live productive lease is read-only and cannot be taken over. Recovery never replays prompts, merges, promotion, or cleanup. Use `subagent_status` to inspect exact durable state without mutation, including candidate identities, generations, and retained resources. `subagent_resume` handles only the reported retry/verify/finalize continuation; it cannot replace Main's stage/integrate decisions or reset policy/corrections. `subagent_stage reject` or `revise` freezes a generation containing the candidate and invalidates dependent attempts; restage chosen work in a new generation. If Main advances cleanly before promotion, use `subagent_integrate refresh` with exact old/new Main and combined-tip identities; restage immutable candidates into the new generation and rerun combined validation. Dirty or divergent Main blocks refresh. At most two unreleased integration worktrees may coexist. Rejected candidates and superseded generations remain retained until Main explicitly calls `subagent_integrate release` with the exact generation and tip; for a rejected worker include its task ID and attempt after releasing all generations using it. Dirty/conflicted or unproved resources block release without force deletion. Impossible fresh dependent work after rejection needs a new request. `subagent_abort` cannot discard retained candidates or integration generations; it may finish once all are released. Old v4 state is rejected (current state is v5), not migrated. Status, abort, process I/O, termination, and verified post-promotion cleanup keep finite safety budgets.
|
|
34
34
|
|
|
35
35
|
All launches use the global policy in `config/pi-subagent/config.json`: productive concurrency, per-child turns/tokens, and idle timeout. Ephemeral children also have a hard runtime limit; isolated requests have a correction cap. Productive requests have no whole-run wall-clock deadline. Request fields cannot replenish or override limits.
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5
|
-
<title>Delegation routing and checked task lifecycle</title>
|
|
6
|
-
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
7
|
-
<style>
|
|
8
|
-
* { box-sizing: border-box; } body { margin: 0; padding: 32px; background: #f0eee9; color: #101828; font-family: 'Geist', sans-serif; }
|
|
9
|
-
main { max-width: 1280px; margin: auto; } h1 { font: 400 28px 'Instrument Serif', serif; margin: 0 0 16px; }
|
|
10
|
-
svg { display: block; width: 100%; height: auto; }
|
|
11
|
-
</style>
|
|
12
|
-
</head>
|
|
13
|
-
<body><main><h1>Delegation routing and checked task lifecycle</h1>
|
|
14
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="delegation-routing-title delegation-routing-desc">
|
|
15
|
-
<title id="delegation-routing-title">Delegation routing and checked task lifecycle</title>
|
|
16
|
-
<desc id="delegation-routing-desc">A read-only Role runs direct in the current workspace; implementation uses an isolated checked graph. In that graph workers produce a checked candidate, Main stages and validates the combined tip, then explicitly promotes it. Interruptions retain work for recovery and revalidation.</desc>
|
|
17
|
-
<defs>
|
|
18
|
-
<marker id="routing-arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#4c5665"/></marker>
|
|
19
|
-
<marker id="routing-arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#1d4ed8"/></marker>
|
|
20
|
-
<marker id="routing-arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#4c5665"/></marker>
|
|
21
|
-
</defs>
|
|
22
|
-
<rect width="1280" height="720" fill="#f0eee9"/>
|
|
23
|
-
<text x="48" y="60" fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1.5">PI SUBAGENT / ROUTING + LIFECYCLE</text>
|
|
24
|
-
<text x="48" y="99" fill="#101828" font-family="'Instrument Serif',serif" font-size="28">One explicit mode; only checked work can be promoted</text>
|
|
25
|
-
<text x="48" y="126" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Direct work returns an observed result. Isolated changesets yield a candidate that Main must validate and promote.</text>
|
|
26
|
-
<!-- Routes and transitions are drawn before nodes. -->
|
|
27
|
-
<line x1="224" y1="212" x2="248" y2="212" stroke="#4c5665" marker-end="url(#routing-arrow)"/>
|
|
28
|
-
<path d="M336 268 V296 Q336 304 328 304 H192 Q184 304 184 312 V352" fill="none" stroke="#4c5665" marker-end="url(#routing-arrow)"/>
|
|
29
|
-
<path d="M424 212 H488 Q496 212 496 220 V352" fill="none" stroke="#1d4ed8" stroke-width="1.4" marker-end="url(#routing-arrow-accent)"/>
|
|
30
|
-
<line x1="560" y1="400" x2="624" y2="400" stroke="#4c5665" marker-end="url(#routing-arrow)"/>
|
|
31
|
-
<line x1="784" y1="400" x2="848" y2="400" stroke="#4c5665" marker-end="url(#routing-arrow)"/>
|
|
32
|
-
<line x1="1008" y1="400" x2="1072" y2="400" stroke="#1d4ed8" stroke-width="1.4" marker-end="url(#routing-arrow-accent)"/>
|
|
33
|
-
<!-- Interruption / re-entry paths use separate attach points. -->
|
|
34
|
-
<line x1="448" y1="448" x2="448" y2="512" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#routing-arrow)"/>
|
|
35
|
-
<line x1="512" y1="512" x2="512" y2="448" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#routing-arrow)"/>
|
|
36
|
-
<line x1="896" y1="448" x2="896" y2="512" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#routing-arrow)"/>
|
|
37
|
-
<line x1="960" y1="512" x2="960" y2="448" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#routing-arrow)"/>
|
|
38
|
-
<!-- Arrow labels have an 8px gap to their strokes. -->
|
|
39
|
-
<g fill="#4c5665" font-family="'Geist Mono',monospace" font-size="8" text-anchor="middle">
|
|
40
|
-
<rect x="256" y="284" width="48" height="12" rx="2" fill="#f0eee9"/><text x="280" y="293">YES</text>
|
|
41
|
-
<rect x="432" y="192" width="72" height="12" rx="2" fill="#f0eee9"/><text x="468" y="201" fill="#1d4ed8">NO / GRAPH</text>
|
|
42
|
-
<rect x="568" y="380" width="48" height="12" rx="2" fill="#f0eee9"/><text x="592" y="389">CHECK</text>
|
|
43
|
-
<rect x="792" y="380" width="48" height="12" rx="2" fill="#f0eee9"/><text x="816" y="389">STAGE</text>
|
|
44
|
-
<rect x="1016" y="380" width="48" height="12" rx="2" fill="#f0eee9"/><text x="1040" y="389" fill="#1d4ed8">VERIFY</text>
|
|
45
|
-
<rect x="384" y="472" width="56" height="12" rx="2" fill="#f0eee9"/><text x="412" y="481">PAUSE</text>
|
|
46
|
-
<rect x="520" y="472" width="56" height="12" rx="2" fill="#f0eee9"/><text x="548" y="481">RESUME</text>
|
|
47
|
-
<rect x="816" y="472" width="72" height="12" rx="2" fill="#f0eee9"/><text x="852" y="481">MAIN DRIFT</text>
|
|
48
|
-
<rect x="968" y="472" width="72" height="12" rx="2" fill="#f0eee9"/><text x="1004" y="481">RECHECK</text>
|
|
49
|
-
</g>
|
|
50
|
-
<!-- Mode selection. -->
|
|
51
|
-
<rect x="80" y="180" width="144" height="64" rx="24" fill="#ffffff" stroke="#101828"/>
|
|
52
|
-
<text x="152" y="217" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Delegate task</text>
|
|
53
|
-
<polygon points="336,156 424,212 336,268 248,212" fill="#ffffff" stroke="#101828"/>
|
|
54
|
-
<text x="336" y="208" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Direct</text>
|
|
55
|
-
<text x="336" y="226" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">eligible?</text>
|
|
56
|
-
<!-- Direct is intentionally a single terminal branch: no worktree or candidate. -->
|
|
57
|
-
<text x="80" y="336" fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">DIRECT</text>
|
|
58
|
-
<rect x="80" y="352" width="208" height="96" rx="8" fill="#ffffff" stroke="#101828"/>
|
|
59
|
-
<text x="184" y="384" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Herdr tabs</text>
|
|
60
|
-
<text x="184" y="406" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Current workspace</text>
|
|
61
|
-
<text x="184" y="428" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Observe result</text>
|
|
62
|
-
<text x="80" y="477" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Single · parallel · chain</text>
|
|
63
|
-
<!-- Isolated changeset rail. -->
|
|
64
|
-
<text x="400" y="336" fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">ISOLATED / CHECKED CHANGESET</text>
|
|
65
|
-
<rect x="400" y="352" width="160" height="96" rx="8" fill="#ffffff" stroke="#101828"/>
|
|
66
|
-
<text x="480" y="387" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Saved + working</text>
|
|
67
|
-
<text x="480" y="411" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Durable ID</text>
|
|
68
|
-
<text x="480" y="429" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Owned worktree</text>
|
|
69
|
-
<rect x="624" y="352" width="160" height="96" rx="8" fill="#ffffff" stroke="#101828"/>
|
|
70
|
-
<text x="704" y="390" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Ready candidate</text>
|
|
71
|
-
<text x="704" y="416" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Clean commit + checks</text>
|
|
72
|
-
<rect x="848" y="352" width="160" height="96" rx="8" fill="#ffffff" stroke="#101828"/>
|
|
73
|
-
<text x="928" y="390" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Staged by Main</text>
|
|
74
|
-
<text x="928" y="416" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Exact candidate tip</text>
|
|
75
|
-
<rect x="1072" y="352" width="160" height="96" rx="8" fill="#1d4ed8" fill-opacity="0.08" stroke="#1d4ed8"/>
|
|
76
|
-
<text x="1152" y="390" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Promoted by Main</text>
|
|
77
|
-
<text x="1152" y="416" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">After combined checks</text>
|
|
78
|
-
<!-- Recovery states do not erase or bypass exact checks. -->
|
|
79
|
-
<text x="400" y="497" fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">WAIT / RECOVERY</text>
|
|
80
|
-
<rect x="400" y="512" width="160" height="72" rx="8" fill="#e6e4de" stroke="#4c5665" stroke-dasharray="5,4"/>
|
|
81
|
-
<text x="480" y="541" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Needs attention</text>
|
|
82
|
-
<text x="480" y="562" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Status / retry</text>
|
|
83
|
-
<rect x="848" y="512" width="160" height="72" rx="8" fill="#e6e4de" stroke="#4c5665" stroke-dasharray="5,4"/>
|
|
84
|
-
<text x="928" y="541" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Revalidate</text>
|
|
85
|
-
<text x="928" y="562" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Restage if needed</text>
|
|
86
|
-
<line x1="48" y1="648" x2="1232" y2="648" stroke="#101828" stroke-opacity="0.12"/>
|
|
87
|
-
<text x="48" y="674" fill="#4c5665" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">GUARD</text>
|
|
88
|
-
<text x="128" y="674" fill="#101828" font-family="'Geist',sans-serif" font-size="12">No fallback between modes. Failed validation retains evidence; abort requires no retained work.</text>
|
|
89
|
-
</svg></main></body></html>
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="delegation-routing-title delegation-routing-desc">
|
|
3
|
-
<title id="delegation-routing-title">Delegation routing and checked task lifecycle</title>
|
|
4
|
-
<desc id="delegation-routing-desc">A read-only Role runs direct in the current workspace; implementation uses an isolated checked graph. In that graph workers produce a checked candidate, Main stages and validates the combined tip, then explicitly promotes it. Interruptions retain work for recovery and revalidation.</desc>
|
|
5
|
-
<defs>
|
|
6
|
-
<style>@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap');</style>
|
|
7
|
-
<marker id="routing-arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#4c5665"/></marker>
|
|
8
|
-
<marker id="routing-arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#1d4ed8"/></marker>
|
|
9
|
-
<marker id="routing-arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#4c5665"/></marker>
|
|
10
|
-
</defs>
|
|
11
|
-
<rect width="1280" height="720" fill="#f0eee9"/>
|
|
12
|
-
<text x="48" y="60" fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1.5">PI SUBAGENT / ROUTING + LIFECYCLE</text>
|
|
13
|
-
<text x="48" y="99" fill="#101828" font-family="'Instrument Serif',serif" font-size="28">One explicit mode; only checked work can be promoted</text>
|
|
14
|
-
<text x="48" y="126" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Direct work returns an observed result. Isolated changesets yield a candidate that Main must validate and promote.</text>
|
|
15
|
-
<!-- Routes and transitions are drawn before nodes. -->
|
|
16
|
-
<line x1="224" y1="212" x2="248" y2="212" stroke="#4c5665" marker-end="url(#routing-arrow)"/>
|
|
17
|
-
<path d="M336 268 V296 Q336 304 328 304 H192 Q184 304 184 312 V352" fill="none" stroke="#4c5665" marker-end="url(#routing-arrow)"/>
|
|
18
|
-
<path d="M424 212 H488 Q496 212 496 220 V352" fill="none" stroke="#1d4ed8" stroke-width="1.4" marker-end="url(#routing-arrow-accent)"/>
|
|
19
|
-
<line x1="560" y1="400" x2="624" y2="400" stroke="#4c5665" marker-end="url(#routing-arrow)"/>
|
|
20
|
-
<line x1="784" y1="400" x2="848" y2="400" stroke="#4c5665" marker-end="url(#routing-arrow)"/>
|
|
21
|
-
<line x1="1008" y1="400" x2="1072" y2="400" stroke="#1d4ed8" stroke-width="1.4" marker-end="url(#routing-arrow-accent)"/>
|
|
22
|
-
<!-- Interruption / re-entry paths use separate attach points. -->
|
|
23
|
-
<line x1="448" y1="448" x2="448" y2="512" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#routing-arrow)"/>
|
|
24
|
-
<line x1="512" y1="512" x2="512" y2="448" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#routing-arrow)"/>
|
|
25
|
-
<line x1="896" y1="448" x2="896" y2="512" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#routing-arrow)"/>
|
|
26
|
-
<line x1="960" y1="512" x2="960" y2="448" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#routing-arrow)"/>
|
|
27
|
-
<!-- Arrow labels have an 8px gap to their strokes. -->
|
|
28
|
-
<g fill="#4c5665" font-family="'Geist Mono',monospace" font-size="8" text-anchor="middle">
|
|
29
|
-
<rect x="256" y="284" width="48" height="12" rx="2" fill="#f0eee9"/><text x="280" y="293">YES</text>
|
|
30
|
-
<rect x="432" y="192" width="72" height="12" rx="2" fill="#f0eee9"/><text x="468" y="201" fill="#1d4ed8">NO / GRAPH</text>
|
|
31
|
-
<rect x="568" y="380" width="48" height="12" rx="2" fill="#f0eee9"/><text x="592" y="389">CHECK</text>
|
|
32
|
-
<rect x="792" y="380" width="48" height="12" rx="2" fill="#f0eee9"/><text x="816" y="389">STAGE</text>
|
|
33
|
-
<rect x="1016" y="380" width="48" height="12" rx="2" fill="#f0eee9"/><text x="1040" y="389" fill="#1d4ed8">VERIFY</text>
|
|
34
|
-
<rect x="384" y="472" width="56" height="12" rx="2" fill="#f0eee9"/><text x="412" y="481">PAUSE</text>
|
|
35
|
-
<rect x="520" y="472" width="56" height="12" rx="2" fill="#f0eee9"/><text x="548" y="481">RESUME</text>
|
|
36
|
-
<rect x="816" y="472" width="72" height="12" rx="2" fill="#f0eee9"/><text x="852" y="481">MAIN DRIFT</text>
|
|
37
|
-
<rect x="968" y="472" width="72" height="12" rx="2" fill="#f0eee9"/><text x="1004" y="481">RECHECK</text>
|
|
38
|
-
</g>
|
|
39
|
-
<!-- Mode selection. -->
|
|
40
|
-
<rect x="80" y="180" width="144" height="64" rx="24" fill="#ffffff" stroke="#101828"/>
|
|
41
|
-
<text x="152" y="217" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Delegate task</text>
|
|
42
|
-
<polygon points="336,156 424,212 336,268 248,212" fill="#ffffff" stroke="#101828"/>
|
|
43
|
-
<text x="336" y="208" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Direct</text>
|
|
44
|
-
<text x="336" y="226" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">eligible?</text>
|
|
45
|
-
<!-- Direct is intentionally a single terminal branch: no worktree or candidate. -->
|
|
46
|
-
<text x="80" y="336" fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">DIRECT</text>
|
|
47
|
-
<rect x="80" y="352" width="208" height="96" rx="8" fill="#ffffff" stroke="#101828"/>
|
|
48
|
-
<text x="184" y="384" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Herdr tabs</text>
|
|
49
|
-
<text x="184" y="406" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Current workspace</text>
|
|
50
|
-
<text x="184" y="428" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Observe result</text>
|
|
51
|
-
<text x="80" y="477" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Single · parallel · chain</text>
|
|
52
|
-
<!-- Isolated changeset rail. -->
|
|
53
|
-
<text x="400" y="336" fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">ISOLATED / CHECKED CHANGESET</text>
|
|
54
|
-
<rect x="400" y="352" width="160" height="96" rx="8" fill="#ffffff" stroke="#101828"/>
|
|
55
|
-
<text x="480" y="387" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Saved + working</text>
|
|
56
|
-
<text x="480" y="411" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Durable ID</text>
|
|
57
|
-
<text x="480" y="429" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Owned worktree</text>
|
|
58
|
-
<rect x="624" y="352" width="160" height="96" rx="8" fill="#ffffff" stroke="#101828"/>
|
|
59
|
-
<text x="704" y="390" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Ready candidate</text>
|
|
60
|
-
<text x="704" y="416" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Clean commit + checks</text>
|
|
61
|
-
<rect x="848" y="352" width="160" height="96" rx="8" fill="#ffffff" stroke="#101828"/>
|
|
62
|
-
<text x="928" y="390" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Staged by Main</text>
|
|
63
|
-
<text x="928" y="416" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Exact candidate tip</text>
|
|
64
|
-
<rect x="1072" y="352" width="160" height="96" rx="8" fill="#1d4ed8" fill-opacity="0.08" stroke="#1d4ed8"/>
|
|
65
|
-
<text x="1152" y="390" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Promoted by Main</text>
|
|
66
|
-
<text x="1152" y="416" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">After combined checks</text>
|
|
67
|
-
<!-- Recovery states do not erase or bypass exact checks. -->
|
|
68
|
-
<text x="400" y="497" fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">WAIT / RECOVERY</text>
|
|
69
|
-
<rect x="400" y="512" width="160" height="72" rx="8" fill="#e6e4de" stroke="#4c5665" stroke-dasharray="5,4"/>
|
|
70
|
-
<text x="480" y="541" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Needs attention</text>
|
|
71
|
-
<text x="480" y="562" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Status / retry</text>
|
|
72
|
-
<rect x="848" y="512" width="160" height="72" rx="8" fill="#e6e4de" stroke="#4c5665" stroke-dasharray="5,4"/>
|
|
73
|
-
<text x="928" y="541" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Revalidate</text>
|
|
74
|
-
<text x="928" y="562" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Restage if needed</text>
|
|
75
|
-
<line x1="48" y1="648" x2="1232" y2="648" stroke="#101828" stroke-opacity="0.12"/>
|
|
76
|
-
<text x="48" y="674" fill="#4c5665" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">GUARD</text>
|
|
77
|
-
<text x="128" y="674" fill="#101828" font-family="'Geist',sans-serif" font-size="12">No fallback between modes. Failed validation retains evidence; abort requires no retained work.</text>
|
|
78
|
-
</svg>
|
package/extensions/task-name.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { DISPLAY_TEXT_CONTRACT } from "@henryqw/pi-subagent";
|
|
2
|
-
import { Type, type Static } from "typebox";
|
|
3
|
-
|
|
4
|
-
const TASK_NAME_MAX_LENGTH = 29 as const;
|
|
5
|
-
const TASK_NAME_LIMIT_WORDING = `about five words and fewer than ${TASK_NAME_MAX_LENGTH + 1} characters`;
|
|
6
|
-
|
|
7
|
-
export const TASK_NAME_CONTRACT = {
|
|
8
|
-
minLength: 1,
|
|
9
|
-
maxLength: TASK_NAME_MAX_LENGTH,
|
|
10
|
-
description: `Short descriptive task name, ${TASK_NAME_LIMIT_WORDING}; C0/C1 control characters are rejected.`,
|
|
11
|
-
controlRanges: DISPLAY_TEXT_CONTRACT.controlRanges,
|
|
12
|
-
pattern: DISPLAY_TEXT_CONTRACT.pattern,
|
|
13
|
-
} as const;
|
|
14
|
-
|
|
15
|
-
export const TaskNameSchema = Type.String({
|
|
16
|
-
minLength: TASK_NAME_CONTRACT.minLength,
|
|
17
|
-
maxLength: TASK_NAME_CONTRACT.maxLength,
|
|
18
|
-
description: TASK_NAME_CONTRACT.description,
|
|
19
|
-
pattern: TASK_NAME_CONTRACT.pattern,
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
export type TaskName = Static<typeof TaskNameSchema>;
|
|
23
|
-
|
|
24
|
-
export function normalizeTaskName(value: TaskName, path: string): TaskName {
|
|
25
|
-
const normalized = value.trim();
|
|
26
|
-
if (!normalized) throw new Error(`${path} must be non-empty text.`);
|
|
27
|
-
return normalized;
|
|
28
|
-
}
|