@ferris1225/pi-subagents 2.3.1 → 4.0.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 +475 -468
- package/agents/cleaner.md +11 -21
- package/agents/{explore.md → explorer.md} +3 -3
- package/agents/reviewer.md +47 -41
- package/agents/worker.md +3 -3
- package/package.json +1 -1
- package/src/announcements.ts +0 -6
- package/src/config.ts +299 -310
- package/src/dispatch.ts +11 -65
- package/src/index.ts +1 -1
- package/src/models.ts +3 -9
- package/src/monitor.ts +1 -1
- package/src/prompt.ts +74 -61
- package/src/runtime.ts +0 -1
- package/src/setup.ts +438 -463
- package/src/thread-lifecycle.ts +4 -12
package/src/config.ts
CHANGED
|
@@ -1,310 +1,299 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configuration load/save for pi-subagents.
|
|
3
|
-
*
|
|
4
|
-
* Config lives at <agentDir>/pi-subagents.json (agentDir defaults to ~/.pi/agent
|
|
5
|
-
* and honors PI_CODING_AGENT_DIR). Parsing is defensive: invalid fields fall back
|
|
6
|
-
* to defaults instead of throwing, so a hand-edited or partially-written file can
|
|
7
|
-
* never break the extension at runtime.
|
|
8
|
-
*
|
|
9
|
-
* Schema upgrades happen transparently on load: a config written by an older
|
|
10
|
-
* version (missing newer keys or containing invalid
|
|
11
|
-
* values) is normalized and persisted back with the new fields filled in.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
15
|
-
import { readFileSync } from "node:fs";
|
|
16
|
-
import { dirname, join } from "node:path";
|
|
17
|
-
import { getAgentDir, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
|
|
19
|
-
/** Full catalog of agents shipped with the package (selectable in /subagents-setup). */
|
|
20
|
-
export const BUILTIN_AGENT_NAMES = ["
|
|
21
|
-
|
|
22
|
-
/** Agents enabled out of the box on a fresh install. Explicit configured lists are preserved. */
|
|
23
|
-
export const DEFAULT_ENABLED_AGENTS: readonly string[] = ["
|
|
24
|
-
|
|
25
|
-
export const AGENT_SCOPE_VALUES = ["user", "project", "both"] as const;
|
|
26
|
-
export type AgentScope = (typeof AGENT_SCOPE_VALUES)[number];
|
|
27
|
-
|
|
28
|
-
/** Thinking levels accepted by pi's `--thinking` option. */
|
|
29
|
-
export const THINKING_LEVEL_VALUES = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
30
|
-
export type ThinkingLevel = (typeof THINKING_LEVEL_VALUES)[number];
|
|
31
|
-
export const DEFAULT_THINKING_LEVEL: ThinkingLevel = "high";
|
|
32
|
-
|
|
33
|
-
/** How many lines of a sub-agent result the completion message may carry. Default: 80. */
|
|
34
|
-
export const DEFAULT_MAX_RESULT_LINES = 80;
|
|
35
|
-
/** Upper bound accepted for maxResultLines (defensive clamp). */
|
|
36
|
-
export const MAX_RESULT_LINES_LIMIT = 2000;
|
|
37
|
-
|
|
38
|
-
const CONFIG_FILE_NAME = "pi-subagents.json";
|
|
39
|
-
|
|
40
|
-
/** How many sub-agent processes may run at once, and how many tasks one parallel `subagent` call may contain. Default: 4. */
|
|
41
|
-
export const DEFAULT_MAX_CONCURRENCY = 4;
|
|
42
|
-
/** Upper bound accepted for maxConcurrency (defensive clamp). */
|
|
43
|
-
export const MAX_CONCURRENCY_LIMIT = 16;
|
|
44
|
-
/**
|
|
45
|
-
* How many automatic worker→reviewer fix rounds run when a reviewer returns
|
|
46
|
-
* REVIEW_FAIL before waking the main agent. 0 disables the auto-fix loop
|
|
47
|
-
* (the main agent is woken to dispatch fixes itself). Default: 2.
|
|
48
|
-
*/
|
|
49
|
-
export const DEFAULT_MAX_FIX_ROUNDS = 2;
|
|
50
|
-
/** Upper bound accepted for maxFixRounds (defensive clamp). 0 disables the loop. */
|
|
51
|
-
export const MAX_FIX_ROUNDS_LIMIT = 5;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Default idle timeout in seconds: a sub-agent whose stdout (JSON event stream)
|
|
55
|
-
* goes silent for this long is terminated; a selected model then hands the
|
|
56
|
-
* retained session to current main. 0 disables the watchdog. Default: 90.
|
|
57
|
-
*/
|
|
58
|
-
export const DEFAULT_IDLE_TIMEOUT_SEC = 90;
|
|
59
|
-
/** Upper bound accepted for idleTimeoutSec (defensive clamp). 0 disables. */
|
|
60
|
-
export const IDLE_TIMEOUT_SEC_LIMIT = 600;
|
|
61
|
-
|
|
62
|
-
export interface SubagentsConfig {
|
|
63
|
-
/** Agent names that are discoverable and injected. Fresh-install default:
|
|
64
|
-
enabledAgents: string[];
|
|
65
|
-
/** Per-agent model override, keyed by agent name, as "provider/model-id". */
|
|
66
|
-
agentModels: Record<string, string>;
|
|
67
|
-
/** Optional per-agent thinking preference. Runtime clamps it to the effective model's supported levels. */
|
|
68
|
-
agentThinkingLevels: Record<string, ThinkingLevel>;
|
|
69
|
-
/**
|
|
70
|
-
* When a review passes (REVIEW_PASS verdict), deliver it without waking the
|
|
71
|
-
* main agent. Disabled by default so passing reviews still resume orchestration.
|
|
72
|
-
*/
|
|
73
|
-
notifyOnReviewPass: boolean;
|
|
74
|
-
/**
|
|
75
|
-
* Max lines of a sub-agent result carried in the completion message. Longer
|
|
76
|
-
* results are truncated; the full text is written to a temp file whose path
|
|
77
|
-
* is included in the message. Default: 80.
|
|
78
|
-
*/
|
|
79
|
-
maxResultLines: number;
|
|
80
|
-
/** Whether to inject the delegation directive into the parent system prompt. Default: true. */
|
|
81
|
-
proactiveInjection: boolean;
|
|
82
|
-
/** Which agent directories to discover from. Default: "user". */
|
|
83
|
-
agentScope: AgentScope;
|
|
84
|
-
/** Max sub-agent processes running at once (extra work queues) and the max tasks
|
|
85
|
-
* one parallel `subagent` call may contain. Default: 4. */
|
|
86
|
-
maxConcurrency: number;
|
|
87
|
-
/**
|
|
88
|
-
* Auto-fix rounds when a reviewer returns REVIEW_FAIL: the extension dispatches
|
|
89
|
-
* a worker (briefed with the review's concrete findings) then a reviewer
|
|
90
|
-
* re-review, repeating up to this many times before waking the main agent with
|
|
91
|
-
* the full chain. 0 disables it (the main agent handles fixes itself).
|
|
92
|
-
* Default: 2.
|
|
93
|
-
*/
|
|
94
|
-
maxFixRounds: number;
|
|
95
|
-
/**
|
|
96
|
-
* Idle timeout in seconds: a sub-agent whose stdout (JSON event stream) goes
|
|
97
|
-
* silent for this long is terminated; a configured agent model then hands
|
|
98
|
-
* off to the current main model. 0 disables the idle watchdog. Default: 90.
|
|
99
|
-
*/
|
|
100
|
-
idleTimeoutSec: number;
|
|
101
|
-
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (typeof raw.
|
|
188
|
-
config.
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
*
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
await writeFile(temporaryPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
|
|
301
|
-
await rename(temporaryPath, configPath);
|
|
302
|
-
} finally {
|
|
303
|
-
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
export function errorMessage(error: unknown): string {
|
|
309
|
-
return error instanceof Error ? error.message : String(error);
|
|
310
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Configuration load/save for pi-subagents.
|
|
3
|
+
*
|
|
4
|
+
* Config lives at <agentDir>/pi-subagents.json (agentDir defaults to ~/.pi/agent
|
|
5
|
+
* and honors PI_CODING_AGENT_DIR). Parsing is defensive: invalid fields fall back
|
|
6
|
+
* to defaults instead of throwing, so a hand-edited or partially-written file can
|
|
7
|
+
* never break the extension at runtime.
|
|
8
|
+
*
|
|
9
|
+
* Schema upgrades happen transparently on load: a config written by an older
|
|
10
|
+
* version (missing newer keys or containing invalid
|
|
11
|
+
* values) is normalized and persisted back with the new fields filled in.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { getAgentDir, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
|
|
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;
|
|
21
|
+
|
|
22
|
+
/** Agents enabled out of the box on a fresh install. Explicit configured lists are preserved. */
|
|
23
|
+
export const DEFAULT_ENABLED_AGENTS: readonly string[] = ["explorer", "worker", "cleaner", "reviewer"];
|
|
24
|
+
|
|
25
|
+
export const AGENT_SCOPE_VALUES = ["user", "project", "both"] as const;
|
|
26
|
+
export type AgentScope = (typeof AGENT_SCOPE_VALUES)[number];
|
|
27
|
+
|
|
28
|
+
/** Thinking levels accepted by pi's `--thinking` option. */
|
|
29
|
+
export const THINKING_LEVEL_VALUES = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
30
|
+
export type ThinkingLevel = (typeof THINKING_LEVEL_VALUES)[number];
|
|
31
|
+
export const DEFAULT_THINKING_LEVEL: ThinkingLevel = "high";
|
|
32
|
+
|
|
33
|
+
/** How many lines of a sub-agent result the completion message may carry. Default: 80. */
|
|
34
|
+
export const DEFAULT_MAX_RESULT_LINES = 80;
|
|
35
|
+
/** Upper bound accepted for maxResultLines (defensive clamp). */
|
|
36
|
+
export const MAX_RESULT_LINES_LIMIT = 2000;
|
|
37
|
+
|
|
38
|
+
const CONFIG_FILE_NAME = "pi-subagents.json";
|
|
39
|
+
|
|
40
|
+
/** How many sub-agent processes may run at once, and how many tasks one parallel `subagent` call may contain. Default: 4. */
|
|
41
|
+
export const DEFAULT_MAX_CONCURRENCY = 4;
|
|
42
|
+
/** Upper bound accepted for maxConcurrency (defensive clamp). */
|
|
43
|
+
export const MAX_CONCURRENCY_LIMIT = 16;
|
|
44
|
+
/**
|
|
45
|
+
* How many automatic worker→reviewer fix rounds run when a reviewer returns
|
|
46
|
+
* REVIEW_FAIL before waking the main agent. 0 disables the auto-fix loop
|
|
47
|
+
* (the main agent is woken to dispatch fixes itself). Default: 2.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_MAX_FIX_ROUNDS = 2;
|
|
50
|
+
/** Upper bound accepted for maxFixRounds (defensive clamp). 0 disables the loop. */
|
|
51
|
+
export const MAX_FIX_ROUNDS_LIMIT = 5;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Default idle timeout in seconds: a sub-agent whose stdout (JSON event stream)
|
|
55
|
+
* goes silent for this long is terminated; a selected model then hands the
|
|
56
|
+
* retained session to current main. 0 disables the watchdog. Default: 90.
|
|
57
|
+
*/
|
|
58
|
+
export const DEFAULT_IDLE_TIMEOUT_SEC = 90;
|
|
59
|
+
/** Upper bound accepted for idleTimeoutSec (defensive clamp). 0 disables. */
|
|
60
|
+
export const IDLE_TIMEOUT_SEC_LIMIT = 600;
|
|
61
|
+
|
|
62
|
+
export interface SubagentsConfig {
|
|
63
|
+
/** Agent names that are discoverable and injected. Fresh-install default: explorer, worker, cleaner, reviewer. */
|
|
64
|
+
enabledAgents: string[];
|
|
65
|
+
/** Per-agent model override, keyed by agent name, as "provider/model-id". */
|
|
66
|
+
agentModels: Record<string, string>;
|
|
67
|
+
/** Optional per-agent thinking preference. Runtime clamps it to the effective model's supported levels. */
|
|
68
|
+
agentThinkingLevels: Record<string, ThinkingLevel>;
|
|
69
|
+
/**
|
|
70
|
+
* When a review passes (REVIEW_PASS verdict), deliver it without waking the
|
|
71
|
+
* main agent. Disabled by default so passing reviews still resume orchestration.
|
|
72
|
+
*/
|
|
73
|
+
notifyOnReviewPass: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Max lines of a sub-agent result carried in the completion message. Longer
|
|
76
|
+
* results are truncated; the full text is written to a temp file whose path
|
|
77
|
+
* is included in the message. Default: 80.
|
|
78
|
+
*/
|
|
79
|
+
maxResultLines: number;
|
|
80
|
+
/** Whether to inject the delegation directive into the parent system prompt. Default: true. */
|
|
81
|
+
proactiveInjection: boolean;
|
|
82
|
+
/** Which agent directories to discover from. Default: "user". */
|
|
83
|
+
agentScope: AgentScope;
|
|
84
|
+
/** Max sub-agent processes running at once (extra work queues) and the max tasks
|
|
85
|
+
* one parallel `subagent` call may contain. Default: 4. */
|
|
86
|
+
maxConcurrency: number;
|
|
87
|
+
/**
|
|
88
|
+
* Auto-fix rounds when a reviewer returns REVIEW_FAIL: the extension dispatches
|
|
89
|
+
* a worker (briefed with the review's concrete findings) then a reviewer
|
|
90
|
+
* re-review, repeating up to this many times before waking the main agent with
|
|
91
|
+
* the full chain. 0 disables it (the main agent handles fixes itself).
|
|
92
|
+
* Default: 2.
|
|
93
|
+
*/
|
|
94
|
+
maxFixRounds: number;
|
|
95
|
+
/**
|
|
96
|
+
* Idle timeout in seconds: a sub-agent whose stdout (JSON event stream) goes
|
|
97
|
+
* silent for this long is terminated; a configured agent model then hands
|
|
98
|
+
* off to the current main model. 0 disables the idle watchdog. Default: 90.
|
|
99
|
+
*/
|
|
100
|
+
idleTimeoutSec: number;
|
|
101
|
+
/**
|
|
102
|
+
* One-time feature announcements already shown to the user. Persisted so
|
|
103
|
+
* the notice never nags again.
|
|
104
|
+
*/
|
|
105
|
+
announcedFeatures: string[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const DEFAULT_CONFIG: SubagentsConfig = {
|
|
109
|
+
enabledAgents: [...DEFAULT_ENABLED_AGENTS],
|
|
110
|
+
agentModels: {},
|
|
111
|
+
agentThinkingLevels: {},
|
|
112
|
+
notifyOnReviewPass: false,
|
|
113
|
+
maxResultLines: DEFAULT_MAX_RESULT_LINES,
|
|
114
|
+
proactiveInjection: true,
|
|
115
|
+
agentScope: "user",
|
|
116
|
+
maxConcurrency: DEFAULT_MAX_CONCURRENCY,
|
|
117
|
+
maxFixRounds: DEFAULT_MAX_FIX_ROUNDS,
|
|
118
|
+
idleTimeoutSec: DEFAULT_IDLE_TIMEOUT_SEC,
|
|
119
|
+
announcedFeatures: [],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export function getConfigPath(agentDir: string = getAgentDir()): string {
|
|
123
|
+
return join(agentDir, CONFIG_FILE_NAME);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
127
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isAgentScope(value: unknown): value is AgentScope {
|
|
131
|
+
return typeof value === "string" && (AGENT_SCOPE_VALUES as readonly string[]).includes(value);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isModelReference(value: unknown): value is string {
|
|
135
|
+
if (typeof value !== "string") return false;
|
|
136
|
+
const normalized = value.trim();
|
|
137
|
+
const slash = normalized.indexOf("/");
|
|
138
|
+
return slash > 0 && slash < normalized.length - 1 && !/\s/u.test(normalized);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Clamp a raw value to a positive integer within [1, upper]; undefined when invalid. */
|
|
142
|
+
function clampCount(value: unknown, upper: number): number | undefined {
|
|
143
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
144
|
+
return Math.max(1, Math.min(upper, Math.round(value)));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Merge a raw parsed JSON value over the defaults, dropping invalid fields.
|
|
149
|
+
* Exported for tests.
|
|
150
|
+
*/
|
|
151
|
+
export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
152
|
+
const config = defaultConfig();
|
|
153
|
+
if (!isRecord(raw)) return config;
|
|
154
|
+
|
|
155
|
+
if (Array.isArray(raw.enabledAgents)) {
|
|
156
|
+
const names = raw.enabledAgents.filter(
|
|
157
|
+
(name): name is string => typeof name === "string" && name.trim().length > 0,
|
|
158
|
+
);
|
|
159
|
+
// An explicitly empty array is honored (disables all agents); otherwise keep valid names.
|
|
160
|
+
config.enabledAgents = [...new Set(names.map((name) => name.trim()))];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (isRecord(raw.agentModels)) {
|
|
164
|
+
for (const [key, value] of Object.entries(raw.agentModels)) {
|
|
165
|
+
if (isModelReference(value)) config.agentModels[key.trim()] = value.trim();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (isRecord(raw.agentThinkingLevels)) {
|
|
170
|
+
for (const [key, value] of Object.entries(raw.agentThinkingLevels)) {
|
|
171
|
+
if (
|
|
172
|
+
typeof value === "string" &&
|
|
173
|
+
(THINKING_LEVEL_VALUES as readonly string[]).includes(value)
|
|
174
|
+
) {
|
|
175
|
+
config.agentThinkingLevels[key.trim()] = value as ThinkingLevel;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (typeof raw.notifyOnReviewPass === "boolean") {
|
|
181
|
+
config.notifyOnReviewPass = raw.notifyOnReviewPass;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const maxResultLines = clampCount(raw.maxResultLines, MAX_RESULT_LINES_LIMIT);
|
|
185
|
+
if (maxResultLines !== undefined) config.maxResultLines = maxResultLines;
|
|
186
|
+
|
|
187
|
+
if (typeof raw.proactiveInjection === "boolean") {
|
|
188
|
+
config.proactiveInjection = raw.proactiveInjection;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (isAgentScope(raw.agentScope)) {
|
|
192
|
+
config.agentScope = raw.agentScope;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const maxConcurrency = clampCount(raw.maxConcurrency, MAX_CONCURRENCY_LIMIT);
|
|
196
|
+
if (maxConcurrency !== undefined) config.maxConcurrency = maxConcurrency;
|
|
197
|
+
|
|
198
|
+
// 0 disables the auto-fix loop (main agent handles fixes itself).
|
|
199
|
+
if (typeof raw.maxFixRounds === "number" && Number.isFinite(raw.maxFixRounds)) {
|
|
200
|
+
config.maxFixRounds = Math.max(0, Math.min(MAX_FIX_ROUNDS_LIMIT, Math.round(raw.maxFixRounds)));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 0 disables the idle watchdog; otherwise clamp to [0, upper].
|
|
204
|
+
if (typeof raw.idleTimeoutSec === "number" && Number.isFinite(raw.idleTimeoutSec)) {
|
|
205
|
+
config.idleTimeoutSec = Math.max(0, Math.min(IDLE_TIMEOUT_SEC_LIMIT, Math.round(raw.idleTimeoutSec)));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (Array.isArray(raw.announcedFeatures)) {
|
|
209
|
+
config.announcedFeatures = raw.announcedFeatures.filter(
|
|
210
|
+
(feature): feature is string => typeof feature === "string" && feature.trim().length > 0,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return config;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function defaultConfig(): SubagentsConfig {
|
|
218
|
+
return {
|
|
219
|
+
...DEFAULT_CONFIG,
|
|
220
|
+
enabledAgents: [...DEFAULT_CONFIG.enabledAgents],
|
|
221
|
+
agentModels: {},
|
|
222
|
+
agentThinkingLevels: {},
|
|
223
|
+
announcedFeatures: [],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Load config. A missing file is a normal state and yields the defaults (not an error).
|
|
229
|
+
* A corrupt file also falls back to defaults rather than throwing, so startup never breaks.
|
|
230
|
+
* A file from an older version (missing newer keys or holding extra keys) is
|
|
231
|
+
* normalized and persisted back, so the on-disk config stays current.
|
|
232
|
+
*/
|
|
233
|
+
export async function loadConfig(configPath: string = getConfigPath()): Promise<SubagentsConfig> {
|
|
234
|
+
let text: string;
|
|
235
|
+
try {
|
|
236
|
+
text = await readFile(configPath, "utf8");
|
|
237
|
+
} catch {
|
|
238
|
+
// Missing or unreadable: fall back to defaults but do not crash startup.
|
|
239
|
+
return defaultConfig();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let parsed: unknown;
|
|
243
|
+
try {
|
|
244
|
+
parsed = JSON.parse(text);
|
|
245
|
+
} catch {
|
|
246
|
+
return defaultConfig();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const config = normalizeConfig(parsed);
|
|
250
|
+
|
|
251
|
+
// Schema upgrade: persist the normalized shape when the file gained fields
|
|
252
|
+
// (new version) or dropped invalid ones.
|
|
253
|
+
if (JSON.stringify(config) !== JSON.stringify(parsed)) {
|
|
254
|
+
try {
|
|
255
|
+
await saveConfig(config, configPath);
|
|
256
|
+
} catch {
|
|
257
|
+
// Non-fatal: keep the in-memory config for this run.
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return config;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Synchronous load for the extension's init-time decisions (e.g. the recursion
|
|
266
|
+
* guard). Runs before any async context is available; never migrates or saves.
|
|
267
|
+
*/
|
|
268
|
+
export function loadConfigSync(configPath: string = getConfigPath()): SubagentsConfig {
|
|
269
|
+
try {
|
|
270
|
+
return normalizeConfig(JSON.parse(readFileSync(configPath, "utf8")));
|
|
271
|
+
} catch {
|
|
272
|
+
return defaultConfig();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Save config atomically (temp file + rename) serialized through pi's per-file
|
|
278
|
+
* mutation queue so concurrent writers cannot interleave.
|
|
279
|
+
*/
|
|
280
|
+
export async function saveConfig(
|
|
281
|
+
config: SubagentsConfig,
|
|
282
|
+
configPath: string = getConfigPath(),
|
|
283
|
+
): Promise<void> {
|
|
284
|
+
const normalized = normalizeConfig(config);
|
|
285
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
286
|
+
await withFileMutationQueue(configPath, async () => {
|
|
287
|
+
const temporaryPath = `${configPath}.${process.pid}.${Date.now()}.tmp`;
|
|
288
|
+
try {
|
|
289
|
+
await writeFile(temporaryPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
|
|
290
|
+
await rename(temporaryPath, configPath);
|
|
291
|
+
} finally {
|
|
292
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function errorMessage(error: unknown): string {
|
|
298
|
+
return error instanceof Error ? error.message : String(error);
|
|
299
|
+
}
|