@magnusekdahl/parallix 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/integration-pipelines.json +9 -0
- package/config/workflow.config.schema.json +12 -0
- package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
- package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
- package/docs/adr/0041-integration-pipeline-gates.md +1 -1
- package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
- package/docs/adr/index.md +1 -0
- package/docs/use-cases.md +44 -0
- package/lib/agents/agents.js +149 -100
- package/lib/agents/claude-telemetry.js +14 -10
- package/lib/agents/claude.js +32 -11
- package/lib/agents/codex-telemetry.js +32 -19
- package/lib/agents/codex.js +36 -11
- package/lib/agents/limit-hit.js +42 -25
- package/lib/agents/mistral-telemetry.js +1 -1
- package/lib/agents/mistral.js +13 -3
- package/lib/agents/opencode-export.js +11 -5
- package/lib/agents/opencode-telemetry.js +75 -90
- package/lib/agents/opencode.js +86 -29
- package/lib/agents/stage-telemetry.js +3 -7
- package/lib/commands/active.js +139 -94
- package/lib/commands/checkpoint.js +3 -1
- package/lib/commands/config.js +5 -3
- package/lib/commands/coverage-gate.js +17 -10
- package/lib/commands/diff.js +11 -5
- package/lib/commands/draft.js +95 -55
- package/lib/commands/handoff.js +253 -65
- package/lib/commands/integrate.js +288 -188
- package/lib/commands/mission-start.js +39 -34
- package/lib/commands/rebase.js +32 -21
- package/lib/commands/repair-handoff.js +21 -18
- package/lib/commands/resolve-conflict.js +5 -2
- package/lib/commands/review.js +1 -0
- package/lib/commands/setup-review.js +1 -0
- package/lib/commands/stats-backfill.js +50 -41
- package/lib/commands/stats.js +653 -204
- package/lib/commands/status.js +33 -28
- package/lib/core/fmt.js +58 -15
- package/lib/core/git.js +7 -5
- package/lib/core/gitignore.js +6 -4
- package/lib/core/mission-utils.js +161 -88
- package/lib/core/nels.js +199 -0
- package/lib/core/persistent-data-migration.js +55 -23
- package/lib/core/product-config.js +44 -23
- package/lib/core/runtime-matrix.js +15 -2
- package/lib/core/spawn-tee.js +40 -13
- package/lib/core/state-map.js +32 -13
- package/lib/core/storage.js +19 -5
- package/lib/core/subagent-limit.js +28 -0
- package/lib/core/verification.js +29 -12
- package/lib/review/rebase.js +11 -3
- package/lib/review/review-adapter.js +93 -6
- package/lib/review/review-artifacts.js +89 -23
- package/lib/review/review-commands.js +139 -45
- package/lib/review/review-events.js +103 -52
- package/lib/review/review-loop.js +117 -87
- package/lib/review/review-polling.js +25 -5
- package/lib/review/review-prompts.js +36 -5
- package/lib/review/review-state.js +39 -15
- package/lib/tools/backlog.js +162 -71
- package/lib/tools/forgejo.js +290 -129
- package/lib/tools/gatekeeper.js +15 -0
- package/lib/tools/redgreen.js +220 -0
- package/lib/tools/sessions.js +12 -5
- package/lib/tools/setup-review.js +170 -70
- package/package.json +11 -3
- package/prompts/draft.md +6 -0
- package/templates/mission-scaffold.md +1 -1
package/lib/agents/agents.js
CHANGED
|
@@ -24,8 +24,10 @@ const RESUME_CAPABLE = new Set(['claude', 'codex', 'custom']);
|
|
|
24
24
|
const CONFIG_PATH = path.join(__dirname, '..', '..', 'config', 'agents.json');
|
|
25
25
|
|
|
26
26
|
// Test hook: when set, used instead of spawning `command -v` to check PATH.
|
|
27
|
+
/** @type {((name: string) => string | null) | null} */
|
|
27
28
|
let _commandPathProbe = null;
|
|
28
29
|
|
|
30
|
+
/** @type {{[key: string]: Function}} */
|
|
29
31
|
const LAUNCHERS = {
|
|
30
32
|
codex: startCodexDraftAgent,
|
|
31
33
|
claude: startClaudeAgent,
|
|
@@ -33,6 +35,7 @@ const LAUNCHERS = {
|
|
|
33
35
|
custom: startOpencodeAgent
|
|
34
36
|
};
|
|
35
37
|
|
|
38
|
+
/** @type {{[key: string]: () => string}} */
|
|
36
39
|
const RESOLVERS = {
|
|
37
40
|
codex: resolveCodexCommand,
|
|
38
41
|
claude: resolveClaudeCommand,
|
|
@@ -40,6 +43,7 @@ const RESOLVERS = {
|
|
|
40
43
|
custom: resolveOpencodeCommand
|
|
41
44
|
};
|
|
42
45
|
|
|
46
|
+
/** @type {{[key: string]: string[]}} */
|
|
43
47
|
const HEALTH_PROBE_ARGS = Object.freeze({
|
|
44
48
|
codex: ['--help'],
|
|
45
49
|
claude: ['--help'],
|
|
@@ -58,6 +62,7 @@ const KNOWN_AGENT_NAMES = Object.freeze([
|
|
|
58
62
|
'human'
|
|
59
63
|
]);
|
|
60
64
|
|
|
65
|
+
/** @param {string} agent */
|
|
61
66
|
function workflowLauncherStatus(agent) {
|
|
62
67
|
const resolver = RESOLVERS[agent];
|
|
63
68
|
if (!resolver) {
|
|
@@ -77,8 +82,10 @@ function workflowLauncherStatus(agent) {
|
|
|
77
82
|
});
|
|
78
83
|
|
|
79
84
|
if (probe.error || probe.status !== 0) {
|
|
85
|
+
/** @type {Error & {code?: string}} */
|
|
86
|
+
const pErr = probe.error || new Error('');
|
|
80
87
|
const reason = probe.error
|
|
81
|
-
?
|
|
88
|
+
? (pErr.code || pErr.message)
|
|
82
89
|
: `exit ${probe.status}`;
|
|
83
90
|
return {
|
|
84
91
|
agent,
|
|
@@ -92,6 +99,7 @@ function workflowLauncherStatus(agent) {
|
|
|
92
99
|
return { agent, supported: true, detail: `${command} ${probeArgs.join(' ')}`.trim(), health: 'ok' };
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
/** @param {string} name */
|
|
95
103
|
function commandInPath(name) {
|
|
96
104
|
if (_commandPathProbe) {
|
|
97
105
|
return _commandPathProbe(name) || false;
|
|
@@ -103,9 +111,11 @@ function commandInPath(name) {
|
|
|
103
111
|
return result.status === 0 && result.stdout.trim().length > 0;
|
|
104
112
|
}
|
|
105
113
|
|
|
114
|
+
/** @param {string} configPath @param {string} scope @param {{message?: string} | null} originalError */
|
|
106
115
|
function buildInvalidAgentConfigError(configPath, scope, originalError) {
|
|
107
116
|
const location = path.resolve(configPath);
|
|
108
117
|
const detail = originalError && originalError.message ? originalError.message : 'invalid JSON';
|
|
118
|
+
/** @type {any} */
|
|
109
119
|
const error = new Error(
|
|
110
120
|
`Invalid ${scope} agent config at ${location}: ${detail}. ` +
|
|
111
121
|
'Fix or remove the malformed file before running workflow commands so agent blocking is applied deterministically.'
|
|
@@ -116,6 +126,7 @@ function buildInvalidAgentConfigError(configPath, scope, originalError) {
|
|
|
116
126
|
return error;
|
|
117
127
|
}
|
|
118
128
|
|
|
129
|
+
/** @param {{code?: string}} error */
|
|
119
130
|
function isInvalidAgentConfigError(error) {
|
|
120
131
|
return Boolean(error && error.code === 'WORKFLOW_AGENT_CONFIG_INVALID');
|
|
121
132
|
}
|
|
@@ -123,30 +134,33 @@ function isInvalidAgentConfigError(error) {
|
|
|
123
134
|
function readAgentConfigOrExit(configPath = CONFIG_PATH, options = {}) {
|
|
124
135
|
try {
|
|
125
136
|
return readAgentConfig(configPath, options);
|
|
126
|
-
} catch (error) {
|
|
127
|
-
if (isInvalidAgentConfigError(error)) {
|
|
128
|
-
fmt.log.fail(error.message);
|
|
137
|
+
} catch (/** @type {unknown} */ error) {
|
|
138
|
+
if (isInvalidAgentConfigError(/** @type {any} */ (error))) {
|
|
139
|
+
fmt.log.fail(/** @type {any} */ (error).message);
|
|
129
140
|
process.exit(1);
|
|
130
141
|
}
|
|
131
142
|
throw error;
|
|
132
143
|
}
|
|
133
144
|
}
|
|
134
145
|
|
|
146
|
+
/** @param {string} configPath @param {string} scope */
|
|
135
147
|
function parseAgentConfigFile(configPath, scope) {
|
|
136
148
|
try {
|
|
137
149
|
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
138
|
-
} catch (err) {
|
|
139
|
-
throw buildInvalidAgentConfigError(configPath, scope, err);
|
|
150
|
+
} catch (/** @type {unknown} */ err) {
|
|
151
|
+
throw buildInvalidAgentConfigError(configPath, scope, /** @type {{message?: string}} */ (err));
|
|
140
152
|
}
|
|
141
153
|
}
|
|
142
154
|
|
|
155
|
+
/** @param {{mergeLocal?: boolean, mainWorktreePath?: string | null, warn?: Function, targetPath?: string}} options */
|
|
143
156
|
function readAgentConfig(configPath = CONFIG_PATH, options = {}) {
|
|
144
157
|
const {
|
|
145
158
|
mergeLocal = path.resolve(configPath) === path.resolve(CONFIG_PATH),
|
|
146
159
|
mainWorktreePath,
|
|
147
160
|
warn = fmt.log.warn
|
|
148
161
|
} = options;
|
|
149
|
-
|
|
162
|
+
/** @type {{blocklist?: {[key: string]: any}, steps?: {[key: string]: any}}} */
|
|
163
|
+
let config = {};
|
|
150
164
|
if (fs.existsSync(configPath)) {
|
|
151
165
|
config = parseAgentConfigFile(configPath, 'workflow');
|
|
152
166
|
}
|
|
@@ -157,12 +171,12 @@ function readAgentConfig(configPath = CONFIG_PATH, options = {}) {
|
|
|
157
171
|
const mainWorktree = mainWorktreePath !== undefined
|
|
158
172
|
? mainWorktreePath
|
|
159
173
|
: getMainWorktreePath({ cwd: projectRoot, warn });
|
|
160
|
-
const legacyPaths = [
|
|
174
|
+
const /** @type {string[]} */ legacyPaths = [
|
|
161
175
|
path.join(path.dirname(configPath), 'agents.local.json'),
|
|
162
176
|
path.join(projectRoot, 'agents.local.json'),
|
|
163
|
-
mainWorktree ? path.join(mainWorktree, 'agents.local.json') :
|
|
164
|
-
].filter(Boolean);
|
|
165
|
-
const targetPath = options.targetPath || storage.resolveAgentsLocalPath({ ensureDir: true });
|
|
177
|
+
mainWorktree ? path.join(mainWorktree, 'agents.local.json') : ''
|
|
178
|
+
].filter(/** @param {string} p */ (p) => Boolean(p));
|
|
179
|
+
const /** @type {string} */ targetPath = options.targetPath || storage.resolveAgentsLocalPath({ ensureDir: true });
|
|
166
180
|
if (!fs.existsSync(targetPath)) {
|
|
167
181
|
try {
|
|
168
182
|
migrateAgentBlocklists({
|
|
@@ -170,14 +184,14 @@ function readAgentConfig(configPath = CONFIG_PATH, options = {}) {
|
|
|
170
184
|
destinationPath: targetPath,
|
|
171
185
|
warn
|
|
172
186
|
});
|
|
173
|
-
} catch (error) {
|
|
174
|
-
throw buildInvalidAgentConfigError(targetPath, 'local', error);
|
|
187
|
+
} catch (/** @type {unknown} */ error) {
|
|
188
|
+
throw buildInvalidAgentConfigError(targetPath, 'local', /** @type {any} */ (error));
|
|
175
189
|
}
|
|
176
190
|
}
|
|
177
191
|
if (fs.existsSync(targetPath)) {
|
|
178
192
|
const localConfig = parseAgentConfigFile(targetPath, 'local');
|
|
179
193
|
if (localConfig && localConfig.blocklist) {
|
|
180
|
-
config.blocklist = Object.assign(config.blocklist || {}, localConfig.blocklist);
|
|
194
|
+
/** @type {{blocklist?: {[key: string]: any}}} */ (config).blocklist = Object.assign(/** @type {{blocklist?: {[key: string]: any}}} */ (config).blocklist || {}, localConfig.blocklist);
|
|
181
195
|
}
|
|
182
196
|
}
|
|
183
197
|
}
|
|
@@ -185,6 +199,7 @@ function readAgentConfig(configPath = CONFIG_PATH, options = {}) {
|
|
|
185
199
|
return config;
|
|
186
200
|
}
|
|
187
201
|
|
|
202
|
+
/** @param {{cwd?: string, warn?: Function}} options */
|
|
188
203
|
function getMainWorktreePath(options = {}) {
|
|
189
204
|
const { cwd = process.cwd(), warn = fmt.log.warn } = options;
|
|
190
205
|
try {
|
|
@@ -209,7 +224,7 @@ function getMainWorktreePath(options = {}) {
|
|
|
209
224
|
const lines = result.stdout.split('\n');
|
|
210
225
|
const mainWorktreePath = detectMainWorktreePath(lines, cwd, commonDir);
|
|
211
226
|
if (mainWorktreePath) {
|
|
212
|
-
if (commonDir) MainWorktreeDetector.byCommonDir.set(commonDir, mainWorktreePath);
|
|
227
|
+
if (commonDir) {MainWorktreeDetector.byCommonDir.set(commonDir, mainWorktreePath);}
|
|
213
228
|
return mainWorktreePath;
|
|
214
229
|
}
|
|
215
230
|
|
|
@@ -220,7 +235,7 @@ function getMainWorktreePath(options = {}) {
|
|
|
220
235
|
const wt = lines[i].slice('worktree '.length).trim();
|
|
221
236
|
const branchLineIdx = i + 1;
|
|
222
237
|
if (branchLineIdx < lines.length && lines[branchLineIdx].startsWith('branch refs/heads/main')) {
|
|
223
|
-
if (commonDir) MainWorktreeDetector.byCommonDir.set(commonDir, wt);
|
|
238
|
+
if (commonDir) {MainWorktreeDetector.byCommonDir.set(commonDir, wt);}
|
|
224
239
|
return wt;
|
|
225
240
|
}
|
|
226
241
|
}
|
|
@@ -232,13 +247,15 @@ function getMainWorktreePath(options = {}) {
|
|
|
232
247
|
if (lines[i].startsWith('worktree ')) {
|
|
233
248
|
const wt = lines[i].slice('worktree '.length).trim();
|
|
234
249
|
if (wt !== cwd) {
|
|
235
|
-
if (commonDir) MainWorktreeDetector.byCommonDir.set(commonDir, wt);
|
|
250
|
+
if (commonDir) {MainWorktreeDetector.byCommonDir.set(commonDir, wt);}
|
|
236
251
|
return wt;
|
|
237
252
|
}
|
|
238
253
|
}
|
|
239
254
|
}
|
|
240
|
-
} catch (err) {
|
|
241
|
-
|
|
255
|
+
} catch (/** @type {unknown} */ err) {
|
|
256
|
+
/** @type {Error & {code?: string}} */
|
|
257
|
+
const e = /** @type {any} */ (err);
|
|
258
|
+
const detail = e && (e.code || e.message) ? (e.code || e.message) : 'unknown error';
|
|
242
259
|
warn(
|
|
243
260
|
`Could not inspect git worktrees while looking for main-worktree agents.local.json; ` +
|
|
244
261
|
`skipping that lookup (${detail}).`
|
|
@@ -261,6 +278,7 @@ const MainWorktreeDetector = {
|
|
|
261
278
|
byCommonDir: new Map()
|
|
262
279
|
};
|
|
263
280
|
|
|
281
|
+
/** @param {string} cwd @param {string[]} args */
|
|
264
282
|
function getGitPath(cwd, args) {
|
|
265
283
|
const result = spawnSync('git', ['-C', cwd, ...args], {
|
|
266
284
|
encoding: 'utf8',
|
|
@@ -273,13 +291,15 @@ function getGitPath(cwd, args) {
|
|
|
273
291
|
return result.stdout.trim() || null;
|
|
274
292
|
}
|
|
275
293
|
|
|
294
|
+
/** @param {string[]} lines */
|
|
276
295
|
function parseWorktreePaths(lines) {
|
|
277
296
|
return lines
|
|
278
|
-
.filter(line => line.startsWith('worktree '))
|
|
279
|
-
.map(line => line.slice('worktree '.length).trim())
|
|
297
|
+
.filter(/** @param {string} line */ (line) => line.startsWith('worktree '))
|
|
298
|
+
.map(/** @param {string} line */ (line) => line.slice('worktree '.length).trim())
|
|
280
299
|
.filter(Boolean);
|
|
281
300
|
}
|
|
282
301
|
|
|
302
|
+
/** @param {string[]} lines @param {string} cwd @param {string | null} commonDir */
|
|
283
303
|
function detectMainWorktreePath(lines, cwd, commonDir) {
|
|
284
304
|
const worktrees = parseWorktreePaths(lines);
|
|
285
305
|
if (worktrees.length === 0) {
|
|
@@ -307,6 +327,7 @@ function detectMainWorktreePath(lines, cwd, commonDir) {
|
|
|
307
327
|
return null;
|
|
308
328
|
}
|
|
309
329
|
|
|
330
|
+
/** @param {string | number} value */
|
|
310
331
|
function parseBlockUntil(value) {
|
|
311
332
|
if (typeof value !== 'string') {
|
|
312
333
|
return NaN;
|
|
@@ -336,16 +357,20 @@ function parseBlockUntil(value) {
|
|
|
336
357
|
return parsed.getTime();
|
|
337
358
|
}
|
|
338
359
|
|
|
360
|
+
/**
|
|
361
|
+
* @param {string} agent
|
|
362
|
+
* @param {{blocklist?: {[key: string]: any}, steps?: {[key: string]: any}} | null} config
|
|
363
|
+
*/
|
|
339
364
|
function isAgentBlocked(agent, config) {
|
|
340
365
|
if (!config || !config.blocklist || config.blocklist[agent] === undefined) {
|
|
341
366
|
return false;
|
|
342
367
|
}
|
|
343
368
|
const entry = config.blocklist[agent];
|
|
344
|
-
if (entry === true) return true;
|
|
345
|
-
if (entry === false) return false;
|
|
369
|
+
if (entry === true) {return true;}
|
|
370
|
+
if (entry === false) {return false;}
|
|
346
371
|
if (entry && typeof entry === 'object') {
|
|
347
|
-
if (entry.blocked === true) return true;
|
|
348
|
-
if (entry.blocked === false) return false;
|
|
372
|
+
if (entry.blocked === true) {return true;}
|
|
373
|
+
if (entry.blocked === false) {return false;}
|
|
349
374
|
if (entry.until) {
|
|
350
375
|
const until = parseBlockUntil(entry.until);
|
|
351
376
|
if (!isNaN(until) && until > Date.now()) {
|
|
@@ -356,29 +381,38 @@ function isAgentBlocked(agent, config) {
|
|
|
356
381
|
return false;
|
|
357
382
|
}
|
|
358
383
|
|
|
384
|
+
/**
|
|
385
|
+
* @param {string} step
|
|
386
|
+
* @param {{config?: {blocklist?: {[key: string]: any}, steps?: {[key: string]: any}}, configPath?: string}} options
|
|
387
|
+
*/
|
|
359
388
|
function eligibleAgentsForStep(step, options = {}) {
|
|
360
|
-
const config = options.config !== undefined
|
|
389
|
+
const /** @type {{blocklist?: {[key: string]: any}, steps?: {[key: string]: any}} | null} */ config = options.config !== undefined
|
|
361
390
|
? options.config
|
|
362
|
-
: readAgentConfig(options.configPath || CONFIG_PATH, options);
|
|
391
|
+
: readAgentConfig(options.configPath || CONFIG_PATH, /** @type {{mergeLocal?: boolean, mainWorktreePath?: string | null, warn?: Function, targetPath?: string}} */ (options));
|
|
363
392
|
let eligible;
|
|
364
393
|
if (!config || !config.steps || !config.steps[step]) {
|
|
365
394
|
eligible = Object.keys(LAUNCHERS);
|
|
366
395
|
} else {
|
|
367
396
|
eligible = config.steps[step].eligible || Object.keys(LAUNCHERS);
|
|
368
397
|
}
|
|
369
|
-
return eligible.filter(agent => !isAgentBlocked(agent, config));
|
|
398
|
+
return eligible.filter(/** @param {string} agent */ (agent) => !isAgentBlocked(agent, config));
|
|
370
399
|
}
|
|
371
400
|
|
|
401
|
+
/** @param {string[]} agents @param {{[key: string]: number}} weights */
|
|
372
402
|
function weightedRandom(agents, weights) {
|
|
373
|
-
const total = agents.reduce((sum, a) => sum + (weights[a] || 1), 0);
|
|
403
|
+
const total = agents.reduce(/** @param {number} sum @param {string} a */ (sum, a) => sum + (weights[a] || 1), 0);
|
|
374
404
|
let r = Math.random() * total;
|
|
375
405
|
for (const agent of agents) {
|
|
376
406
|
r -= weights[agent] || 1;
|
|
377
|
-
if (r <= 0) return agent;
|
|
407
|
+
if (r <= 0) {return agent;}
|
|
378
408
|
}
|
|
379
409
|
return agents[agents.length - 1];
|
|
380
410
|
}
|
|
381
411
|
|
|
412
|
+
/**
|
|
413
|
+
* @param {string} step
|
|
414
|
+
* @param {{exclude?: Set<string>, config?: {blocklist?: {[key: string]: any}, steps?: {[key: string]: any}}, configPath?: string}} options
|
|
415
|
+
*/
|
|
382
416
|
function selectAgent(step, options = {}) {
|
|
383
417
|
const envOverride = process.env.WORKFLOW_AGENT;
|
|
384
418
|
const excluded = options.exclude instanceof Set ? options.exclude : new Set();
|
|
@@ -393,7 +427,7 @@ function selectAgent(step, options = {}) {
|
|
|
393
427
|
return envOverride;
|
|
394
428
|
}
|
|
395
429
|
|
|
396
|
-
const pool = eligible.filter(agent => !excluded.has(agent));
|
|
430
|
+
const pool = eligible.filter(/** @param {string} agent */ (agent) => !excluded.has(agent));
|
|
397
431
|
if (eligible.length === 0) {
|
|
398
432
|
throw new Error(`No agents are eligible for workflow step: ${step}`);
|
|
399
433
|
}
|
|
@@ -407,15 +441,15 @@ function selectAgent(step, options = {}) {
|
|
|
407
441
|
// Filter to agents that are both eligible (per config) and supported (launcher present).
|
|
408
442
|
const statuses = new Map(
|
|
409
443
|
pool
|
|
410
|
-
.filter(agent => LAUNCHERS[agent])
|
|
411
|
-
.map(agent => [agent, workflowLauncherStatus(agent)])
|
|
444
|
+
.filter(/** @param {string} agent */ (agent) => LAUNCHERS[agent])
|
|
445
|
+
.map(/** @param {string} agent */ (agent) => [agent, workflowLauncherStatus(agent)])
|
|
412
446
|
);
|
|
413
|
-
const available = pool.filter(agent => {
|
|
447
|
+
const available = pool.filter(/** @param {string} agent */ (agent) => {
|
|
414
448
|
const status = statuses.get(agent);
|
|
415
449
|
return Boolean(status && status.supported);
|
|
416
450
|
});
|
|
417
451
|
if (available.length === 0) {
|
|
418
|
-
const blockers = pool.map(agent => {
|
|
452
|
+
const blockers = pool.map(/** @param {string} agent */ (agent) => {
|
|
419
453
|
const status = statuses.get(agent) || { detail: agent, reason: 'unsupported-agent' };
|
|
420
454
|
const suffix = status.reason ? `; ${status.reason}` : '';
|
|
421
455
|
return `${agent} (looked for: ${status.detail}${suffix})`;
|
|
@@ -427,9 +461,9 @@ function selectAgent(step, options = {}) {
|
|
|
427
461
|
);
|
|
428
462
|
}
|
|
429
463
|
|
|
430
|
-
const config = options.config !== undefined
|
|
464
|
+
const /** @type {{blocklist?: {[key: string]: any}, steps?: {[key: string]: any}} | null} */ config = options.config !== undefined
|
|
431
465
|
? options.config
|
|
432
|
-
: readAgentConfig(options.configPath || CONFIG_PATH, options);
|
|
466
|
+
: readAgentConfig(options.configPath || CONFIG_PATH, /** @type {{mergeLocal?: boolean, mainWorktreePath?: string | null, warn?: Function, targetPath?: string}} */ (options));
|
|
433
467
|
const stepConfig = config && config.steps && config.steps[step] ? config.steps[step] : {};
|
|
434
468
|
const selection = stepConfig.selection || 'random';
|
|
435
469
|
|
|
@@ -445,8 +479,10 @@ function selectAgent(step, options = {}) {
|
|
|
445
479
|
return available[0];
|
|
446
480
|
}
|
|
447
481
|
|
|
482
|
+
/** @param {string} agent */
|
|
448
483
|
function assertAgentSupported(agent) {
|
|
449
484
|
if (!LAUNCHERS[agent]) {
|
|
485
|
+
/** @type {any} */
|
|
450
486
|
const error = new Error(
|
|
451
487
|
`Unknown agent: "${fmt.agent(agent)}". Supported agents: ${Object.keys(LAUNCHERS).join(', ')}.`
|
|
452
488
|
);
|
|
@@ -458,6 +494,7 @@ function assertAgentSupported(agent) {
|
|
|
458
494
|
if (!status.supported) {
|
|
459
495
|
const health = status.health ? ` (${status.health})` : '';
|
|
460
496
|
const reason = status.reason ? `; reason: ${status.reason}` : '';
|
|
497
|
+
/** @type {any} */
|
|
461
498
|
const error = new Error(
|
|
462
499
|
`Agent "${fmt.agent(agent)}" launcher is not available on this workstation${health}. ` +
|
|
463
500
|
`Looked for: ${fmt.path(status.detail)}${reason}. ` +
|
|
@@ -468,11 +505,13 @@ function assertAgentSupported(agent) {
|
|
|
468
505
|
}
|
|
469
506
|
}
|
|
470
507
|
|
|
508
|
+
/** @param {{targetPath?: string}} options */
|
|
471
509
|
function resolveBlocklistTargetPath(options = {}) {
|
|
472
|
-
if (options.targetPath) return options.targetPath;
|
|
510
|
+
if (options.targetPath) {return options.targetPath;}
|
|
473
511
|
return storage.resolveAgentsLocalPath({ ensureDir: true });
|
|
474
512
|
}
|
|
475
513
|
|
|
514
|
+
/** @param {string} agent @param {string} until @param {{targetPath?: string}} options */
|
|
476
515
|
function updateAgentBlock(agent, until, options = {}) {
|
|
477
516
|
if (!agent || typeof agent !== 'string') {
|
|
478
517
|
throw new Error('updateAgentBlock requires an agent name');
|
|
@@ -483,6 +522,7 @@ function updateAgentBlock(agent, until, options = {}) {
|
|
|
483
522
|
|
|
484
523
|
const targetPath = resolveBlocklistTargetPath(options);
|
|
485
524
|
|
|
525
|
+
/** @type {{blocklist?: {[key: string]: any}}} */
|
|
486
526
|
let payload = {};
|
|
487
527
|
if (fs.existsSync(targetPath)) {
|
|
488
528
|
// Match the read-path contract (parseAgentConfigFile): malformed local agent
|
|
@@ -490,8 +530,8 @@ function updateAgentBlock(agent, until, options = {}) {
|
|
|
490
530
|
// corrupted agents.local.json would destroy whatever was on disk.
|
|
491
531
|
try {
|
|
492
532
|
payload = JSON.parse(fs.readFileSync(targetPath, 'utf8')) || {};
|
|
493
|
-
} catch (err) {
|
|
494
|
-
throw buildInvalidAgentConfigError(targetPath, 'local', err);
|
|
533
|
+
} catch (/** @type {unknown} */ err) {
|
|
534
|
+
throw buildInvalidAgentConfigError(targetPath, 'local', /** @type {{message?: string}} */ (err));
|
|
495
535
|
}
|
|
496
536
|
if (typeof payload !== 'object' || Array.isArray(payload)) {
|
|
497
537
|
throw buildInvalidAgentConfigError(
|
|
@@ -510,11 +550,12 @@ function updateAgentBlock(agent, until, options = {}) {
|
|
|
510
550
|
return { path: targetPath, blocklist: payload.blocklist };
|
|
511
551
|
}
|
|
512
552
|
|
|
553
|
+
/** @param {string} agent */
|
|
513
554
|
function defaultIsAgentBlockedNow(agent) {
|
|
514
555
|
try {
|
|
515
556
|
const config = readAgentConfig(CONFIG_PATH, {});
|
|
516
557
|
return isAgentBlocked(agent, config);
|
|
517
|
-
} catch (err) {
|
|
558
|
+
} catch (/** @type {unknown} */ err) {
|
|
518
559
|
// If the config is malformed, surface that through the launcher path
|
|
519
560
|
// (assertAgentSupported / launch) instead of silently rerouting. Treat as
|
|
520
561
|
// not-blocked here so the existing error path runs.
|
|
@@ -522,18 +563,20 @@ function defaultIsAgentBlockedNow(agent) {
|
|
|
522
563
|
}
|
|
523
564
|
}
|
|
524
565
|
|
|
566
|
+
/** @param {string} name */
|
|
525
567
|
function readPositiveMsEnv(name) {
|
|
526
568
|
const raw = process.env[name];
|
|
527
|
-
if (raw === undefined || raw === '') return null;
|
|
569
|
+
if (raw === undefined || raw === '') {return null;}
|
|
528
570
|
const value = Number(raw);
|
|
529
571
|
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
530
572
|
}
|
|
531
573
|
|
|
532
|
-
|
|
574
|
+
/** @param {{initialDelayMs?: number, intervalMs?: number}|boolean} config @param {string | null} step */
|
|
575
|
+
function resolveNoOutputWatchdogConfig(config, step = null) {
|
|
533
576
|
if (config === false || process.env.WORKFLOW_AGENT_NO_OUTPUT_WATCHDOG === '0') {
|
|
534
577
|
return null;
|
|
535
578
|
}
|
|
536
|
-
const explicit = config && typeof config === 'object' ? config : {};
|
|
579
|
+
const /** @type {{initialDelayMs?: number, intervalMs?: number}} */ explicit = config && typeof config === 'object' ? config : {};
|
|
537
580
|
// Draft gets a shorter default watchdog to surface agent-launch visibility
|
|
538
581
|
// quickly; the generic default (60s) is too slow for the draft entrypoint
|
|
539
582
|
// where an operator cannot tell launch from hang.
|
|
@@ -557,15 +600,20 @@ function resolveNoOutputWatchdogConfig(config = {}, step = null) {
|
|
|
557
600
|
return { initialDelayMs, intervalMs };
|
|
558
601
|
}
|
|
559
602
|
|
|
603
|
+
/** @param {number} elapsedMs */
|
|
560
604
|
function formatElapsed(elapsedMs) {
|
|
561
605
|
const seconds = Math.max(0, Math.round(elapsedMs / 1000));
|
|
562
|
-
if (seconds < 60) return `${seconds}s`;
|
|
606
|
+
if (seconds < 60) {return `${seconds}s`;}
|
|
563
607
|
const minutes = Math.floor(seconds / 60);
|
|
564
608
|
const remainder = seconds % 60;
|
|
565
609
|
return remainder === 0 ? `${minutes}m` : `${minutes}m ${remainder}s`;
|
|
566
610
|
}
|
|
567
611
|
|
|
568
|
-
|
|
612
|
+
/**
|
|
613
|
+
* @param {string} step
|
|
614
|
+
* @param {{prompt: string | Function, worktree?: string, agent?: string, env?: {[key: string]: string}, exclude?: (string|null|Set<string>)[], onLimitHit?: Function, onLaunch?: Function, slug?: string | null, role?: string | null, detectLimitHitFn?: Function, updateAgentBlockFn?: Function, selectAgentFn?: Function, resolveAgentModelFn?: Function, isAgentBlockedFn?: Function, sessionsModule?: object, log?: Function, noOutputWatchdog?: {initialDelayMs?: number, intervalMs?: number}}} opts
|
|
615
|
+
*/
|
|
616
|
+
async function startAgent(step, opts = { prompt: '' }) {
|
|
569
617
|
const {
|
|
570
618
|
prompt,
|
|
571
619
|
worktree,
|
|
@@ -602,17 +650,16 @@ async function startAgent(step, opts = {}) {
|
|
|
602
650
|
if (!chosen) {
|
|
603
651
|
try {
|
|
604
652
|
chosen = selectAgentFn(step, { exclude: tried });
|
|
605
|
-
} catch (err) {
|
|
653
|
+
} catch (/** @type {unknown} */ err) {
|
|
606
654
|
// Only catch pool exhaustion errors from selectAgent.
|
|
607
655
|
// Configuration errors (no eligible agents, no working launcher) must
|
|
608
656
|
// propagate unchanged to preserve diagnostics (SC 3).
|
|
609
657
|
// Exhaustion is indicated by:
|
|
610
658
|
// - "exhausted" from real selectAgent pool exhaustion ("are exhausted")
|
|
611
659
|
// - "No agents available" from test mocks simulating exhaustion
|
|
612
|
-
if (!err.message ||
|
|
613
|
-
err.message.includes('
|
|
614
|
-
|
|
615
|
-
)) {
|
|
660
|
+
if (!(/** @type {any} */ (err).message || '').includes('exhausted') &&
|
|
661
|
+
!(/** @type {any} */ (err).message || '').includes('No agents available')
|
|
662
|
+
) {
|
|
616
663
|
throw err;
|
|
617
664
|
}
|
|
618
665
|
// Pool exhausted; build clear exhaustion diagnostics with per-agent errors (SC 3)
|
|
@@ -639,33 +686,35 @@ async function startAgent(step, opts = {}) {
|
|
|
639
686
|
// the same limit. Reroute through normal selection on the next iteration.
|
|
640
687
|
log(fmt.status('WARN', `Pinned agent "${fmt.agent(chosen)}" is currently blocked in agents.local.json; rerouting via selectAgent for step "${step}".`));
|
|
641
688
|
tried.add(chosen);
|
|
642
|
-
chosen =
|
|
689
|
+
chosen = undefined;
|
|
643
690
|
continue;
|
|
644
691
|
}
|
|
645
692
|
|
|
646
693
|
try {
|
|
647
|
-
assertAgentSupported(chosen);
|
|
648
|
-
} catch (err) {
|
|
649
|
-
|
|
694
|
+
assertAgentSupported(chosen || '');
|
|
695
|
+
} catch (/** @type {unknown} */ err) {
|
|
696
|
+
/** @type {Error & {code?: string}} */
|
|
697
|
+
const e = /** @type {any} */ (err);
|
|
698
|
+
if (e.code !== 'LAUNCHER_UNAVAILABLE') {
|
|
650
699
|
throw err;
|
|
651
700
|
}
|
|
652
|
-
log(fmt.status('WARN', err.message));
|
|
701
|
+
log(fmt.status('WARN', /** @type {any} */ (err).message));
|
|
653
702
|
// Only reroute for launcher-availability failures (missing or probe-failed).
|
|
654
|
-
tried.add(chosen);
|
|
703
|
+
tried.add(chosen || '');
|
|
655
704
|
// If the caller pinned a specific agent, allow one retry that ignores
|
|
656
705
|
// the override and falls back to normal selection (matches limit-hit logic).
|
|
657
706
|
if (agentOverride && agentOverride === chosen && iteration === 1) {
|
|
658
|
-
chosen =
|
|
707
|
+
chosen = undefined;
|
|
659
708
|
continue;
|
|
660
709
|
}
|
|
661
|
-
chosen =
|
|
710
|
+
chosen = undefined;
|
|
662
711
|
continue;
|
|
663
712
|
}
|
|
664
|
-
tried.add(chosen);
|
|
665
|
-
launched.add(chosen);
|
|
713
|
+
tried.add(chosen || '');
|
|
714
|
+
launched.add(chosen || '');
|
|
666
715
|
|
|
667
|
-
const launcher = LAUNCHERS[chosen];
|
|
668
|
-
log(fmt.status('INFO', `Selected agent for step "${step}": ${fmt.agent(chosen)}${iteration > 1 ? ` (attempt ${iteration})` : ''}`));
|
|
716
|
+
const launcher = LAUNCHERS[chosen || ''];
|
|
717
|
+
log(fmt.status('INFO', `Selected agent for step "${step}": ${fmt.agent(chosen || '')}${iteration > 1 ? ` (attempt ${iteration})` : ''}`));
|
|
669
718
|
|
|
670
719
|
// Enforce the agent family as the Forgejo identity (ADR 0029 / task-095).
|
|
671
720
|
// FORGEJO_USER is set last so the harness-selected identity always wins;
|
|
@@ -678,15 +727,15 @@ async function startAgent(step, opts = {}) {
|
|
|
678
727
|
// invalidates the prior session).
|
|
679
728
|
const resume = Boolean(
|
|
680
729
|
worktree && slug && role &&
|
|
681
|
-
RESUME_CAPABLE.has(chosen) &&
|
|
682
|
-
sessionsModule.shouldResume(worktree, slug, role, chosen)
|
|
730
|
+
RESUME_CAPABLE.has(chosen || '') &&
|
|
731
|
+
(/** @type {any} */ (sessionsModule)).shouldResume(worktree, slug, role, chosen || '')
|
|
683
732
|
);
|
|
684
|
-
const sessionId = sessionsModule.getSessionId(worktree, slug, role);
|
|
733
|
+
const sessionId = (/** @type {any} */ (sessionsModule)).getSessionId(worktree, slug, role);
|
|
685
734
|
if (slug && role) {
|
|
686
735
|
if (resume) {
|
|
687
|
-
log(fmt.status('INFO', `Resuming ${fmt.agent(chosen)} session for ${fmt.slug(slug)} (${role}).${sessionId ? ` Session: ${sessionId}` : ''}`));
|
|
688
|
-
} else if (RESUME_CAPABLE.has(chosen)) {
|
|
689
|
-
log(fmt.status('INFO', `No prior ${fmt.agent(chosen)} session for ${fmt.slug(slug)} (${role}); launching fresh.`));
|
|
736
|
+
log(fmt.status('INFO', `Resuming ${fmt.agent(chosen || '')} session for ${fmt.slug(slug)} (${role}).${sessionId ? ` Session: ${sessionId}` : ''}`));
|
|
737
|
+
} else if (RESUME_CAPABLE.has(chosen || '')) {
|
|
738
|
+
log(fmt.status('INFO', `No prior ${fmt.agent(chosen || '')} session for ${fmt.slug(slug)} (${role}); launching fresh.`));
|
|
690
739
|
}
|
|
691
740
|
}
|
|
692
741
|
|
|
@@ -699,9 +748,9 @@ async function startAgent(step, opts = {}) {
|
|
|
699
748
|
// Resolve the per-family model override (adapters.agents.models[chosen]).
|
|
700
749
|
// null when the family is not configured, in which case the launcher omits
|
|
701
750
|
// the model flag entirely and the agent uses its own default.
|
|
702
|
-
const model = resolveAgentModelFn(chosen, worktree || process.cwd());
|
|
751
|
+
const model = resolveAgentModelFn(chosen || '', worktree || process.cwd());
|
|
703
752
|
if (model) {
|
|
704
|
-
log(fmt.status('INFO', `Using configured model for ${fmt.agent(chosen)}: ${model}`));
|
|
753
|
+
log(fmt.status('INFO', `Using configured model for ${fmt.agent(chosen || '')}: ${model}`));
|
|
705
754
|
}
|
|
706
755
|
|
|
707
756
|
const watchdogConfig = resolveNoOutputWatchdogConfig(noOutputWatchdog, step);
|
|
@@ -717,14 +766,14 @@ async function startAgent(step, opts = {}) {
|
|
|
717
766
|
teeOptions: watchdogConfig ? {
|
|
718
767
|
noOutputWatchdog: {
|
|
719
768
|
...watchdogConfig,
|
|
720
|
-
onNoOutput: ({
|
|
721
|
-
const stage = elapsedMs < (step === 'draft' ? DRAFT_NO_OUTPUT_INITIAL_DELAY_MS : DEFAULT_NO_OUTPUT_INITIAL_DELAY_MS)
|
|
769
|
+
onNoOutput: (/** @type {{pid: number, elapsedMs: number}} */ evt) => {
|
|
770
|
+
const stage = evt.elapsedMs < (step === 'draft' ? DRAFT_NO_OUTPUT_INITIAL_DELAY_MS : DEFAULT_NO_OUTPUT_INITIAL_DELAY_MS)
|
|
722
771
|
? 'starting up'
|
|
723
772
|
: 'running';
|
|
724
773
|
log(fmt.status(
|
|
725
774
|
'INFO',
|
|
726
|
-
`No output yet from ${fmt.agent(chosen)} for step "${step}" after ${formatElapsed(elapsedMs)} ` +
|
|
727
|
-
`(pid ${pid || 'unknown'}, agent ${stage}). ` +
|
|
775
|
+
`No output yet from ${fmt.agent(chosen || '')} for step "${step}" after ${formatElapsed(evt.elapsedMs)} ` +
|
|
776
|
+
`(pid ${evt.pid || 'unknown'}, agent ${stage}). ` +
|
|
728
777
|
`Launcher is still running; stdout/stderr have not produced visible output.`
|
|
729
778
|
));
|
|
730
779
|
}
|
|
@@ -760,12 +809,12 @@ async function startAgent(step, opts = {}) {
|
|
|
760
809
|
});
|
|
761
810
|
|
|
762
811
|
if (limitHit) {
|
|
763
|
-
log(fmt.status('WARN', `Limit hit detected for ${fmt.agent(chosen)}; reset estimate "${limitHit.until}" (${limitHit.source}). Blocking and retrying.`));
|
|
812
|
+
log(fmt.status('WARN', `Limit hit detected for ${fmt.agent(chosen || '')}; reset estimate "${limitHit.until}" (${limitHit.source}). Blocking and retrying.`));
|
|
764
813
|
try {
|
|
765
|
-
const blockResult = updateAgentBlockFn(chosen, limitHit.until);
|
|
766
|
-
log(fmt.status('INFO', `Wrote blocklist entry for ${fmt.agent(chosen)} -> ${fmt.path(blockResult.path)}`));
|
|
767
|
-
} catch (err) {
|
|
768
|
-
log(fmt.status('WARN', `Could not persist blocklist entry for ${fmt.agent(chosen)}: ${err.message}`));
|
|
814
|
+
const blockResult = updateAgentBlockFn(chosen || '', limitHit.until);
|
|
815
|
+
log(fmt.status('INFO', `Wrote blocklist entry for ${fmt.agent(chosen || '')} -> ${fmt.path(blockResult.path)}`));
|
|
816
|
+
} catch (/** @type {unknown} */ err) {
|
|
817
|
+
log(fmt.status('WARN', `Could not persist blocklist entry for ${fmt.agent(chosen || '')}: ${/** @type {any} */ (err).message}`));
|
|
769
818
|
}
|
|
770
819
|
if (typeof onLimitHit === 'function') {
|
|
771
820
|
onLimitHit({ agent: chosen, until: limitHit.until, source: limitHit.source });
|
|
@@ -774,22 +823,22 @@ async function startAgent(step, opts = {}) {
|
|
|
774
823
|
// If the caller pinned a specific agent, fail loudly — there is no fallback.
|
|
775
824
|
if (agentOverride && agentOverride === chosen && iteration === 1) {
|
|
776
825
|
// Allow one retry that ignores the override.
|
|
777
|
-
chosen =
|
|
826
|
+
chosen = undefined;
|
|
778
827
|
continue;
|
|
779
828
|
}
|
|
780
|
-
chosen =
|
|
829
|
+
chosen = undefined;
|
|
781
830
|
continue;
|
|
782
831
|
}
|
|
783
832
|
|
|
784
833
|
// Reroute if the launcher binary could not be started (ENOENT = not found, EACCES = not executable).
|
|
785
834
|
if (result && result.error && (result.error.code === 'ENOENT' || result.error.code === 'EACCES')) {
|
|
786
|
-
log(fmt.status('WARN', `Launcher for "${chosen}" could not be started (${result.error.code}); rerouting.`));
|
|
787
|
-
tried.add(chosen);
|
|
835
|
+
log(fmt.status('WARN', `Launcher for "${chosen || ''}" could not be started (${result.error.code}); rerouting.`));
|
|
836
|
+
tried.add(chosen || '');
|
|
788
837
|
if (agentOverride && agentOverride === chosen && iteration === 1) {
|
|
789
|
-
chosen =
|
|
838
|
+
chosen = undefined;
|
|
790
839
|
continue;
|
|
791
840
|
}
|
|
792
|
-
chosen =
|
|
841
|
+
chosen = undefined;
|
|
793
842
|
continue;
|
|
794
843
|
}
|
|
795
844
|
|
|
@@ -814,29 +863,29 @@ async function startAgent(step, opts = {}) {
|
|
|
814
863
|
const stderrSnippet = result && result.stderr
|
|
815
864
|
? ` (${result.stderr.trim().split('\n')[0]})`
|
|
816
865
|
: '';
|
|
817
|
-
log(fmt.status('WARN', `Agent ${fmt.agent(chosen)} failed to complete (${exitInfo}${stderrSnippet}); retrying with next eligible agent.`));
|
|
818
|
-
agentErrors.set(chosen, {
|
|
866
|
+
log(fmt.status('WARN', `Agent ${fmt.agent(chosen || '')} failed to complete (${exitInfo}${stderrSnippet}); retrying with next eligible agent.`));
|
|
867
|
+
agentErrors.set(chosen || '', {
|
|
819
868
|
exitInfo,
|
|
820
869
|
stderr: result.stderr,
|
|
821
870
|
stdout: result.stdout,
|
|
822
871
|
signal: result.signal,
|
|
823
872
|
status: result.status,
|
|
824
873
|
});
|
|
825
|
-
tried.add(chosen);
|
|
826
|
-
launched.add(chosen);
|
|
874
|
+
tried.add(chosen || '');
|
|
875
|
+
launched.add(chosen || '');
|
|
827
876
|
// Block non-custom agents on non-limit failures so selectAgent excludes them
|
|
828
877
|
// on the next retry iteration, and the review-loop fallback path can activate.
|
|
829
878
|
// custom (opencode/local AI) is excluded — exit 1 is a temporary local error.
|
|
830
879
|
if (chosen !== 'custom') {
|
|
831
880
|
const blockUntil = formatBlockUntil(new Date(Date.now() + DEFAULT_FALLBACK_HOURS * 60 * 60 * 1000));
|
|
832
881
|
try {
|
|
833
|
-
const blockResult = updateAgentBlockFn(chosen, blockUntil);
|
|
834
|
-
log(fmt.status('INFO', `Wrote blocklist entry for ${fmt.agent(chosen)} -> ${fmt.path(blockResult.path)} (${DEFAULT_FALLBACK_HOURS}h block)`));
|
|
835
|
-
} catch (err) {
|
|
836
|
-
log(fmt.status('WARN', `Could not persist blocklist entry for ${fmt.agent(chosen)}: ${err.message}`));
|
|
882
|
+
const blockResult = updateAgentBlockFn(chosen || '', blockUntil);
|
|
883
|
+
log(fmt.status('INFO', `Wrote blocklist entry for ${fmt.agent(chosen || '')} -> ${fmt.path(blockResult.path)} (${DEFAULT_FALLBACK_HOURS}h block)`));
|
|
884
|
+
} catch (/** @type {unknown} */ err) {
|
|
885
|
+
log(fmt.status('WARN', `Could not persist blocklist entry for ${fmt.agent(chosen || '')}: ${/** @type {any} */ (err).message}`));
|
|
837
886
|
}
|
|
838
887
|
}
|
|
839
|
-
chosen =
|
|
888
|
+
chosen = undefined;
|
|
840
889
|
continue;
|
|
841
890
|
}
|
|
842
891
|
// Record the marker so a subsequent same-(slug, role) launch knows which
|
|
@@ -846,9 +895,9 @@ async function startAgent(step, opts = {}) {
|
|
|
846
895
|
if (worktree && slug && role && result && result.status === 0 && !result.error) {
|
|
847
896
|
try {
|
|
848
897
|
const sessionId = result && result.sessionId ? result.sessionId : null;
|
|
849
|
-
sessionsModule.writeSession(worktree, slug, role, { agent: chosen, sessionId });
|
|
850
|
-
} catch (err) {
|
|
851
|
-
log(fmt.status('WARN', `Could not persist session marker for ${fmt.slug(slug)} (${role}): ${err.message}`));
|
|
898
|
+
(/** @type {any} */ (sessionsModule)).writeSession(worktree, slug, role, { agent: chosen || '', sessionId });
|
|
899
|
+
} catch (/** @type {unknown} */ err) {
|
|
900
|
+
log(fmt.status('WARN', `Could not persist session marker for ${fmt.slug(slug)} (${role}): ${/** @type {any} */ (err).message}`));
|
|
852
901
|
}
|
|
853
902
|
}
|
|
854
903
|
|
|
@@ -858,7 +907,7 @@ async function startAgent(step, opts = {}) {
|
|
|
858
907
|
|
|
859
908
|
// Legacy alias kept for backwards compatibility — draft.js calls this directly.
|
|
860
909
|
// Returns { agent, invocation, result } so callers can log which agent ran.
|
|
861
|
-
async function startDraftAgent(opts = {}) {
|
|
910
|
+
async function startDraftAgent(opts = { prompt: '' }) {
|
|
862
911
|
return startAgent('draft', opts);
|
|
863
912
|
}
|
|
864
913
|
|
|
@@ -873,7 +922,7 @@ module.exports = {
|
|
|
873
922
|
readAgentConfigOrExit,
|
|
874
923
|
assertAgentSupported,
|
|
875
924
|
workflowLauncherStatus,
|
|
876
|
-
setCommandPathProbe: (fn) => { _commandPathProbe = fn; },
|
|
925
|
+
setCommandPathProbe: (/** @type {(name: string) => string | null} */ fn) => { _commandPathProbe = fn; },
|
|
877
926
|
isAgentBlocked,
|
|
878
927
|
parseBlockUntil,
|
|
879
928
|
isInvalidAgentConfigError,
|