@ferris1225/pi-subagents 4.1.3 → 4.1.5
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 +110 -99
- package/agents/cleaner.md +3 -2
- package/agents/documenter.md +6 -6
- package/agents/reviewer.md +8 -3
- package/agents/worker.md +4 -4
- package/package.json +1 -1
- package/src/completion.ts +160 -160
- package/src/config.ts +8 -6
- package/src/dispatch.ts +173 -65
- package/src/fixloop.ts +108 -102
- package/src/models.ts +189 -189
- package/src/monitor.ts +25 -0
- package/src/prompt.ts +24 -26
- package/src/recovery.ts +145 -145
- package/src/session-fork.ts +80 -80
- package/src/thread-lifecycle.ts +19 -5
- package/src/widget.ts +59 -8
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
|
@@ -20,6 +20,15 @@ import type { IsolationMode, WorktreeFinalizationStatus } from "./worktree.ts";
|
|
|
20
20
|
|
|
21
21
|
export type RunStatus = "queued" | "running" | "steering" | "interrupting" | "parked" | "done" | "failed";
|
|
22
22
|
export type ContinuationKind = "resume-retained" | "resume-appended" | "fork-retained" | "fork-appended" | "retarget";
|
|
23
|
+
export type WorkflowStageStatus = "done" | "active" | "pending" | "changes" | "failed";
|
|
24
|
+
|
|
25
|
+
/** Ephemeral projection of one real or currently planned managed stage. It is
|
|
26
|
+
* live monitor state only; durable results remain the per-run chain records. */
|
|
27
|
+
export interface WorkflowStage {
|
|
28
|
+
agent: string;
|
|
29
|
+
relation: string;
|
|
30
|
+
status: WorkflowStageStatus;
|
|
31
|
+
}
|
|
23
32
|
|
|
24
33
|
export function isRunActiveStatus(status: RunStatus): boolean {
|
|
25
34
|
return status === "queued" || status === "running" || status === "steering" || status === "interrupting";
|
|
@@ -65,6 +74,9 @@ export interface RunView {
|
|
|
65
74
|
/** This stable top-level row currently owns a multi-stage managed workflow.
|
|
66
75
|
* Its elapsed time is workflow-wide; active child rows own stage telemetry. */
|
|
67
76
|
managedWorkflow?: boolean;
|
|
77
|
+
/** Live-only stage timeline retained on the parent while completed internal
|
|
78
|
+
* child rows leave the monitor. */
|
|
79
|
+
workflowStages?: WorkflowStage[];
|
|
68
80
|
}
|
|
69
81
|
|
|
70
82
|
/** Optional metadata for documenter/reviewer/fix children of a stable parent run. */
|
|
@@ -481,6 +493,18 @@ export class MonitorStore {
|
|
|
481
493
|
const run = this.find(id);
|
|
482
494
|
if (!run) return;
|
|
483
495
|
run.managedWorkflow = active || undefined;
|
|
496
|
+
if (!active) run.workflowStages = undefined;
|
|
497
|
+
this.notify();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Replace the live workflow projection atomically so renderers never observe
|
|
501
|
+
* a half-updated fix/re-review plan. */
|
|
502
|
+
setWorkflowStages(id: number, stages: readonly WorkflowStage[]): void {
|
|
503
|
+
const run = this.find(id);
|
|
504
|
+
if (!run) return;
|
|
505
|
+
run.workflowStages = stages.length > 0
|
|
506
|
+
? stages.map((stage) => ({ ...stage }))
|
|
507
|
+
: undefined;
|
|
484
508
|
this.notify();
|
|
485
509
|
}
|
|
486
510
|
|
|
@@ -615,6 +639,7 @@ export class MonitorStore {
|
|
|
615
639
|
run.usage = emptyUsage();
|
|
616
640
|
run.activity = undefined;
|
|
617
641
|
run.managedWorkflow = undefined;
|
|
642
|
+
run.workflowStages = undefined;
|
|
618
643
|
run.activeSince = undefined;
|
|
619
644
|
run.endedAt = undefined;
|
|
620
645
|
run.elapsedMs = Math.max(run.elapsedMs, meta?.elapsedMs ?? 0);
|
package/src/prompt.ts
CHANGED
|
@@ -29,14 +29,7 @@ export function buildDelegationDirective(
|
|
|
29
29
|
...(hasWorker ? ["worker"] : []),
|
|
30
30
|
...(hasCleaner ? ["cleaner"] : []),
|
|
31
31
|
];
|
|
32
|
-
const reviewedWriterNames = [
|
|
33
|
-
...codeWriterNames,
|
|
34
|
-
...(hasDocumenter ? ["documenter"] : []),
|
|
35
|
-
];
|
|
36
|
-
const automaticWriterRoute = [
|
|
37
|
-
...(hasDocumenter ? ["documenter"] : []),
|
|
38
|
-
...(hasReviewer ? ["reviewer"] : []),
|
|
39
|
-
].join(" → ");
|
|
32
|
+
const reviewedWriterNames = [...codeWriterNames];
|
|
40
33
|
const namedWorktreeTargets = [
|
|
41
34
|
...(hasWorker ? ["worker"] : []),
|
|
42
35
|
...(hasCleaner ? ["cleaner"] : []),
|
|
@@ -47,34 +40,43 @@ export function buildDelegationDirective(
|
|
|
47
40
|
: namedWorktreeTargets.length === 1
|
|
48
41
|
? `${namedWorktreeTargets[0]} or another`
|
|
49
42
|
: `${namedWorktreeTargets.slice(0, -1).join(", ")}, ${namedWorktreeTargets.at(-1)}, or another`;
|
|
43
|
+
const managedWriterWorkflowRule = reviewedWriterNames.length === 0
|
|
44
|
+
? undefined
|
|
45
|
+
: hasReviewer && hasDocumenter
|
|
46
|
+
? `Successful top-level ${reviewedWriterNames.join("/")} runs continue through the enabled reviewer gate. Only REVIEW_PASS can authorize documenter, which runs for DOCUMENTATION: NEEDED or a missing marker; the workflow delivers once. Never duplicate stages.`
|
|
47
|
+
: hasReviewer
|
|
48
|
+
? `Successful top-level ${reviewedWriterNames.join("/")} runs continue through the enabled reviewer gate and then deliver once; never duplicate the gate.`
|
|
49
|
+
: hasDocumenter
|
|
50
|
+
? `With reviewer disabled, successful top-level ${reviewedWriterNames.join("/")} runs use documenter as the conservative final fallback and then deliver once; never duplicate the fallback.`
|
|
51
|
+
: undefined;
|
|
50
52
|
|
|
51
53
|
const dispatchRules = [
|
|
52
|
-
"
|
|
54
|
+
"Keep small, known-target work in the main thread with direct tools: lookups and focused reads/edits do not justify a child context.",
|
|
53
55
|
...(hasExplorer
|
|
54
56
|
? [
|
|
55
|
-
"Use `explorer` proactively
|
|
57
|
+
"Use `explorer` proactively only for broad or cross-file reconnaissance: mapping unfamiliar code, tracing symbols/dependencies, or finding multi-file references. It is a lightweight retrieval index, never an automatic gate. Re-read load-bearing files before edits or high-risk decisions. Use a stronger model/specialist for dynamic, concurrent, migration, or security analysis.",
|
|
56
58
|
]
|
|
57
59
|
: []),
|
|
58
60
|
...(hasWorker
|
|
59
|
-
? ["Use `worker` for a self-contained implementation, fix, refactor, or test
|
|
61
|
+
? ["Use `worker` for a self-contained implementation, fix, refactor, or test whose separate context pays for itself—not a small known-target edit."]
|
|
60
62
|
: []),
|
|
61
63
|
...(hasCleaner
|
|
62
64
|
? [
|
|
63
|
-
`Use \`cleaner\` only for user-authorized cleanup, removal, simplification, duplicate-code consolidation, or maintenance
|
|
65
|
+
`Use \`cleaner\` only as the separate evidence-first entry for user-authorized cleanup, removal, simplification, duplicate-code consolidation, or maintenance; never substitute it for \`worker\`. It applies every safe proven in-scope cut without item-by-item approval. 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
66
|
]
|
|
65
67
|
: []),
|
|
66
68
|
...(hasDocumenter
|
|
67
69
|
? [
|
|
68
|
-
`Use \`documenter\` directly for explicit whole-codebase maintenance or standalone documentation work.${codeWriterNames.length > 0 ? `
|
|
70
|
+
`Use \`documenter\` directly only for explicit whole-codebase maintenance or standalone documentation/comment work; a top-level documenter delivers directly without an automatic reviewer.${codeWriterNames.length > 0 ? ` ${codeWriterNames.join("/")} must sync existing docs they directly affect; runtime runs documenter only after REVIEW_PASS with DOCUMENTATION: NEEDED or a missing marker, or as the reviewer-disabled fallback—never dispatch a duplicate.` : ""} It never changes runtime behavior, versions, or release state.`,
|
|
69
71
|
]
|
|
70
72
|
: []),
|
|
71
73
|
...(hasReviewer
|
|
72
74
|
? [
|
|
73
|
-
`Use \`reviewer\` for read-only assessments or
|
|
75
|
+
`Use \`reviewer\` for read-only assessments or a gate.${reviewedWriterNames.length > 0 ? ` Successful ${reviewedWriterNames.join("/")} runs already get one fresh read-only reviewer gate, independent of the writer.` : ""} Advisory output has no VERDICT and cannot authorize follow-up edits${hasDocumenter ? "; gates classify docs separately for the enabled documenter." : "."}`,
|
|
74
76
|
]
|
|
75
77
|
: []),
|
|
76
|
-
"Brief
|
|
77
|
-
"Children are leaf processes without delegation tools
|
|
78
|
+
"Brief each child with the complete goal, exact paths, constraints, and expected output; it has no conversation memory.",
|
|
79
|
+
"Children are leaf processes without delegation tools; use `subagent_control fork` on a parked/settled thread for an independent continuation.",
|
|
78
80
|
...(hasMultiple
|
|
79
81
|
? [
|
|
80
82
|
"Dispatch independent work in one `tasks` array and let the resumed main agent start dependent work only after prerequisites finish.",
|
|
@@ -86,24 +88,20 @@ export function buildDelegationDirective(
|
|
|
86
88
|
];
|
|
87
89
|
|
|
88
90
|
const handoffRules = [
|
|
89
|
-
"Dispatch
|
|
90
|
-
"Use `subagent_wait` with explicit `timeoutMs` only when the user
|
|
91
|
-
"
|
|
91
|
+
"Dispatch ends this turn; results resume the main agent, even mid-turn. Never sleep, poll, or call `subagent_wait` to hold the turn.",
|
|
92
|
+
"Use `subagent_wait` with explicit `timeoutMs` only when the user asks to wait in-turn; its default lookup is non-blocking.",
|
|
93
|
+
"Results are already shown. Do not restate, paraphrase, or re-summarize them; add only your conclusion or next action.",
|
|
92
94
|
"A delivered result does not mean siblings are finished. Before declaring the overall task done, use `subagent_status` to confirm that no runs remain active.",
|
|
93
95
|
];
|
|
94
96
|
|
|
95
97
|
const verificationRules = [
|
|
96
98
|
"Never report an unrun check as passed; identify unavailable checks and pre-existing failures honestly.",
|
|
97
|
-
...(
|
|
98
|
-
? [
|
|
99
|
-
`Successful top-level write roles automatically continue through enabled downstream roles (${automaticWriterRoute}) to one final delivery; never duplicate stages.`,
|
|
100
|
-
]
|
|
101
|
-
: []),
|
|
99
|
+
...(managedWriterWorkflowRule ? [managedWriterWorkflowRule] : []),
|
|
102
100
|
...(hasReviewer
|
|
103
101
|
? [
|
|
104
102
|
...(hasDocumenter
|
|
105
103
|
? [
|
|
106
|
-
`A direct REVIEW_PASS
|
|
104
|
+
`A direct REVIEW_PASS with DOCUMENTATION: CLEAN delivers immediately; NEEDED or a missing marker runs one documentation sync. A direct REVIEW_FAIL ${autoFixEnabled ? "keeps bounded worker/reviewer auto-fix, with docs considered only after its terminal REVIEW_PASS." : "cannot start fixes while worker/fix rounds are disabled."}`,
|
|
107
105
|
]
|
|
108
106
|
: []),
|
|
109
107
|
"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.",
|
|
@@ -116,7 +114,7 @@ export function buildDelegationDirective(
|
|
|
116
114
|
return `
|
|
117
115
|
## Sub-agent delegation (pi-subagents)
|
|
118
116
|
|
|
119
|
-
The \`subagent\` tool starts
|
|
117
|
+
The \`subagent\` tool starts isolated Pi child processes and context windows. Completions automatically resume the main agent.
|
|
120
118
|
|
|
121
119
|
Available agents:
|
|
122
120
|
${catalog}
|