@ferris1225/pi-subagents 4.0.1 → 4.1.2
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 +506 -478
- package/agents/cleaner.md +51 -41
- package/agents/documenter.md +44 -0
- package/agents/reviewer.md +71 -70
- package/agents/worker.md +4 -1
- package/package.json +2 -2
- package/src/agents.ts +12 -0
- package/src/announcements.ts +34 -7
- package/src/completion.ts +160 -160
- package/src/config.ts +86 -15
- package/src/dispatch.ts +637 -704
- package/src/fixloop.ts +266 -52
- package/src/index.ts +93 -93
- package/src/models.ts +189 -189
- package/src/monitor.ts +12 -3
- package/src/prompt.ts +47 -12
- package/src/recovery.ts +145 -145
- package/src/rpc-run.ts +23 -7
- package/src/runtime.ts +8 -7
- package/src/session-fork.ts +80 -80
- package/src/setup.ts +36 -5
- package/src/spawn.ts +45 -11
- package/src/thread-lifecycle.ts +203 -49
- package/src/tools.ts +23 -9
- package/src/widget.ts +4 -4
- package/src/worktree.ts +27 -4
package/src/models.ts
CHANGED
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Model routing, capability-aware thinking, and setup-picker helpers.
|
|
3
|
-
*
|
|
4
|
-
* Runtime has one explicit fallback only: a configured agent model hands
|
|
5
|
-
* off directly to the current main-window model. Setup lists only currently
|
|
6
|
-
* available models and derives thinking choices from Pi's model metadata.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
clampThinkingLevel,
|
|
11
|
-
getSupportedThinkingLevels,
|
|
12
|
-
type Api,
|
|
13
|
-
type Model,
|
|
14
|
-
} from "@earendil-works/pi-ai";
|
|
15
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import { DEFAULT_THINKING_LEVEL, type ThinkingLevel } from "./config.ts";
|
|
17
|
-
|
|
18
|
-
export type ModelContext = Pick<ExtensionContext, "model" | "modelRegistry"> &
|
|
19
|
-
Partial<Pick<ExtensionContext, "scopedModels">>;
|
|
20
|
-
|
|
21
|
-
export const CURRENT_MAIN_MODEL = "__current_main_model__";
|
|
22
|
-
|
|
23
|
-
export interface ModelPickerItem {
|
|
24
|
-
value: string;
|
|
25
|
-
label: string;
|
|
26
|
-
description?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type ModelListEntry = Pick<
|
|
30
|
-
Model<Api>,
|
|
31
|
-
"provider" | "id" | "name" | "input" | "reasoning" | "thinkingLevelMap"
|
|
32
|
-
>;
|
|
33
|
-
|
|
34
|
-
export interface ResolvedAgentModelRoute {
|
|
35
|
-
/** Effective first candidate. Undefined means let Pi use its normal default. */
|
|
36
|
-
primaryRef?: string;
|
|
37
|
-
/** Current main-window model when it differs from the selection. */
|
|
38
|
-
mainFallbackRef?: string;
|
|
39
|
-
/** Runtime order, useful for status/tests. */
|
|
40
|
-
candidateRefs: string[];
|
|
41
|
-
/** Configured ref skipped because Pi does not currently report it available. */
|
|
42
|
-
unavailableSelectedRef?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface AgentModelRouteInput {
|
|
46
|
-
selectedRef?: string;
|
|
47
|
-
mainRef?: string;
|
|
48
|
-
declaredDefaultRef?: string;
|
|
49
|
-
/** When supplied, a configured selection outside this live set is skipped. */
|
|
50
|
-
availableRefs?: readonly string[];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function cleanModelRef(ref: string | undefined): string | undefined {
|
|
54
|
-
const trimmed = ref?.trim();
|
|
55
|
-
return trimmed || undefined;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function modelRef(model: { provider: string; id: string }): string {
|
|
59
|
-
return `${model.provider}/${model.id}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function currentModelRef(ctx: Pick<ModelContext, "model">): string | undefined {
|
|
63
|
-
return ctx.model ? modelRef(ctx.model) : undefined;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Current authenticated registry models narrowed by the session scope. Scope
|
|
68
|
-
* entries are a session snapshot, so they act only as a whitelist; the live
|
|
69
|
-
* registry remains the source of truth for availability and model metadata.
|
|
70
|
-
*/
|
|
71
|
-
export function availableModelsInScope(ctx: ModelContext): readonly Model<Api>[] {
|
|
72
|
-
const models = ctx.modelRegistry.getAvailable();
|
|
73
|
-
// scopedModels was added after the original Pi minimum. Treat a missing field
|
|
74
|
-
// exactly like an empty scope and use the full live registry.
|
|
75
|
-
const scopedModels = ctx.scopedModels ?? [];
|
|
76
|
-
if (scopedModels.length === 0) return models;
|
|
77
|
-
const scopedRefs = new Set(scopedModels.map((entry) => modelRef(entry.model)));
|
|
78
|
-
return models.filter((model) => scopedRefs.has(modelRef(model)));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function findModelByRef(
|
|
82
|
-
models: readonly Model<Api>[],
|
|
83
|
-
ref: string | undefined,
|
|
84
|
-
): Model<Api> | undefined {
|
|
85
|
-
const normalized = cleanModelRef(ref);
|
|
86
|
-
return normalized ? models.find((model) => modelRef(model) === normalized) : undefined;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Resolve one agent's runtime route:
|
|
91
|
-
*
|
|
92
|
-
* configured selection -> current main-window model
|
|
93
|
-
*
|
|
94
|
-
* Without an override, current main is primary; the agent-declared default is
|
|
95
|
-
* used only when no main model exists. A configured selection that Pi no longer
|
|
96
|
-
* reports as available is skipped immediately instead of spawning a doomed child.
|
|
97
|
-
*/
|
|
98
|
-
export function resolveAgentModelRoute(input: AgentModelRouteInput): ResolvedAgentModelRoute {
|
|
99
|
-
const selectedRef = cleanModelRef(input.selectedRef);
|
|
100
|
-
const mainRef = cleanModelRef(input.mainRef);
|
|
101
|
-
const declaredDefaultRef = cleanModelRef(input.declaredDefaultRef);
|
|
102
|
-
const available = input.availableRefs
|
|
103
|
-
? new Set(input.availableRefs.map((ref) => ref.trim()).filter(Boolean))
|
|
104
|
-
: undefined;
|
|
105
|
-
const selectedAvailable = !selectedRef || !available || available.has(selectedRef);
|
|
106
|
-
const usableSelectedRef = selectedAvailable ? selectedRef : undefined;
|
|
107
|
-
const primaryRef = usableSelectedRef ?? mainRef ?? declaredDefaultRef;
|
|
108
|
-
const ordered = [primaryRef, usableSelectedRef && usableSelectedRef !== mainRef ? mainRef : undefined];
|
|
109
|
-
const candidateRefs = [...new Set(ordered.filter((ref): ref is string => Boolean(ref)))];
|
|
110
|
-
return {
|
|
111
|
-
primaryRef,
|
|
112
|
-
...(candidateRefs[1] ? { mainFallbackRef: candidateRefs[1] } : {}),
|
|
113
|
-
candidateRefs,
|
|
114
|
-
...(!selectedAvailable && selectedRef ? { unavailableSelectedRef: selectedRef } : {}),
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** The exact levels Pi exposes for this model, including `off` when supported. */
|
|
119
|
-
export function supportedThinkingLevels(model: Model<Api> | undefined): ThinkingLevel[] {
|
|
120
|
-
return model ? (getSupportedThinkingLevels(model) as ThinkingLevel[]) : [];
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Clamp an agent preference to the effective model's actual capability map. */
|
|
124
|
-
export function resolveThinkingLevel(
|
|
125
|
-
model: Model<Api> | undefined,
|
|
126
|
-
preferred: ThinkingLevel = DEFAULT_THINKING_LEVEL,
|
|
127
|
-
): ThinkingLevel {
|
|
128
|
-
return model ? (clampThinkingLevel(model, preferred) as ThinkingLevel) : preferred;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function modelCapabilities(model: ModelListEntry): string {
|
|
132
|
-
const input = model.input.includes("image") ? "vision" : "text-only";
|
|
133
|
-
const thinking = getSupportedThinkingLevels(model as Model<Api>).join("/");
|
|
134
|
-
return `${input} · thinking: ${thinking}`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Build one searchable list for agent model selection. Only models Pi
|
|
138
|
-
* currently reports as available are supplied by setup. */
|
|
139
|
-
export function buildModelPickerItems(options: {
|
|
140
|
-
models: readonly ModelListEntry[];
|
|
141
|
-
configuredRef?: string;
|
|
142
|
-
mainRef?: string;
|
|
143
|
-
}): ModelPickerItem[] {
|
|
144
|
-
const configuredRef = cleanModelRef(options.configuredRef);
|
|
145
|
-
const mainRef = cleanModelRef(options.mainRef);
|
|
146
|
-
const byRef = new Map<string, ModelListEntry>();
|
|
147
|
-
for (const model of options.models) {
|
|
148
|
-
const ref = modelRef(model);
|
|
149
|
-
if (!byRef.has(ref)) byRef.set(ref, model);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const refs = [...byRef.keys()]
|
|
153
|
-
.sort((left, right) => {
|
|
154
|
-
const leftRank = left === configuredRef ? 0 : left === mainRef ? 1 : 2;
|
|
155
|
-
const rightRank = right === configuredRef ? 0 : right === mainRef ? 1 : 2;
|
|
156
|
-
return leftRank - rightRank || left.localeCompare(right);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
const dynamic: ModelPickerItem = {
|
|
160
|
-
value: CURRENT_MAIN_MODEL,
|
|
161
|
-
label: "Current main model (dynamic)",
|
|
162
|
-
description: "Clear agent override; use the current main model dynamically",
|
|
163
|
-
};
|
|
164
|
-
const items: ModelPickerItem[] = [dynamic];
|
|
165
|
-
for (const ref of refs) {
|
|
166
|
-
const model = byRef.get(ref)!;
|
|
167
|
-
const tags = [ref === configuredRef ? "configured" : "", ref === mainRef ? "current main" : ""]
|
|
168
|
-
.filter(Boolean);
|
|
169
|
-
const name = model.name.trim() && model.name !== model.id ? model.name.trim() : undefined;
|
|
170
|
-
items.push({
|
|
171
|
-
value: ref,
|
|
172
|
-
label: ref,
|
|
173
|
-
description: [name, modelCapabilities(model), ...tags].filter(Boolean).join(" · "),
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
return items;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** The dynamic choice removes the persisted per-agent override. */
|
|
180
|
-
export function applyAgentModelChoice(
|
|
181
|
-
current: Record<string, string>,
|
|
182
|
-
agentName: string,
|
|
183
|
-
choice: string,
|
|
184
|
-
): Record<string, string> {
|
|
185
|
-
const next = { ...current };
|
|
186
|
-
if (choice === CURRENT_MAIN_MODEL) delete next[agentName];
|
|
187
|
-
else next[agentName] = choice.trim();
|
|
188
|
-
return next;
|
|
189
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* Model routing, capability-aware thinking, and setup-picker helpers.
|
|
3
|
+
*
|
|
4
|
+
* Runtime has one explicit fallback only: a configured agent model hands
|
|
5
|
+
* off directly to the current main-window model. Setup lists only currently
|
|
6
|
+
* available models and derives thinking choices from Pi's model metadata.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
clampThinkingLevel,
|
|
11
|
+
getSupportedThinkingLevels,
|
|
12
|
+
type Api,
|
|
13
|
+
type Model,
|
|
14
|
+
} from "@earendil-works/pi-ai";
|
|
15
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { DEFAULT_THINKING_LEVEL, type ThinkingLevel } from "./config.ts";
|
|
17
|
+
|
|
18
|
+
export type ModelContext = Pick<ExtensionContext, "model" | "modelRegistry"> &
|
|
19
|
+
Partial<Pick<ExtensionContext, "scopedModels">>;
|
|
20
|
+
|
|
21
|
+
export const CURRENT_MAIN_MODEL = "__current_main_model__";
|
|
22
|
+
|
|
23
|
+
export interface ModelPickerItem {
|
|
24
|
+
value: string;
|
|
25
|
+
label: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ModelListEntry = Pick<
|
|
30
|
+
Model<Api>,
|
|
31
|
+
"provider" | "id" | "name" | "input" | "reasoning" | "thinkingLevelMap"
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
export interface ResolvedAgentModelRoute {
|
|
35
|
+
/** Effective first candidate. Undefined means let Pi use its normal default. */
|
|
36
|
+
primaryRef?: string;
|
|
37
|
+
/** Current main-window model when it differs from the selection. */
|
|
38
|
+
mainFallbackRef?: string;
|
|
39
|
+
/** Runtime order, useful for status/tests. */
|
|
40
|
+
candidateRefs: string[];
|
|
41
|
+
/** Configured ref skipped because Pi does not currently report it available. */
|
|
42
|
+
unavailableSelectedRef?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AgentModelRouteInput {
|
|
46
|
+
selectedRef?: string;
|
|
47
|
+
mainRef?: string;
|
|
48
|
+
declaredDefaultRef?: string;
|
|
49
|
+
/** When supplied, a configured selection outside this live set is skipped. */
|
|
50
|
+
availableRefs?: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function cleanModelRef(ref: string | undefined): string | undefined {
|
|
54
|
+
const trimmed = ref?.trim();
|
|
55
|
+
return trimmed || undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function modelRef(model: { provider: string; id: string }): string {
|
|
59
|
+
return `${model.provider}/${model.id}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function currentModelRef(ctx: Pick<ModelContext, "model">): string | undefined {
|
|
63
|
+
return ctx.model ? modelRef(ctx.model) : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Current authenticated registry models narrowed by the session scope. Scope
|
|
68
|
+
* entries are a session snapshot, so they act only as a whitelist; the live
|
|
69
|
+
* registry remains the source of truth for availability and model metadata.
|
|
70
|
+
*/
|
|
71
|
+
export function availableModelsInScope(ctx: ModelContext): readonly Model<Api>[] {
|
|
72
|
+
const models = ctx.modelRegistry.getAvailable();
|
|
73
|
+
// scopedModels was added after the original Pi minimum. Treat a missing field
|
|
74
|
+
// exactly like an empty scope and use the full live registry.
|
|
75
|
+
const scopedModels = ctx.scopedModels ?? [];
|
|
76
|
+
if (scopedModels.length === 0) return models;
|
|
77
|
+
const scopedRefs = new Set(scopedModels.map((entry) => modelRef(entry.model)));
|
|
78
|
+
return models.filter((model) => scopedRefs.has(modelRef(model)));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function findModelByRef(
|
|
82
|
+
models: readonly Model<Api>[],
|
|
83
|
+
ref: string | undefined,
|
|
84
|
+
): Model<Api> | undefined {
|
|
85
|
+
const normalized = cleanModelRef(ref);
|
|
86
|
+
return normalized ? models.find((model) => modelRef(model) === normalized) : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve one agent's runtime route:
|
|
91
|
+
*
|
|
92
|
+
* configured selection -> current main-window model
|
|
93
|
+
*
|
|
94
|
+
* Without an override, current main is primary; the agent-declared default is
|
|
95
|
+
* used only when no main model exists. A configured selection that Pi no longer
|
|
96
|
+
* reports as available is skipped immediately instead of spawning a doomed child.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveAgentModelRoute(input: AgentModelRouteInput): ResolvedAgentModelRoute {
|
|
99
|
+
const selectedRef = cleanModelRef(input.selectedRef);
|
|
100
|
+
const mainRef = cleanModelRef(input.mainRef);
|
|
101
|
+
const declaredDefaultRef = cleanModelRef(input.declaredDefaultRef);
|
|
102
|
+
const available = input.availableRefs
|
|
103
|
+
? new Set(input.availableRefs.map((ref) => ref.trim()).filter(Boolean))
|
|
104
|
+
: undefined;
|
|
105
|
+
const selectedAvailable = !selectedRef || !available || available.has(selectedRef);
|
|
106
|
+
const usableSelectedRef = selectedAvailable ? selectedRef : undefined;
|
|
107
|
+
const primaryRef = usableSelectedRef ?? mainRef ?? declaredDefaultRef;
|
|
108
|
+
const ordered = [primaryRef, usableSelectedRef && usableSelectedRef !== mainRef ? mainRef : undefined];
|
|
109
|
+
const candidateRefs = [...new Set(ordered.filter((ref): ref is string => Boolean(ref)))];
|
|
110
|
+
return {
|
|
111
|
+
primaryRef,
|
|
112
|
+
...(candidateRefs[1] ? { mainFallbackRef: candidateRefs[1] } : {}),
|
|
113
|
+
candidateRefs,
|
|
114
|
+
...(!selectedAvailable && selectedRef ? { unavailableSelectedRef: selectedRef } : {}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The exact levels Pi exposes for this model, including `off` when supported. */
|
|
119
|
+
export function supportedThinkingLevels(model: Model<Api> | undefined): ThinkingLevel[] {
|
|
120
|
+
return model ? (getSupportedThinkingLevels(model) as ThinkingLevel[]) : [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Clamp an agent preference to the effective model's actual capability map. */
|
|
124
|
+
export function resolveThinkingLevel(
|
|
125
|
+
model: Model<Api> | undefined,
|
|
126
|
+
preferred: ThinkingLevel = DEFAULT_THINKING_LEVEL,
|
|
127
|
+
): ThinkingLevel {
|
|
128
|
+
return model ? (clampThinkingLevel(model, preferred) as ThinkingLevel) : preferred;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function modelCapabilities(model: ModelListEntry): string {
|
|
132
|
+
const input = model.input.includes("image") ? "vision" : "text-only";
|
|
133
|
+
const thinking = getSupportedThinkingLevels(model as Model<Api>).join("/");
|
|
134
|
+
return `${input} · thinking: ${thinking}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Build one searchable list for agent model selection. Only models Pi
|
|
138
|
+
* currently reports as available are supplied by setup. */
|
|
139
|
+
export function buildModelPickerItems(options: {
|
|
140
|
+
models: readonly ModelListEntry[];
|
|
141
|
+
configuredRef?: string;
|
|
142
|
+
mainRef?: string;
|
|
143
|
+
}): ModelPickerItem[] {
|
|
144
|
+
const configuredRef = cleanModelRef(options.configuredRef);
|
|
145
|
+
const mainRef = cleanModelRef(options.mainRef);
|
|
146
|
+
const byRef = new Map<string, ModelListEntry>();
|
|
147
|
+
for (const model of options.models) {
|
|
148
|
+
const ref = modelRef(model);
|
|
149
|
+
if (!byRef.has(ref)) byRef.set(ref, model);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const refs = [...byRef.keys()]
|
|
153
|
+
.sort((left, right) => {
|
|
154
|
+
const leftRank = left === configuredRef ? 0 : left === mainRef ? 1 : 2;
|
|
155
|
+
const rightRank = right === configuredRef ? 0 : right === mainRef ? 1 : 2;
|
|
156
|
+
return leftRank - rightRank || left.localeCompare(right);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const dynamic: ModelPickerItem = {
|
|
160
|
+
value: CURRENT_MAIN_MODEL,
|
|
161
|
+
label: "Current main model (dynamic)",
|
|
162
|
+
description: "Clear agent override; use the current main model dynamically",
|
|
163
|
+
};
|
|
164
|
+
const items: ModelPickerItem[] = [dynamic];
|
|
165
|
+
for (const ref of refs) {
|
|
166
|
+
const model = byRef.get(ref)!;
|
|
167
|
+
const tags = [ref === configuredRef ? "configured" : "", ref === mainRef ? "current main" : ""]
|
|
168
|
+
.filter(Boolean);
|
|
169
|
+
const name = model.name.trim() && model.name !== model.id ? model.name.trim() : undefined;
|
|
170
|
+
items.push({
|
|
171
|
+
value: ref,
|
|
172
|
+
label: ref,
|
|
173
|
+
description: [name, modelCapabilities(model), ...tags].filter(Boolean).join(" · "),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return items;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The dynamic choice removes the persisted per-agent override. */
|
|
180
|
+
export function applyAgentModelChoice(
|
|
181
|
+
current: Record<string, string>,
|
|
182
|
+
agentName: string,
|
|
183
|
+
choice: string,
|
|
184
|
+
): Record<string, string> {
|
|
185
|
+
const next = { ...current };
|
|
186
|
+
if (choice === CURRENT_MAIN_MODEL) delete next[agentName];
|
|
187
|
+
else next[agentName] = choice.trim();
|
|
188
|
+
return next;
|
|
189
|
+
}
|
package/src/monitor.ts
CHANGED
|
@@ -49,15 +49,15 @@ export interface RunView {
|
|
|
49
49
|
startedAt?: number;
|
|
50
50
|
/** Epoch ms when the run finished (set on "done"/"failed"). */
|
|
51
51
|
endedAt?: number;
|
|
52
|
-
/** When set, this
|
|
52
|
+
/** When set, this is an internal managed-workflow step. */
|
|
53
53
|
groupId?: string;
|
|
54
54
|
/** Human-readable role within a chain, e.g. "fix round 1" or "re-review round 1". */
|
|
55
55
|
relationLabel?: string;
|
|
56
|
-
/**
|
|
56
|
+
/** Stable owning run whose row represents the whole managed workflow. */
|
|
57
57
|
parentRunId?: number;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/** Optional
|
|
60
|
+
/** Optional metadata for documenter/reviewer/fix children of a stable parent run. */
|
|
61
61
|
export interface RunChainMeta {
|
|
62
62
|
groupId?: string;
|
|
63
63
|
relationLabel?: string;
|
|
@@ -503,6 +503,15 @@ export class MonitorStore {
|
|
|
503
503
|
this.notify();
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
/** Reflect the currently owned internal stage when a managed parent is parked
|
|
507
|
+
* or inspected between children; the stable id and original task stay intact. */
|
|
508
|
+
setAgent(id: number, agent: string): void {
|
|
509
|
+
const run = this.find(id);
|
|
510
|
+
if (!run) return;
|
|
511
|
+
run.agent = agent;
|
|
512
|
+
this.notify();
|
|
513
|
+
}
|
|
514
|
+
|
|
506
515
|
/** Update the objective shown for a queued retarget or resumed generation. */
|
|
507
516
|
setTask(id: number, task: string): void {
|
|
508
517
|
const run = this.find(id);
|
package/src/prompt.ts
CHANGED
|
@@ -11,22 +11,42 @@ function bullets(lines: readonly string[]): string {
|
|
|
11
11
|
return lines.map((line) => `- ${line}`).join("\n");
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export function buildDelegationDirective(
|
|
14
|
+
export function buildDelegationDirective(
|
|
15
|
+
agents: AgentConfig[],
|
|
16
|
+
options: { maxFixRounds?: number } = {},
|
|
17
|
+
): string {
|
|
15
18
|
if (agents.length === 0) return "";
|
|
16
19
|
|
|
17
20
|
const catalog = agents.map(formatCatalogEntry).join("\n");
|
|
18
21
|
const hasExplorer = agents.some((agent) => agent.name === "explorer");
|
|
19
22
|
const hasWorker = agents.some((agent) => agent.name === "worker");
|
|
20
23
|
const hasCleaner = agents.some((agent) => agent.name === "cleaner");
|
|
24
|
+
const hasDocumenter = agents.some((agent) => agent.name === "documenter");
|
|
21
25
|
const hasReviewer = agents.some((agent) => agent.name === "reviewer");
|
|
22
26
|
const hasMultiple = agents.length > 1;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const autoFixEnabled = hasWorker && (options.maxFixRounds ?? 1) > 0;
|
|
28
|
+
const codeWriterNames = [
|
|
29
|
+
...(hasWorker ? ["worker"] : []),
|
|
30
|
+
...(hasCleaner ? ["cleaner"] : []),
|
|
31
|
+
];
|
|
32
|
+
const reviewedWriterNames = [
|
|
33
|
+
...codeWriterNames,
|
|
34
|
+
...(hasDocumenter ? ["documenter"] : []),
|
|
35
|
+
];
|
|
36
|
+
const automaticWriterRoute = [
|
|
37
|
+
...(hasDocumenter ? ["documenter"] : []),
|
|
38
|
+
...(hasReviewer ? ["reviewer"] : []),
|
|
39
|
+
].join(" → ");
|
|
40
|
+
const namedWorktreeTargets = [
|
|
41
|
+
...(hasWorker ? ["worker"] : []),
|
|
42
|
+
...(hasCleaner ? ["cleaner"] : []),
|
|
43
|
+
...(hasDocumenter ? ["documenter"] : []),
|
|
44
|
+
];
|
|
45
|
+
const worktreeTargets = namedWorktreeTargets.length === 0
|
|
46
|
+
? "a"
|
|
47
|
+
: namedWorktreeTargets.length === 1
|
|
48
|
+
? `${namedWorktreeTargets[0]} or another`
|
|
49
|
+
: `${namedWorktreeTargets.slice(0, -1).join(", ")}, ${namedWorktreeTargets.at(-1)}, or another`;
|
|
30
50
|
|
|
31
51
|
const dispatchRules = [
|
|
32
52
|
"Handle simple work inline with direct tools: one-line lookups, known-target reads/edits, and quick questions do not justify a child process.",
|
|
@@ -40,12 +60,17 @@ export function buildDelegationDirective(agents: AgentConfig[]): string {
|
|
|
40
60
|
: []),
|
|
41
61
|
...(hasCleaner
|
|
42
62
|
? [
|
|
43
|
-
`Use \`cleaner\` only
|
|
63
|
+
`Use \`cleaner\` only for user-authorized cleanup, removal, simplification, duplicate-code consolidation, or maintenance. Once dispatched, it applies every safe proven in-scope cut without item-by-item approval; zero edits is valid only if none is proved. Generic or read-only audit, review, code-health, plan, or cleanup-candidate assessment goes to ${hasReviewer ? "`reviewer`" : "direct main-context inspection because `reviewer` is disabled"}. Never dispatch cleaner by PR count or as the pre-commit gate.`,
|
|
64
|
+
]
|
|
65
|
+
: []),
|
|
66
|
+
...(hasDocumenter
|
|
67
|
+
? [
|
|
68
|
+
`Use \`documenter\` directly for explicit whole-codebase maintenance or standalone documentation work.${codeWriterNames.length > 0 ? ` Successful ${codeWriterNames.join("/")} runs already auto-sync the actual diff; never dispatch a duplicate.` : ""} Zero edits is valid and broad mode is never inferred. It changes docs/comments only and never runtime behavior, versions, release state, or ${hasReviewer ? "the final reviewer gate" : "direct final verification"}.`,
|
|
44
69
|
]
|
|
45
70
|
: []),
|
|
46
71
|
...(hasReviewer
|
|
47
72
|
? [
|
|
48
|
-
`Use \`reviewer\` for
|
|
73
|
+
`Use \`reviewer\` for read-only assessments or an explicit gate.${reviewedWriterNames.length > 0 ? ` Successful ${reviewedWriterNames.join("/")} runs already get a fresh read-only reviewer gate.` : ""} Advisory output has no VERDICT: it stays read-only and does not authorize follow-up edits.`,
|
|
49
74
|
]
|
|
50
75
|
: []),
|
|
51
76
|
"Brief every child with the complete goal, exact paths, constraints, and expected output. It has no memory of this conversation.",
|
|
@@ -55,7 +80,7 @@ export function buildDelegationDirective(agents: AgentConfig[]): string {
|
|
|
55
80
|
"Dispatch independent work in one `tasks` array and let the resumed main agent start dependent work only after prerequisites finish.",
|
|
56
81
|
]
|
|
57
82
|
: []),
|
|
58
|
-
`Filesystem isolation: single tasks default to shared${hasWorker ? "; parallel worker tasks default to detached Git worktrees" : ""}${hasCleaner ? "; cleaner defaults to shared" : ""}. Request \`isolation: "worktree"\` only for ${worktreeTargets} write-capable agent in a Git repository with committed HEAD. Read-only agents reject it, and setup/integration failure never falls back silently to shared.`,
|
|
83
|
+
`Filesystem isolation: single tasks default to shared${hasWorker ? "; parallel worker tasks default to detached Git worktrees" : ""}${hasCleaner ? "; cleaner defaults to shared" : ""}${hasDocumenter ? "; documenter defaults to shared" : ""}. Request \`isolation: "worktree"\` only for ${worktreeTargets} write-capable agent in a Git repository with committed HEAD. Read-only agents reject it, and setup/integration failure never falls back silently to shared.`,
|
|
59
84
|
"A configured child model/provider failure automatically continues the same retained session on the current main model; do not redispatch. Ordinary tool/task failures stay on the selected model.",
|
|
60
85
|
"Trust but verify: inspect actual changes/results before reporting completion.",
|
|
61
86
|
];
|
|
@@ -69,9 +94,19 @@ export function buildDelegationDirective(agents: AgentConfig[]): string {
|
|
|
69
94
|
|
|
70
95
|
const verificationRules = [
|
|
71
96
|
"Never report an unrun check as passed; identify unavailable checks and pre-existing failures honestly.",
|
|
97
|
+
...(automaticWriterRoute && reviewedWriterNames.length > 0
|
|
98
|
+
? [
|
|
99
|
+
`Successful top-level write roles automatically continue through enabled downstream roles (${automaticWriterRoute}) to one final delivery; never duplicate stages.`,
|
|
100
|
+
]
|
|
101
|
+
: []),
|
|
72
102
|
...(hasReviewer
|
|
73
103
|
? [
|
|
74
|
-
|
|
104
|
+
...(hasDocumenter
|
|
105
|
+
? [
|
|
106
|
+
`A direct REVIEW_PASS is preliminary: runtime runs documenter on the pending diff, then a fresh reviewer. A direct REVIEW_FAIL ${autoFixEnabled ? "keeps auto-fix; maxFixRounds limits worker fixes only, not initial docs/review." : "cannot start fixes while worker/fix rounds are disabled."}`,
|
|
107
|
+
]
|
|
108
|
+
: []),
|
|
109
|
+
"Resolve every gate finding; do not bypass the configured auto-fix/re-review cap. A reviewer report without a standalone VERDICT is advisory and cannot trigger writes.",
|
|
75
110
|
"Use multi-model cross-review only when explicitly requested or for genuinely high-risk security, unsafe/FFI, persistence-migration, or concurrency changes.",
|
|
76
111
|
]
|
|
77
112
|
: []),
|