@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/completion.ts
CHANGED
|
@@ -1,160 +1,160 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Smart batching for successful background completions.
|
|
3
|
-
*
|
|
4
|
-
* A short debounce coalesces sibling runs while a max-wait timer, measured from
|
|
5
|
-
* the first item in the open group, bounds delivery latency. Failures are
|
|
6
|
-
* intentionally handled by the caller: flush held successes, then emit the
|
|
7
|
-
* failure directly so it is never delayed.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
11
|
-
import { formatUsageCompact, sumUsage } from "./monitor.ts";
|
|
12
|
-
import type { UsageStats } from "./rpc-run.ts";
|
|
13
|
-
|
|
14
|
-
export interface CompletionBatchTimings {
|
|
15
|
-
debounceMs: number;
|
|
16
|
-
maxWaitMs: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const DEFAULT_COMPLETION_BATCH_TIMINGS: CompletionBatchTimings = {
|
|
20
|
-
debounceMs: 150,
|
|
21
|
-
maxWaitMs: 1_000,
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
type TimerHandle = ReturnType<typeof setTimeout>;
|
|
25
|
-
|
|
26
|
-
function unrefHandle(handle: TimerHandle): void {
|
|
27
|
-
if (
|
|
28
|
-
handle &&
|
|
29
|
-
typeof handle === "object" &&
|
|
30
|
-
"unref" in handle &&
|
|
31
|
-
typeof (handle as { unref: unknown }).unref === "function"
|
|
32
|
-
) {
|
|
33
|
-
(handle as { unref: () => void }).unref();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface CompletionBatcherOptions<T> {
|
|
38
|
-
emit: (items: T[]) => void;
|
|
39
|
-
timings?: Partial<CompletionBatchTimings>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface CompletionBatcher<T> {
|
|
43
|
-
/** Add an item to the current debounced group. */
|
|
44
|
-
push(item: T): void;
|
|
45
|
-
/** Emit any held items immediately as one group. */
|
|
46
|
-
flush(): void;
|
|
47
|
-
/** Clear timers and return held items without emitting them. */
|
|
48
|
-
dispose(): T[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {
|
|
52
|
-
const timings = { ...DEFAULT_COMPLETION_BATCH_TIMINGS, ...options.timings };
|
|
53
|
-
let pending: T[] = [];
|
|
54
|
-
let debounceTimer: TimerHandle | null = null;
|
|
55
|
-
let maxWaitTimer: TimerHandle | null = null;
|
|
56
|
-
|
|
57
|
-
const clearTimers = (): void => {
|
|
58
|
-
if (debounceTimer !== null) {
|
|
59
|
-
clearTimeout(debounceTimer);
|
|
60
|
-
debounceTimer = null;
|
|
61
|
-
}
|
|
62
|
-
if (maxWaitTimer !== null) {
|
|
63
|
-
clearTimeout(maxWaitTimer);
|
|
64
|
-
maxWaitTimer = null;
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const emitGroup = (): void => {
|
|
69
|
-
clearTimers();
|
|
70
|
-
if (pending.length === 0) return;
|
|
71
|
-
const items = pending;
|
|
72
|
-
pending = [];
|
|
73
|
-
options.emit(items);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
push(item: T): void {
|
|
78
|
-
pending.push(item);
|
|
79
|
-
|
|
80
|
-
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
81
|
-
debounceTimer = setTimeout(emitGroup, timings.debounceMs);
|
|
82
|
-
unrefHandle(debounceTimer);
|
|
83
|
-
|
|
84
|
-
if (maxWaitTimer === null) {
|
|
85
|
-
maxWaitTimer = setTimeout(emitGroup, timings.maxWaitMs);
|
|
86
|
-
unrefHandle(maxWaitTimer);
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
flush: emitGroup,
|
|
90
|
-
dispose(): T[] {
|
|
91
|
-
clearTimers();
|
|
92
|
-
const abandoned = pending;
|
|
93
|
-
pending = [];
|
|
94
|
-
return abandoned;
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export interface CompletionMessageItem {
|
|
100
|
-
agent: string;
|
|
101
|
-
block: string;
|
|
102
|
-
triggerTurn: boolean;
|
|
103
|
-
/** Final usage of the underlying run (or chain); aggregated into the group totals. */
|
|
104
|
-
usage?: UsageStats;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Keep the established single-result shape; add a group header and an aggregate
|
|
108
|
-
* token/cost footer only for real groups. */
|
|
109
|
-
export function formatCompletionMessage(items: readonly CompletionMessageItem[]): string {
|
|
110
|
-
if (items.length === 0) return "";
|
|
111
|
-
if (items.length === 1) return items[0].block;
|
|
112
|
-
const agents = items.map((item) => item.agent).join(", ");
|
|
113
|
-
const withUsage = items.filter((item) => item.usage !== undefined);
|
|
114
|
-
const totals = withUsage.length > 0 ? formatUsageCompact(sumUsage(withUsage.map((item) => item.usage!))) : "";
|
|
115
|
-
const footer = totals ? `\n\nTotals: ${items.length} runs · ${totals}` : "";
|
|
116
|
-
return `### Subagents completed (${items.length}): ${agents}\n\n${items.map((item) => item.block).join("\n\n")}${footer}`;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** A grouped completion wakes the main agent when any member requires a turn. */
|
|
120
|
-
export function completionGroupTriggersTurn(items: readonly CompletionMessageItem[]): boolean {
|
|
121
|
-
return items.some((item) => item.triggerTurn);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Passing reviewer notifications may opt out of waking; every other result wakes. */
|
|
125
|
-
export function completionTriggersTurn(result: SingleResult, notifyOnReviewPass: boolean): boolean {
|
|
126
|
-
if (isFailedResult(result)) return true;
|
|
127
|
-
return !(
|
|
128
|
-
notifyOnReviewPass &&
|
|
129
|
-
result.agent === "reviewer" &&
|
|
130
|
-
reviewVerdict(getResultOutput(result)) === "pass"
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Minimal shape of an active run, for the "others still running" footer. Kept
|
|
135
|
-
* decoupled from the monitor's RunView so this stays a pure, easily tested
|
|
136
|
-
* formatter; the caller maps its live runs into this shape. */
|
|
137
|
-
export interface ActiveRunFoot {
|
|
138
|
-
id: number;
|
|
139
|
-
agent: string;
|
|
140
|
-
/** Optional content label (task-derived) shown next to the agent name. */
|
|
141
|
-
label?: string;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Footer appended to a completion message when OTHER runs are still active, so
|
|
146
|
-
* the main agent does not declare the overall task done prematurely. A result
|
|
147
|
-
* arriving for one run does not mean sibling runs are finished; naming them
|
|
148
|
-
* gives the main agent concrete, in-context awareness to keep waiting.
|
|
149
|
-
*
|
|
150
|
-
* Returns "" when nothing is active (the common, single-run case stays quiet).
|
|
151
|
-
*/
|
|
152
|
-
export function formatActiveRunsFooter(runs: readonly ActiveRunFoot[], maxListed = 4): string {
|
|
153
|
-
if (runs.length === 0) return "";
|
|
154
|
-
const listed = runs.slice(0, maxListed);
|
|
155
|
-
const items = listed
|
|
156
|
-
.map((run) => `#${run.id} ${run.agent}${run.label ? `·${run.label}` : ""}`)
|
|
157
|
-
.join(", ");
|
|
158
|
-
const more = runs.length > listed.length ? `, +${runs.length - listed.length} more` : "";
|
|
159
|
-
return `\n\n⚠ ${runs.length} other run${runs.length === 1 ? "" : "s"} still active: ${items}${more}. Do not conclude the overall task yet — wait for their results (they wake you automatically) or check subagent_status.`;
|
|
160
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Smart batching for successful background completions.
|
|
3
|
+
*
|
|
4
|
+
* A short debounce coalesces sibling runs while a max-wait timer, measured from
|
|
5
|
+
* the first item in the open group, bounds delivery latency. Failures are
|
|
6
|
+
* intentionally handled by the caller: flush held successes, then emit the
|
|
7
|
+
* failure directly so it is never delayed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
11
|
+
import { formatUsageCompact, sumUsage } from "./monitor.ts";
|
|
12
|
+
import type { UsageStats } from "./rpc-run.ts";
|
|
13
|
+
|
|
14
|
+
export interface CompletionBatchTimings {
|
|
15
|
+
debounceMs: number;
|
|
16
|
+
maxWaitMs: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_COMPLETION_BATCH_TIMINGS: CompletionBatchTimings = {
|
|
20
|
+
debounceMs: 150,
|
|
21
|
+
maxWaitMs: 1_000,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type TimerHandle = ReturnType<typeof setTimeout>;
|
|
25
|
+
|
|
26
|
+
function unrefHandle(handle: TimerHandle): void {
|
|
27
|
+
if (
|
|
28
|
+
handle &&
|
|
29
|
+
typeof handle === "object" &&
|
|
30
|
+
"unref" in handle &&
|
|
31
|
+
typeof (handle as { unref: unknown }).unref === "function"
|
|
32
|
+
) {
|
|
33
|
+
(handle as { unref: () => void }).unref();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CompletionBatcherOptions<T> {
|
|
38
|
+
emit: (items: T[]) => void;
|
|
39
|
+
timings?: Partial<CompletionBatchTimings>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CompletionBatcher<T> {
|
|
43
|
+
/** Add an item to the current debounced group. */
|
|
44
|
+
push(item: T): void;
|
|
45
|
+
/** Emit any held items immediately as one group. */
|
|
46
|
+
flush(): void;
|
|
47
|
+
/** Clear timers and return held items without emitting them. */
|
|
48
|
+
dispose(): T[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {
|
|
52
|
+
const timings = { ...DEFAULT_COMPLETION_BATCH_TIMINGS, ...options.timings };
|
|
53
|
+
let pending: T[] = [];
|
|
54
|
+
let debounceTimer: TimerHandle | null = null;
|
|
55
|
+
let maxWaitTimer: TimerHandle | null = null;
|
|
56
|
+
|
|
57
|
+
const clearTimers = (): void => {
|
|
58
|
+
if (debounceTimer !== null) {
|
|
59
|
+
clearTimeout(debounceTimer);
|
|
60
|
+
debounceTimer = null;
|
|
61
|
+
}
|
|
62
|
+
if (maxWaitTimer !== null) {
|
|
63
|
+
clearTimeout(maxWaitTimer);
|
|
64
|
+
maxWaitTimer = null;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const emitGroup = (): void => {
|
|
69
|
+
clearTimers();
|
|
70
|
+
if (pending.length === 0) return;
|
|
71
|
+
const items = pending;
|
|
72
|
+
pending = [];
|
|
73
|
+
options.emit(items);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
push(item: T): void {
|
|
78
|
+
pending.push(item);
|
|
79
|
+
|
|
80
|
+
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
81
|
+
debounceTimer = setTimeout(emitGroup, timings.debounceMs);
|
|
82
|
+
unrefHandle(debounceTimer);
|
|
83
|
+
|
|
84
|
+
if (maxWaitTimer === null) {
|
|
85
|
+
maxWaitTimer = setTimeout(emitGroup, timings.maxWaitMs);
|
|
86
|
+
unrefHandle(maxWaitTimer);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
flush: emitGroup,
|
|
90
|
+
dispose(): T[] {
|
|
91
|
+
clearTimers();
|
|
92
|
+
const abandoned = pending;
|
|
93
|
+
pending = [];
|
|
94
|
+
return abandoned;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface CompletionMessageItem {
|
|
100
|
+
agent: string;
|
|
101
|
+
block: string;
|
|
102
|
+
triggerTurn: boolean;
|
|
103
|
+
/** Final usage of the underlying run (or chain); aggregated into the group totals. */
|
|
104
|
+
usage?: UsageStats;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Keep the established single-result shape; add a group header and an aggregate
|
|
108
|
+
* token/cost footer only for real groups. */
|
|
109
|
+
export function formatCompletionMessage(items: readonly CompletionMessageItem[]): string {
|
|
110
|
+
if (items.length === 0) return "";
|
|
111
|
+
if (items.length === 1) return items[0].block;
|
|
112
|
+
const agents = items.map((item) => item.agent).join(", ");
|
|
113
|
+
const withUsage = items.filter((item) => item.usage !== undefined);
|
|
114
|
+
const totals = withUsage.length > 0 ? formatUsageCompact(sumUsage(withUsage.map((item) => item.usage!))) : "";
|
|
115
|
+
const footer = totals ? `\n\nTotals: ${items.length} runs · ${totals}` : "";
|
|
116
|
+
return `### Subagents completed (${items.length}): ${agents}\n\n${items.map((item) => item.block).join("\n\n")}${footer}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A grouped completion wakes the main agent when any member requires a turn. */
|
|
120
|
+
export function completionGroupTriggersTurn(items: readonly CompletionMessageItem[]): boolean {
|
|
121
|
+
return items.some((item) => item.triggerTurn);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Passing reviewer notifications may opt out of waking; every other result wakes. */
|
|
125
|
+
export function completionTriggersTurn(result: SingleResult, notifyOnReviewPass: boolean): boolean {
|
|
126
|
+
if (isFailedResult(result)) return true;
|
|
127
|
+
return !(
|
|
128
|
+
notifyOnReviewPass &&
|
|
129
|
+
result.agent === "reviewer" &&
|
|
130
|
+
reviewVerdict(getResultOutput(result)) === "pass"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Minimal shape of an active run, for the "others still running" footer. Kept
|
|
135
|
+
* decoupled from the monitor's RunView so this stays a pure, easily tested
|
|
136
|
+
* formatter; the caller maps its live runs into this shape. */
|
|
137
|
+
export interface ActiveRunFoot {
|
|
138
|
+
id: number;
|
|
139
|
+
agent: string;
|
|
140
|
+
/** Optional content label (task-derived) shown next to the agent name. */
|
|
141
|
+
label?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Footer appended to a completion message when OTHER runs are still active, so
|
|
146
|
+
* the main agent does not declare the overall task done prematurely. A result
|
|
147
|
+
* arriving for one run does not mean sibling runs are finished; naming them
|
|
148
|
+
* gives the main agent concrete, in-context awareness to keep waiting.
|
|
149
|
+
*
|
|
150
|
+
* Returns "" when nothing is active (the common, single-run case stays quiet).
|
|
151
|
+
*/
|
|
152
|
+
export function formatActiveRunsFooter(runs: readonly ActiveRunFoot[], maxListed = 4): string {
|
|
153
|
+
if (runs.length === 0) return "";
|
|
154
|
+
const listed = runs.slice(0, maxListed);
|
|
155
|
+
const items = listed
|
|
156
|
+
.map((run) => `#${run.id} ${run.agent}${run.label ? `·${run.label}` : ""}`)
|
|
157
|
+
.join(", ");
|
|
158
|
+
const more = runs.length > listed.length ? `, +${runs.length - listed.length} more` : "";
|
|
159
|
+
return `\n\n⚠ ${runs.length} other run${runs.length === 1 ? "" : "s"} still active: ${items}${more}. Do not conclude the overall task yet — wait for their results (they wake you automatically) or check subagent_status.`;
|
|
160
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -17,9 +17,10 @@ import { dirname, join } from "node:path";
|
|
|
17
17
|
import { getAgentDir, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
|
|
19
19
|
/** Full catalog of agents shipped with the package (selectable in /subagents-setup). */
|
|
20
|
-
export const BUILTIN_AGENT_NAMES = ["explorer", "worker", "cleaner", "reviewer"] as const;
|
|
20
|
+
export const BUILTIN_AGENT_NAMES = ["explorer", "worker", "cleaner", "documenter", "reviewer"] as const;
|
|
21
21
|
|
|
22
|
-
/** Agents enabled out of the box on a fresh install.
|
|
22
|
+
/** Agents enabled out of the box on a fresh install. Documenter remains an
|
|
23
|
+
* explicit setup choice; existing non-empty configs receive it via migration. */
|
|
23
24
|
export const DEFAULT_ENABLED_AGENTS: readonly string[] = ["explorer", "worker", "cleaner", "reviewer"];
|
|
24
25
|
|
|
25
26
|
export const AGENT_SCOPE_VALUES = ["user", "project", "both"] as const;
|
|
@@ -27,6 +28,28 @@ export type AgentScope = (typeof AGENT_SCOPE_VALUES)[number];
|
|
|
27
28
|
|
|
28
29
|
const LEGACY_EXPLORER_NAME = "explore";
|
|
29
30
|
const EXPLORER_NAME = "explorer";
|
|
31
|
+
const CLEANER_NAME = "cleaner";
|
|
32
|
+
const DOCUMENTER_NAME = "documenter";
|
|
33
|
+
const REVIEWER_NAME = "reviewer";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Stamps recorded in `announcedFeatures` by the one-time upgrade that defaults
|
|
37
|
+
* cleaner on for configs written before it shipped. The first marks a config as
|
|
38
|
+
* processed, so a later deliberate disable is not undone; the second records
|
|
39
|
+
* that cleaner was actually injected, so session start can tell the user once;
|
|
40
|
+
* the third records that reviewer model/thinking settings were actually copied,
|
|
41
|
+
* so that notice never claims an inheritance that did not happen.
|
|
42
|
+
*/
|
|
43
|
+
export const CLEANER_DEFAULTED_FEATURE = "cleanerDefaulted";
|
|
44
|
+
export const CLEANER_AUTO_ENABLED_FEATURE = "cleanerAutoEnabled";
|
|
45
|
+
export const CLEANER_INHERITED_FEATURE = "cleanerInheritedReviewer";
|
|
46
|
+
|
|
47
|
+
/** One-time upgrade stamps for the pre-commit documenter role. Existing
|
|
48
|
+
* non-empty configs gain it before reviewer and inherit explorer routing; fresh
|
|
49
|
+
* installs keep it off until setup explicitly enables it. */
|
|
50
|
+
export const DOCUMENTER_DEFAULTED_FEATURE = "documenterDefaulted";
|
|
51
|
+
export const DOCUMENTER_AUTO_ENABLED_FEATURE = "documenterAutoEnabled";
|
|
52
|
+
export const DOCUMENTER_INHERITED_FEATURE = "documenterInheritedExplorer";
|
|
30
53
|
|
|
31
54
|
function migrateAgentName(name: string): string {
|
|
32
55
|
return name === LEGACY_EXPLORER_NAME ? EXPLORER_NAME : name;
|
|
@@ -49,9 +72,9 @@ export const DEFAULT_MAX_CONCURRENCY = 4;
|
|
|
49
72
|
/** Upper bound accepted for maxConcurrency (defensive clamp). */
|
|
50
73
|
export const MAX_CONCURRENCY_LIMIT = 16;
|
|
51
74
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
75
|
+
* Maximum worker fixes after REVIEW_FAIL. Each fix is followed by optional
|
|
76
|
+
* documenter and reviewer; this cap does not suppress the initial post-writer
|
|
77
|
+
* documentation/final-review workflow. 0 disables fixes. Default: 2.
|
|
55
78
|
*/
|
|
56
79
|
export const DEFAULT_MAX_FIX_ROUNDS = 2;
|
|
57
80
|
/** Upper bound accepted for maxFixRounds (defensive clamp). 0 disables the loop. */
|
|
@@ -74,8 +97,8 @@ export interface SubagentsConfig {
|
|
|
74
97
|
/** Optional per-agent thinking preference. Runtime clamps it to the effective model's supported levels. */
|
|
75
98
|
agentThinkingLevels: Record<string, ThinkingLevel>;
|
|
76
99
|
/**
|
|
77
|
-
* When a review passes (REVIEW_PASS verdict), deliver it without
|
|
78
|
-
* main agent.
|
|
100
|
+
* When a standalone review passes (REVIEW_PASS verdict), deliver it without
|
|
101
|
+
* waking the main agent. Managed workflows always wake once at final delivery.
|
|
79
102
|
*/
|
|
80
103
|
notifyOnReviewPass: boolean;
|
|
81
104
|
/**
|
|
@@ -92,11 +115,9 @@ export interface SubagentsConfig {
|
|
|
92
115
|
* one parallel `subagent` call may contain. Default: 4. */
|
|
93
116
|
maxConcurrency: number;
|
|
94
117
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* the full chain. 0 disables it (the main agent handles fixes itself).
|
|
99
|
-
* Default: 2.
|
|
118
|
+
* Maximum worker fixes after REVIEW_FAIL. Every fix receives the full review,
|
|
119
|
+
* then enabled documenter/reviewer stages run. Initial post-writer docs/review
|
|
120
|
+
* do not consume this budget. 0 disables fixes. Default: 2.
|
|
100
121
|
*/
|
|
101
122
|
maxFixRounds: number;
|
|
102
123
|
/**
|
|
@@ -106,8 +127,9 @@ export interface SubagentsConfig {
|
|
|
106
127
|
*/
|
|
107
128
|
idleTimeoutSec: number;
|
|
108
129
|
/**
|
|
109
|
-
* One-time feature announcements already shown to the user
|
|
110
|
-
* the
|
|
130
|
+
* One-time feature announcements already shown to the user, plus schema
|
|
131
|
+
* upgrade stamps (e.g. the cleaner default-enable upgrade). Persisted so
|
|
132
|
+
* notices and migrations never repeat.
|
|
111
133
|
*/
|
|
112
134
|
announcedFeatures: string[];
|
|
113
135
|
}
|
|
@@ -225,7 +247,7 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
225
247
|
const maxConcurrency = clampCount(raw.maxConcurrency, MAX_CONCURRENCY_LIMIT);
|
|
226
248
|
if (maxConcurrency !== undefined) config.maxConcurrency = maxConcurrency;
|
|
227
249
|
|
|
228
|
-
// 0 disables
|
|
250
|
+
// 0 disables worker fixes, not the initial managed docs/review workflow.
|
|
229
251
|
if (typeof raw.maxFixRounds === "number" && Number.isFinite(raw.maxFixRounds)) {
|
|
230
252
|
config.maxFixRounds = Math.max(0, Math.min(MAX_FIX_ROUNDS_LIMIT, Math.round(raw.maxFixRounds)));
|
|
231
253
|
}
|
|
@@ -241,6 +263,55 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
241
263
|
);
|
|
242
264
|
}
|
|
243
265
|
|
|
266
|
+
// One-time upgrade for configs written before cleaner shipped: a non-empty
|
|
267
|
+
// explicit enabledAgents list gets cleaner defaulted on (inserted before
|
|
268
|
+
// reviewer, matching the fresh-install order) and inherits the reviewer's
|
|
269
|
+
// configured model and thinking level — its closest peer. The stamps make
|
|
270
|
+
// the upgrade idempotent and keep a later deliberate disable from being undone.
|
|
271
|
+
if (!config.announcedFeatures.includes(CLEANER_DEFAULTED_FEATURE)) {
|
|
272
|
+
config.announcedFeatures.push(CLEANER_DEFAULTED_FEATURE);
|
|
273
|
+
if (config.enabledAgents.length > 0 && !config.enabledAgents.includes(CLEANER_NAME)) {
|
|
274
|
+
const reviewerIndex = config.enabledAgents.indexOf(REVIEWER_NAME);
|
|
275
|
+
config.enabledAgents.splice(reviewerIndex === -1 ? config.enabledAgents.length : reviewerIndex, 0, CLEANER_NAME);
|
|
276
|
+
config.announcedFeatures.push(CLEANER_AUTO_ENABLED_FEATURE);
|
|
277
|
+
let inherited = false;
|
|
278
|
+
if (!config.agentModels[CLEANER_NAME] && config.agentModels[REVIEWER_NAME]) {
|
|
279
|
+
config.agentModels[CLEANER_NAME] = config.agentModels[REVIEWER_NAME];
|
|
280
|
+
inherited = true;
|
|
281
|
+
}
|
|
282
|
+
if (!config.agentThinkingLevels[CLEANER_NAME] && config.agentThinkingLevels[REVIEWER_NAME]) {
|
|
283
|
+
config.agentThinkingLevels[CLEANER_NAME] = config.agentThinkingLevels[REVIEWER_NAME];
|
|
284
|
+
inherited = true;
|
|
285
|
+
}
|
|
286
|
+
// An old explicit list may have no reviewer overrides to copy; only the
|
|
287
|
+
// copied case is stamped so the one-time notice stays accurate.
|
|
288
|
+
if (inherited) config.announcedFeatures.push(CLEANER_INHERITED_FEATURE);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!config.announcedFeatures.includes(DOCUMENTER_DEFAULTED_FEATURE)) {
|
|
293
|
+
config.announcedFeatures.push(DOCUMENTER_DEFAULTED_FEATURE);
|
|
294
|
+
if (config.enabledAgents.length > 0 && !config.enabledAgents.includes(DOCUMENTER_NAME)) {
|
|
295
|
+
const reviewerIndex = config.enabledAgents.indexOf(REVIEWER_NAME);
|
|
296
|
+
config.enabledAgents.splice(
|
|
297
|
+
reviewerIndex === -1 ? config.enabledAgents.length : reviewerIndex,
|
|
298
|
+
0,
|
|
299
|
+
DOCUMENTER_NAME,
|
|
300
|
+
);
|
|
301
|
+
config.announcedFeatures.push(DOCUMENTER_AUTO_ENABLED_FEATURE);
|
|
302
|
+
let inherited = false;
|
|
303
|
+
if (!config.agentModels[DOCUMENTER_NAME] && config.agentModels[EXPLORER_NAME]) {
|
|
304
|
+
config.agentModels[DOCUMENTER_NAME] = config.agentModels[EXPLORER_NAME];
|
|
305
|
+
inherited = true;
|
|
306
|
+
}
|
|
307
|
+
if (!config.agentThinkingLevels[DOCUMENTER_NAME] && config.agentThinkingLevels[EXPLORER_NAME]) {
|
|
308
|
+
config.agentThinkingLevels[DOCUMENTER_NAME] = config.agentThinkingLevels[EXPLORER_NAME];
|
|
309
|
+
inherited = true;
|
|
310
|
+
}
|
|
311
|
+
if (inherited) config.announcedFeatures.push(DOCUMENTER_INHERITED_FEATURE);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
244
315
|
return config;
|
|
245
316
|
}
|
|
246
317
|
|