@yemi33/minions 0.1.1650 → 0.1.1652
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/CHANGELOG.md +11 -3
- package/dashboard/js/live-stream.js +6 -4
- package/engine/ado.js +17 -30
- package/engine/copilot-models.json +1 -1
- package/engine/dispatch.js +14 -6
- package/engine/github.js +24 -22
- package/engine/lifecycle.js +147 -48
- package/engine/runtimes/claude.js +90 -0
- package/engine/runtimes/copilot.js +90 -0
- package/engine/shared.js +45 -3
- package/engine/spawn-agent.js +9 -6
- package/engine/steering.js +23 -0
- package/engine.js +157 -156
- package/package.json +1 -1
- package/playbooks/fix.md +2 -2
- package/playbooks/implement-shared.md +2 -2
- package/playbooks/review.md +2 -3
- package/playbooks/shared-rules.md +12 -2
package/engine/spawn-agent.js
CHANGED
|
@@ -94,7 +94,7 @@ function parseSpawnArgs(argv) {
|
|
|
94
94
|
* Returns:
|
|
95
95
|
* {
|
|
96
96
|
* bin, leadingArgs, args, // → spawn(execPath OR bin, [bin?, ...leadingArgs, ...args])
|
|
97
|
-
* deliveryMode, // 'stdin' | 'arg' (
|
|
97
|
+
* deliveryMode, // 'stdin' | 'arg' (runtime adapter decision)
|
|
98
98
|
* finalPrompt, // adapter-built prompt text (may include sysprompt for some runtimes)
|
|
99
99
|
* usingNodeShim, // true → runtime returned a non-native binary (Claude cli.js)
|
|
100
100
|
* }
|
|
@@ -103,9 +103,10 @@ function buildSpawnInvocation({ runtime, resolved, promptText, sysPromptText, op
|
|
|
103
103
|
const finalPrompt = runtime.buildPrompt(promptText, sysPromptText);
|
|
104
104
|
const adapterOpts = { ...opts };
|
|
105
105
|
if (Array.isArray(addDirs) && addDirs.length) adapterOpts.addDirs = addDirs;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
const deliveryMode = typeof runtime.getPromptDeliveryMode === 'function'
|
|
107
|
+
? runtime.getPromptDeliveryMode(adapterOpts)
|
|
108
|
+
: (runtime.capabilities && runtime.capabilities.promptViaArg ? 'arg' : 'stdin');
|
|
109
|
+
if (deliveryMode === 'arg') {
|
|
109
110
|
adapterOpts.prompt = finalPrompt;
|
|
110
111
|
}
|
|
111
112
|
const adapterArgs = runtime.buildArgs(adapterOpts);
|
|
@@ -115,7 +116,7 @@ function buildSpawnInvocation({ runtime, resolved, promptText, sysPromptText, op
|
|
|
115
116
|
native,
|
|
116
117
|
leadingArgs,
|
|
117
118
|
args: [...adapterArgs, ...(passthrough || [])],
|
|
118
|
-
deliveryMode
|
|
119
|
+
deliveryMode,
|
|
119
120
|
finalPrompt,
|
|
120
121
|
usingNodeShim: !native,
|
|
121
122
|
};
|
|
@@ -159,7 +160,9 @@ function main() {
|
|
|
159
160
|
// already merged inside `runtime.buildPrompt(prompt, sys)`.
|
|
160
161
|
const isResume = opts.sessionId != null;
|
|
161
162
|
const sysTmpPath = sysPromptFile + '.tmp';
|
|
162
|
-
const wantsSystemPromptFile =
|
|
163
|
+
const wantsSystemPromptFile = typeof runtime.usesSystemPromptFile === 'function'
|
|
164
|
+
? runtime.usesSystemPromptFile({ isResume, opts })
|
|
165
|
+
: (!isResume && runtime.capabilities && runtime.capabilities.systemPromptFile);
|
|
163
166
|
if (wantsSystemPromptFile) {
|
|
164
167
|
fs.writeFileSync(sysTmpPath, sysPromptText);
|
|
165
168
|
opts.sysPromptFile = sysTmpPath;
|
package/engine/steering.js
CHANGED
|
@@ -162,6 +162,27 @@ function _processEvidenceTimes(rawOutput, observedAtMs) {
|
|
|
162
162
|
return times;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
function sessionIdFromEvent(obj) {
|
|
166
|
+
if (!obj || typeof obj !== 'object') return null;
|
|
167
|
+
const candidates = [
|
|
168
|
+
obj.session_id,
|
|
169
|
+
obj.sessionId,
|
|
170
|
+
obj.data?.session_id,
|
|
171
|
+
obj.data?.sessionId,
|
|
172
|
+
];
|
|
173
|
+
for (const value of candidates) {
|
|
174
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
175
|
+
}
|
|
176
|
+
if (obj.raw && obj.raw !== obj) return sessionIdFromEvent(obj.raw);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sessionIdFromOutputLine(line) {
|
|
181
|
+
const trimmed = String(line || '').trim();
|
|
182
|
+
if (!trimmed.startsWith('{')) return null;
|
|
183
|
+
try { return sessionIdFromEvent(JSON.parse(trimmed)); } catch { return null; }
|
|
184
|
+
}
|
|
185
|
+
|
|
165
186
|
function ackProcessedSteeringMessages(agentId, pendingEntries, rawOutput, opts = {}) {
|
|
166
187
|
const entries = Array.isArray(pendingEntries) ? pendingEntries : [];
|
|
167
188
|
if (entries.length === 0) return [];
|
|
@@ -183,5 +204,7 @@ module.exports = {
|
|
|
183
204
|
writeSteeringMessage,
|
|
184
205
|
listUnreadSteeringMessages,
|
|
185
206
|
buildPendingSteeringPrompt,
|
|
207
|
+
sessionIdFromEvent,
|
|
208
|
+
sessionIdFromOutputLine,
|
|
186
209
|
ackProcessedSteeringMessages,
|
|
187
210
|
};
|
package/engine.js
CHANGED
|
@@ -141,7 +141,7 @@ const { renderPlaybook, validatePlaybookVars, PLAYBOOK_REQUIRED_VARS,
|
|
|
141
141
|
const { runPostCompletionHooks, updateWorkItemStatus, syncPrdItemStatus, reconcilePrdStatuses, handlePostMerge, checkPlanCompletion,
|
|
142
142
|
syncPrsFromOutput, updatePrAfterReview, updatePrAfterFix, checkForLearnings, extractSkillsFromOutput,
|
|
143
143
|
updateAgentHistory, updateMetrics, createReviewFeedbackForAuthor, parseAgentOutput, syncPrdFromPrs,
|
|
144
|
-
isItemCompleted, classifyFailure, diagnoseEmptyOutput, processPendingRebases, resolveWorkItemPath } = require('./engine/lifecycle');
|
|
144
|
+
isItemCompleted, classifyFailure: classifyFailureFallback, diagnoseEmptyOutput, processPendingRebases, resolveWorkItemPath } = require('./engine/lifecycle');
|
|
145
145
|
|
|
146
146
|
// ─── Agent Spawner ──────────────────────────────────────────────────────────
|
|
147
147
|
|
|
@@ -232,62 +232,27 @@ async function preflightMergeSimulation(deps, mainRef, gitOpts, cwd) {
|
|
|
232
232
|
return { ok: true };
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
[WORK_TYPE.EXPLORE]: 30, [WORK_TYPE.ASK]: 20, [WORK_TYPE.REVIEW]: 30,
|
|
238
|
-
[WORK_TYPE.DECOMPOSE]: 15, [WORK_TYPE.PLAN]: 30, [WORK_TYPE.PLAN_TO_PRD]: 35,
|
|
239
|
-
[WORK_TYPE.MEETING]: 30,
|
|
240
|
-
[WORK_TYPE.IMPLEMENT]: 75, [WORK_TYPE.IMPLEMENT_LARGE]: 75, [WORK_TYPE.FIX]: 75,
|
|
241
|
-
[WORK_TYPE.TEST]: 50, [WORK_TYPE.VERIFY]: 100, [WORK_TYPE.DOCS]: 30,
|
|
242
|
-
};
|
|
243
|
-
function _maxTurnsForType(type, engineConfig) {
|
|
244
|
-
// Priority: per-type config override → global config override → built-in per-type default → global default
|
|
235
|
+
function _maxTurnsForType(type, engineConfig = {}) {
|
|
236
|
+
// Engine keeps only explicit operator caps; runtime/playbook owns task-level pacing.
|
|
245
237
|
const perType = engineConfig.maxTurnsByType || {};
|
|
246
238
|
if (perType[type]) return perType[type];
|
|
247
|
-
|
|
248
|
-
return globalOverride || _MAX_TURNS_BY_TYPE[type] || ENGINE_DEFAULTS.maxTurns;
|
|
239
|
+
return engineConfig.maxTurns || ENGINE_DEFAULTS.maxTurns;
|
|
249
240
|
}
|
|
250
241
|
|
|
251
242
|
// ─── Runtime adapter integration (P-2a6d9c4f) ────────────────────────────────
|
|
252
|
-
//
|
|
253
|
-
// _buildAgentSpawnFlags translates a resolved opts bag into the named CLI
|
|
254
|
-
// flags consumed by `engine/spawn-agent.js`. spawn-agent.js parses these back
|
|
255
|
-
// into an opts object and calls `runtime.buildArgs(opts)` once — keeping the
|
|
256
|
-
// adapter as the single source of truth for CLI-level flag formatting and
|
|
257
|
-
// avoiding double-emission.
|
|
258
|
-
//
|
|
259
|
-
// Capability gating happens HERE (in engine.js), not in the adapter:
|
|
260
|
-
// - Runtimes that don't support a feature (Copilot has no budgetCap, no
|
|
261
|
-
// bareMode, no fallbackModel) never see the flag. The adapter doesn't
|
|
262
|
-
// have to silently drop opts it can't honor.
|
|
263
|
-
// - Truly cross-runtime opts (model, maxTurns, allowedTools, effort,
|
|
264
|
-
// sessionId) are emitted whenever set; capability flags filter the
|
|
265
|
-
// ones that are runtime-specific.
|
|
266
|
-
// - Copilot-specific opts (`stream`, `disableBuiltinMcps`,
|
|
267
|
-
// `suppressAgentsMd`, `reasoningSummaries`) are emitted unconditionally;
|
|
268
|
-
// the Claude adapter ignores them via the "tolerate unknown opts" rule.
|
|
269
243
|
|
|
270
244
|
function _buildAgentSpawnFlags(runtime, opts = {}) {
|
|
245
|
+
if (runtime && typeof runtime.buildSpawnFlags === 'function') return runtime.buildSpawnFlags(opts);
|
|
271
246
|
const caps = (runtime && runtime.capabilities) || {};
|
|
272
247
|
const flags = ['--runtime', String(runtime?.name || 'claude')];
|
|
273
|
-
|
|
274
|
-
// Always-applicable: every runtime understands these.
|
|
275
248
|
if (opts.maxTurns != null) flags.push('--max-turns', String(opts.maxTurns));
|
|
276
249
|
if (opts.model) flags.push('--model', String(opts.model));
|
|
277
250
|
if (opts.allowedTools) flags.push('--allowedTools', String(opts.allowedTools));
|
|
278
|
-
|
|
279
|
-
// Capability-gated. The first three (effort/resume) gate on whether the
|
|
280
|
-
// runtime exposes the feature at all; the next three gate on whether the
|
|
281
|
-
// runtime's CLI surface actually accepts the flag.
|
|
282
251
|
if (caps.effortLevels && opts.effort) flags.push('--effort', String(opts.effort));
|
|
283
252
|
if (caps.sessionResume && opts.sessionId) flags.push('--resume', String(opts.sessionId));
|
|
284
253
|
if (caps.budgetCap && opts.maxBudget != null) flags.push('--max-budget-usd', String(opts.maxBudget));
|
|
285
254
|
if (caps.bareMode && opts.bare === true) flags.push('--bare');
|
|
286
255
|
if (caps.fallbackModel && opts.fallbackModel) flags.push('--fallback-model', String(opts.fallbackModel));
|
|
287
|
-
|
|
288
|
-
// Copilot-specific opts. Always emitted when set; the Claude adapter
|
|
289
|
-
// silently ignores them (per the "tolerate unknown opts" rule from
|
|
290
|
-
// P-7e3a8b1c). Engine code never branches on `runtime.name`.
|
|
291
256
|
if (opts.stream != null && opts.stream !== '') flags.push('--stream', String(opts.stream));
|
|
292
257
|
if (opts.disableBuiltinMcps === true) flags.push('--disable-builtin-mcps');
|
|
293
258
|
if (opts.suppressAgentsMd === true) flags.push('--no-custom-instructions');
|
|
@@ -296,8 +261,29 @@ function _buildAgentSpawnFlags(runtime, opts = {}) {
|
|
|
296
261
|
return flags;
|
|
297
262
|
}
|
|
298
263
|
|
|
264
|
+
function _runtimeLogger() {
|
|
265
|
+
return {
|
|
266
|
+
info: (msg) => log('info', msg),
|
|
267
|
+
warn: (msg) => log('warn', msg),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function _classifyAgentFailure(runtime, code, stdout, stderr) {
|
|
272
|
+
if (runtime && typeof runtime.classifyFailure === 'function') {
|
|
273
|
+
const classified = runtime.classifyFailure({
|
|
274
|
+
code,
|
|
275
|
+
stdout,
|
|
276
|
+
stderr,
|
|
277
|
+
fallback: classifyFailureFallback,
|
|
278
|
+
});
|
|
279
|
+
if (classified && classified.failureClass) return classified;
|
|
280
|
+
}
|
|
281
|
+
return { failureClass: classifyFailureFallback(code, stdout, stderr) };
|
|
282
|
+
}
|
|
283
|
+
|
|
299
284
|
function ackPendingSteeringFiles(agentId, procInfo, rawOutput, observedAtMs = Date.now()) {
|
|
300
285
|
if (!procInfo?._pendingSteeringFiles?.length || !rawOutput) return;
|
|
286
|
+
if (procInfo._steeringMessage || procInfo._steeringNoSession) return;
|
|
301
287
|
const acked = steering.ackProcessedSteeringMessages(agentId, procInfo._pendingSteeringFiles, rawOutput, { observedAtMs });
|
|
302
288
|
if (acked.length === 0) return;
|
|
303
289
|
|
|
@@ -307,6 +293,45 @@ function ackPendingSteeringFiles(agentId, procInfo, rawOutput, observedAtMs = Da
|
|
|
307
293
|
log('info', `Steering: ACKed ${acked.length} processed message(s) for ${agentId}`);
|
|
308
294
|
}
|
|
309
295
|
|
|
296
|
+
function captureSessionIdFromStdoutChunk(agentId, dispatchId, branchName, runtime, procInfo, chunk, state) {
|
|
297
|
+
if (!procInfo || procInfo.sessionId || !chunk) return;
|
|
298
|
+
const text = String(state.sessionLineBuffer || '') + String(chunk);
|
|
299
|
+
const lines = text.split('\n');
|
|
300
|
+
state.sessionLineBuffer = /[\r\n]$/.test(text) ? '' : (lines.pop() || '');
|
|
301
|
+
if (state.sessionLineBuffer.length > 65536) state.sessionLineBuffer = '';
|
|
302
|
+
|
|
303
|
+
for (const line of lines) {
|
|
304
|
+
const sessionId = steering.sessionIdFromOutputLine(line);
|
|
305
|
+
if (!sessionId) continue;
|
|
306
|
+
procInfo.sessionId = sessionId;
|
|
307
|
+
if (runtime && typeof runtime.saveSession === 'function') {
|
|
308
|
+
runtime.saveSession({
|
|
309
|
+
agentId,
|
|
310
|
+
dispatchId,
|
|
311
|
+
branch: branchName,
|
|
312
|
+
sessionId,
|
|
313
|
+
agentsDir: AGENTS_DIR,
|
|
314
|
+
logger: _runtimeLogger(),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function mergePendingSteeringEntries(...groups) {
|
|
322
|
+
const merged = [];
|
|
323
|
+
const seen = new Set();
|
|
324
|
+
for (const group of groups) {
|
|
325
|
+
const entries = Array.isArray(group) ? group : (group ? [group] : []);
|
|
326
|
+
for (const entry of entries) {
|
|
327
|
+
if (!entry?.path || seen.has(entry.path)) continue;
|
|
328
|
+
seen.add(entry.path);
|
|
329
|
+
merged.push(entry);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return merged;
|
|
333
|
+
}
|
|
334
|
+
|
|
310
335
|
// Resolve dependency plan item IDs to their PR branches
|
|
311
336
|
function resolveDependencyBranches(depIds, sourcePlan, project, config) {
|
|
312
337
|
const results = []; // [{ branch, prId }]
|
|
@@ -449,12 +474,31 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
449
474
|
const systemPrompt = buildSystemPrompt(agentId, config, project);
|
|
450
475
|
const agentContext = buildAgentContext(agentId, config, project);
|
|
451
476
|
const pendingSteering = steering.buildPendingSteeringPrompt(agentId);
|
|
477
|
+
const completionReportPath = shared.dispatchCompletionReportPath(id);
|
|
478
|
+
if (completionReportPath) {
|
|
479
|
+
try {
|
|
480
|
+
fs.mkdirSync(path.dirname(completionReportPath), { recursive: true });
|
|
481
|
+
safeUnlink(completionReportPath);
|
|
482
|
+
} catch (e) { log('warn', `completion report setup: ${e.message}`); }
|
|
483
|
+
}
|
|
484
|
+
const completionReportInstruction = completionReportPath ? [
|
|
485
|
+
'## Completion Report',
|
|
486
|
+
'',
|
|
487
|
+
`Before exiting, write a JSON completion report to: \`${completionReportPath}\``,
|
|
488
|
+
'',
|
|
489
|
+
'Use this shape: {"status":"success|partial|failed","summary":"...","verdict":"approved|changes-requested|null","pr":"PR URL or id if relevant","failure_class":"...","retryable":true|false,"needs_rerun":true|false}.',
|
|
490
|
+
'This report is the primary completion signal; fenced completion blocks are only a fallback.',
|
|
491
|
+
'',
|
|
492
|
+
].join('\n') : '';
|
|
452
493
|
const taskPromptWithSteering = pendingSteering.prompt
|
|
453
494
|
? `${pendingSteering.prompt}\n\n---\n\n${taskPrompt}`
|
|
454
495
|
: taskPrompt;
|
|
455
|
-
const
|
|
456
|
-
?
|
|
496
|
+
const taskPromptWithReport = completionReportInstruction
|
|
497
|
+
? `${taskPromptWithSteering}\n\n---\n\n${completionReportInstruction}`
|
|
457
498
|
: taskPromptWithSteering;
|
|
499
|
+
const fullTaskPrompt = agentContext
|
|
500
|
+
? `## Agent Context\n\n${agentContext}\n---\n\n## Your Task\n\n${taskPromptWithReport}`
|
|
501
|
+
: taskPromptWithReport;
|
|
458
502
|
const tmpDir = path.join(ENGINE_DIR, 'tmp');
|
|
459
503
|
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
|
460
504
|
const safeId = id.replace(/[:\\/*?"<>|]/g, '-');
|
|
@@ -890,32 +934,18 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
890
934
|
const resolvedMaxBudget = shared.resolveAgentMaxBudget(agentConfig, engineConfig);
|
|
891
935
|
const resolvedBare = shared.resolveAgentBareMode(agentConfig, engineConfig);
|
|
892
936
|
|
|
893
|
-
|
|
894
|
-
// The capability gate happens inside _buildAgentSpawnFlags — runtimes
|
|
895
|
-
// without effortLevels never see the flag.
|
|
896
|
-
const requestedEffort = engineConfig.agentEffort || (_FAST_WORK_TYPES.has(type) ? 'low' : null);
|
|
937
|
+
const requestedEffort = engineConfig.agentEffort || null;
|
|
897
938
|
|
|
898
|
-
// Session resume: gated on `runtime.capabilities.sessionResume`. Same branch
|
|
899
|
-
// means the agent is continuing work on the same PR/feature (e.g., author
|
|
900
|
-
// fixing their own build failure).
|
|
901
939
|
let cachedSessionId = null;
|
|
902
|
-
if (runtime
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
cachedSessionId = sessionFile.sessionId;
|
|
910
|
-
log('info', `Resuming session ${sessionFile.sessionId} for ${agentId} on branch ${branchName} (age: ${Math.round(sessionAge / 60000)}min)`);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
} catch (e) { log('warn', 'session resume lookup: ' + e.message); }
|
|
940
|
+
if (runtime && typeof runtime.getResumeSessionId === 'function') {
|
|
941
|
+
cachedSessionId = runtime.getResumeSessionId({
|
|
942
|
+
agentId,
|
|
943
|
+
branchName,
|
|
944
|
+
agentsDir: AGENTS_DIR,
|
|
945
|
+
logger: _runtimeLogger(),
|
|
946
|
+
});
|
|
914
947
|
}
|
|
915
948
|
|
|
916
|
-
// Build the spawn-agent.js flag bag. spawn-agent parses the named flags
|
|
917
|
-
// back into an opts object and calls runtime.buildArgs(opts) — so the
|
|
918
|
-
// adapter is the single source of truth for the actual CLI args.
|
|
919
949
|
const args = _buildAgentSpawnFlags(runtime, {
|
|
920
950
|
model: resolvedModel,
|
|
921
951
|
maxTurns: _maxTurnsForType(type, engineConfig),
|
|
@@ -941,6 +971,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
941
971
|
|
|
942
972
|
// Spawn the claude process
|
|
943
973
|
const childEnv = shared.cleanChildEnv();
|
|
974
|
+
if (completionReportPath) childEnv.MINIONS_COMPLETION_REPORT = completionReportPath;
|
|
944
975
|
|
|
945
976
|
// Inject cached ADO token so agents skip re-authentication (#998)
|
|
946
977
|
// getAdoToken() returns cached token (30-min TTL) or null — never blocks on browser auth
|
|
@@ -1014,6 +1045,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1014
1045
|
const MAX_OUTPUT = 1024 * 1024; // 1MB
|
|
1015
1046
|
let stdout = '';
|
|
1016
1047
|
let stderr = '';
|
|
1048
|
+
const sessionCaptureState = { sessionLineBuffer: '' };
|
|
1017
1049
|
let _trustCheckDone = false;
|
|
1018
1050
|
const _spawnTime = Date.now();
|
|
1019
1051
|
|
|
@@ -1025,8 +1057,10 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1025
1057
|
|
|
1026
1058
|
// Trust gate detection: check first 30s of output for trust/permission prompts
|
|
1027
1059
|
if (!_trustCheckDone && (Date.now() - _spawnTime) <= 30000) {
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1060
|
+
const blockedOnPermission = runtime && typeof runtime.detectPermissionGate === 'function'
|
|
1061
|
+
? runtime.detectPermissionGate(chunk)
|
|
1062
|
+
: false;
|
|
1063
|
+
if (blockedOnPermission) {
|
|
1030
1064
|
_trustCheckDone = true;
|
|
1031
1065
|
updateAgentStatus(id, AGENT_STATUS.TRUST_BLOCKED, 'Agent appears to be waiting for trust approval');
|
|
1032
1066
|
log('warn', `Trust gate detected for ${agentId} (${id}) — agent may be blocked on a permission prompt`);
|
|
@@ -1035,23 +1069,10 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1035
1069
|
_trustCheckDone = true; // past 30s window
|
|
1036
1070
|
}
|
|
1037
1071
|
|
|
1038
|
-
// Capture sessionId early for mid-session steering
|
|
1072
|
+
// Capture sessionId early for mid-session steering. Claude emits session_id;
|
|
1073
|
+
// Copilot emits sessionId, so use the runtime-neutral steering helper.
|
|
1039
1074
|
const procInfo = activeProcesses.get(id);
|
|
1040
|
-
|
|
1041
|
-
try {
|
|
1042
|
-
for (const line of chunk.split('\n')) {
|
|
1043
|
-
if (!line.trim() || !line.startsWith('{')) continue;
|
|
1044
|
-
const obj = JSON.parse(line);
|
|
1045
|
-
if (obj.session_id) {
|
|
1046
|
-
procInfo.sessionId = obj.session_id;
|
|
1047
|
-
safeWrite(path.join(AGENTS_DIR, agentId, 'session.json'), {
|
|
1048
|
-
sessionId: obj.session_id, dispatchId: id, savedAt: ts(), branch: branchName
|
|
1049
|
-
});
|
|
1050
|
-
break;
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
} catch { /* JSON parse — output may not be valid JSON */ }
|
|
1054
|
-
}
|
|
1075
|
+
captureSessionIdFromStdoutChunk(agentId, id, branchName, runtime, procInfo, chunk, sessionCaptureState);
|
|
1055
1076
|
|
|
1056
1077
|
ackPendingSteeringFiles(agentId, procInfo, chunk);
|
|
1057
1078
|
});
|
|
@@ -1112,8 +1133,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1112
1133
|
}
|
|
1113
1134
|
}
|
|
1114
1135
|
|
|
1115
|
-
// Write new prompt with steering
|
|
1116
|
-
|
|
1136
|
+
// Write new prompt with all unACKed steering messages. This keeps delivery
|
|
1137
|
+
// durable if the killed process had older pending messages that never
|
|
1138
|
+
// produced processing evidence before the resume.
|
|
1139
|
+
const pendingForResume = steering.buildPendingSteeringPrompt(agentId);
|
|
1140
|
+
const steerPromptBody = pendingForResume.prompt || steerMsg;
|
|
1141
|
+
const steerPrompt = `Message from your human teammate:\n\n${steerPromptBody}\n\nRespond to this, then continue working on your current task.`;
|
|
1117
1142
|
const steerPromptPath = path.join(ENGINE_DIR, 'tmp', `prompt-steer-${safeId}.md`);
|
|
1118
1143
|
try { safeWrite(steerPromptPath, steerPrompt); } catch (e) {
|
|
1119
1144
|
log('warn', `Steering: failed to write prompt for ${agentId}: ${e.message}`);
|
|
@@ -1123,19 +1148,6 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1123
1148
|
return;
|
|
1124
1149
|
}
|
|
1125
1150
|
|
|
1126
|
-
// Steering resume — gated on `runtime.capabilities.sessionResume`. If the
|
|
1127
|
-
// runtime can't resume sessions, fail fast: nothing to spawn here.
|
|
1128
|
-
if (!runtime.capabilities.sessionResume) {
|
|
1129
|
-
log('warn', `Steering: runtime ${runtime.name} does not support session resume — skipping for ${agentId}`);
|
|
1130
|
-
try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] Runtime ${runtime.name} does not support session resume. Message was: ${steerMsg}\n`); } catch {}
|
|
1131
|
-
try { fs.unlinkSync(steerPromptPath); } catch {}
|
|
1132
|
-
activeProcesses.delete(id);
|
|
1133
|
-
completeDispatch(id, DISPATCH_RESULT.SUCCESS, 'Steering not supported by runtime', '', { processWorkItemFailure: false });
|
|
1134
|
-
return;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
// Reuse the same flag-builder used by the main spawn so capability gates
|
|
1138
|
-
// and runtime-specific opts stay consistent across the two paths.
|
|
1139
1151
|
const resumeArgs = _buildAgentSpawnFlags(runtime, {
|
|
1140
1152
|
model: resolvedModel,
|
|
1141
1153
|
maxTurns: engineConfig?.maxTurns || ENGINE_DEFAULTS.maxTurns,
|
|
@@ -1149,9 +1161,18 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1149
1161
|
suppressAgentsMd: engineConfig?.copilotSuppressAgentsMd,
|
|
1150
1162
|
reasoningSummaries: engineConfig?.copilotReasoningSummaries,
|
|
1151
1163
|
});
|
|
1164
|
+
if (!resumeArgs.includes('--resume')) {
|
|
1165
|
+
log('warn', `Steering: runtime ${runtime.name} did not accept session resume — skipping for ${agentId}`);
|
|
1166
|
+
try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] Runtime ${runtime.name} does not support session resume. Message was: ${steerMsg}\n`); } catch {}
|
|
1167
|
+
try { fs.unlinkSync(steerPromptPath); } catch {}
|
|
1168
|
+
activeProcesses.delete(id);
|
|
1169
|
+
completeDispatch(id, DISPATCH_RESULT.SUCCESS, 'Steering not supported by runtime', '', { processWorkItemFailure: false });
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1152
1172
|
|
|
1153
1173
|
const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
1154
1174
|
const childEnv = shared.cleanChildEnv();
|
|
1175
|
+
if (completionReportPath) childEnv.MINIONS_COMPLETION_REPORT = completionReportPath;
|
|
1155
1176
|
// Inject cached ADO token for steering session too (#998)
|
|
1156
1177
|
try {
|
|
1157
1178
|
const adoToken = await getAdoToken();
|
|
@@ -1184,19 +1205,26 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1184
1205
|
startedAt: procInfo.startedAt,
|
|
1185
1206
|
sessionId: steerSessionId,
|
|
1186
1207
|
lastRealOutputAt: Date.now(),
|
|
1187
|
-
_pendingSteeringFiles:
|
|
1208
|
+
_pendingSteeringFiles: mergePendingSteeringEntries(
|
|
1209
|
+
procInfo._pendingSteeringFiles,
|
|
1210
|
+
pendingForResume.entries,
|
|
1211
|
+
steerEntry,
|
|
1212
|
+
),
|
|
1188
1213
|
});
|
|
1189
1214
|
|
|
1190
1215
|
// Reset output buffers so post-completion parsing only sees the resumed session
|
|
1191
1216
|
stdout = '';
|
|
1192
1217
|
stderr = '';
|
|
1218
|
+
sessionCaptureState.sessionLineBuffer = '';
|
|
1193
1219
|
// Re-wire stdout/stderr handlers (same as original)
|
|
1194
1220
|
resumeProc.stdout.on('data', (data) => {
|
|
1195
1221
|
const chunk = data.toString();
|
|
1196
1222
|
realActivityMap.set(id, Date.now());
|
|
1197
1223
|
if (stdout.length < MAX_OUTPUT) stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
|
|
1198
1224
|
try { fs.appendFileSync(liveOutputPath, chunk); } catch { /* optional */ }
|
|
1199
|
-
|
|
1225
|
+
const resumeInfo = activeProcesses.get(id);
|
|
1226
|
+
captureSessionIdFromStdoutChunk(agentId, id, branchName, runtime, resumeInfo, chunk, sessionCaptureState);
|
|
1227
|
+
ackPendingSteeringFiles(agentId, resumeInfo, chunk);
|
|
1200
1228
|
});
|
|
1201
1229
|
resumeProc.stderr.on('data', (data) => {
|
|
1202
1230
|
const chunk = data.toString();
|
|
@@ -1284,11 +1312,13 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1284
1312
|
safeWrite(latestPath, outputContent); // overwrite latest for dashboard compat
|
|
1285
1313
|
|
|
1286
1314
|
// Classify failure for non-zero exits
|
|
1287
|
-
const
|
|
1315
|
+
const failureInfo = code !== 0 ? _classifyAgentFailure(runtime, code, stdout, stderr) : {};
|
|
1316
|
+
const failureClass = failureInfo.failureClass;
|
|
1288
1317
|
|
|
1289
|
-
// Detect configuration errors (e.g.
|
|
1318
|
+
// Detect configuration errors (e.g. missing runtime CLI) — fail immediately with clear message
|
|
1290
1319
|
if (code === 78) {
|
|
1291
|
-
const
|
|
1320
|
+
const runtimeHint = runtime.installHint ? ` ${runtime.installHint}` : '';
|
|
1321
|
+
const errMsg = stderr.trim() || `Configuration error — ${runtimeName} runtime unavailable.${runtimeHint}`;
|
|
1292
1322
|
log('error', `Agent ${agentId} (${id}) failed: ${errMsg} [${failureClass}]`);
|
|
1293
1323
|
completeDispatch(id, DISPATCH_RESULT.ERROR, errMsg, '', { failureClass });
|
|
1294
1324
|
try { fs.unlinkSync(sysPromptPath); } catch { /* cleanup */ }
|
|
@@ -1298,18 +1328,27 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1298
1328
|
}
|
|
1299
1329
|
|
|
1300
1330
|
// Parse output and run all post-completion hooks
|
|
1301
|
-
const { resultSummary, autoRecovered, completionContractFailure } = await runPostCompletionHooks(dispatchItem, agentId, code, stdout, config);
|
|
1331
|
+
const { resultSummary, autoRecovered, completionContractFailure, structuredCompletion, agentReportedFailure, agentRetryable } = await runPostCompletionHooks(dispatchItem, agentId, code, stdout, config);
|
|
1332
|
+
const retryableDecision = typeof agentRetryable === 'boolean' ? agentRetryable : failureInfo.retryable;
|
|
1302
1333
|
|
|
1303
1334
|
// Move from active to completed in dispatch (single source of truth for agent status)
|
|
1304
1335
|
// autoRecovered: agent failed after creating PRs — treat as success
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1336
|
+
const hardContractFail = completionContractFailure?.severity === 'hard'
|
|
1337
|
+
|| completionContractFailure?.nonTerminal === true;
|
|
1338
|
+
const effectiveResult = hardContractFail ? DISPATCH_RESULT.ERROR : (((code === 0 && !agentReportedFailure) || autoRecovered) ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR);
|
|
1339
|
+
const completeOpts = hardContractFail
|
|
1307
1340
|
? { processWorkItemFailure: false }
|
|
1308
|
-
: (effectiveResult === DISPATCH_RESULT.ERROR
|
|
1341
|
+
: (effectiveResult === DISPATCH_RESULT.ERROR ? {
|
|
1342
|
+
...(failureClass ? { failureClass } : {}),
|
|
1343
|
+
...(typeof retryableDecision === 'boolean' ? { agentRetryable: retryableDecision } : {}),
|
|
1344
|
+
...(structuredCompletion?.failure_class ? { failureClass: structuredCompletion.failure_class } : {}),
|
|
1345
|
+
} : {});
|
|
1309
1346
|
// Extract last 5 non-empty stderr lines as error context when exit code is non-zero
|
|
1310
1347
|
let errorReason = '';
|
|
1311
|
-
if (
|
|
1348
|
+
if (hardContractFail) {
|
|
1312
1349
|
errorReason = completionContractFailure.reason || 'PR attachment contract failed';
|
|
1350
|
+
} else if (agentReportedFailure && structuredCompletion?.summary) {
|
|
1351
|
+
errorReason = String(structuredCompletion.summary).slice(0, 300);
|
|
1313
1352
|
} else if (effectiveResult === DISPATCH_RESULT.ERROR) {
|
|
1314
1353
|
errorReason = stderr.split('\n').filter(l => l.trim()).slice(-5).join(' | ').trim().slice(0, 300);
|
|
1315
1354
|
// W-mo3zul9pirjb — when claude CLI exits in <3s with code 1 and no output (the
|
|
@@ -2177,24 +2216,10 @@ async function discoverFromPrs(config, project) {
|
|
|
2177
2216
|
// The poller holds reviewStatus at 'waiting' until the reviewer acts on the new code.
|
|
2178
2217
|
const awaitingReReview = reviewStatus === 'waiting' && !!pr.minionsReview?.fixedAt;
|
|
2179
2218
|
|
|
2180
|
-
// Review→fix cycle cap — stop review/fix dispatch after N iterations, but allow build fixes and conflict fixes
|
|
2181
|
-
const evalMax = config.engine?.evalMaxIterations ?? ENGINE_DEFAULTS.evalMaxIterations;
|
|
2182
|
-
const evalCycles = pr._reviewFixCycles || 0;
|
|
2183
|
-
const evalEscalated = evalCycles >= evalMax;
|
|
2184
|
-
if (evalEscalated && !pr._evalEscalated) {
|
|
2185
|
-
try {
|
|
2186
|
-
mutatePullRequests(projectPrPath(project), prs => {
|
|
2187
|
-
const target = shared.findPrRecord(prs, pr, project);
|
|
2188
|
-
if (target) target._evalEscalated = true;
|
|
2189
|
-
});
|
|
2190
|
-
} catch (e) { log('warn', 'mark eval escalated: ' + e.message); }
|
|
2191
|
-
log('warn', `PR ${pr.id}: review→fix escalated after ${evalCycles} cycles — suspending review/re-review and review-fix dispatch; build/conflict fixes may continue`);
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
2219
|
// PRs needing review: evalLoop gates the entire review+fix cycle; pollEnabled ensures reviewStatus is fresh
|
|
2195
2220
|
const reviewEnabled = evalLoopEnabled && pollEnabled;
|
|
2196
2221
|
const alreadyReviewed = pr.lastReviewedAt && (!pr.lastPushedAt || pr.lastPushedAt <= pr.lastReviewedAt);
|
|
2197
|
-
const needsReview = reviewEnabled && reviewStatus === 'pending' && !alreadyReviewed
|
|
2222
|
+
const needsReview = reviewEnabled && reviewStatus === 'pending' && !alreadyReviewed;
|
|
2198
2223
|
if (needsReview) {
|
|
2199
2224
|
const key = `review-${project?.name || 'default'}-${prDisplayId}`;
|
|
2200
2225
|
if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
@@ -2266,6 +2291,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2266
2291
|
const earlier = coalesced.map(c => c.feedbackContent).filter(Boolean).join('\n\n---\n\n');
|
|
2267
2292
|
if (earlier) reviewNote = earlier + '\n\n---\n\n' + reviewNote;
|
|
2268
2293
|
}
|
|
2294
|
+
reviewNote = `New PR comments were observed. Read the full PR thread, decide whether the comments require code/documentation/test changes, make only necessary changes, and push if action is needed.\n\n${reviewNote}`;
|
|
2269
2295
|
|
|
2270
2296
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
2271
2297
|
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: prBranch,
|
|
@@ -2280,7 +2306,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2280
2306
|
const fixedAfterReview = !!(pr.minionsReview?.fixedAt &&
|
|
2281
2307
|
(!pr.lastReviewedAt || pr.minionsReview.fixedAt > pr.lastReviewedAt));
|
|
2282
2308
|
const needsReReview = reviewEnabled && reviewStatus === 'waiting' &&
|
|
2283
|
-
fixedAfterReview && !
|
|
2309
|
+
fixedAfterReview && !fixDispatched;
|
|
2284
2310
|
if (needsReReview) {
|
|
2285
2311
|
const key = `rereview-${project?.name || 'default'}-${prDisplayId}`;
|
|
2286
2312
|
// Skip isAlreadyDispatched — fixedAfterReview/lastReviewedAt already dedupe; the 1hr
|
|
@@ -2320,7 +2346,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2320
2346
|
|
|
2321
2347
|
// PRs with changes requested → route back to author for fix
|
|
2322
2348
|
// Gate on evalLoopEnabled — the review→fix cycle is the eval loop
|
|
2323
|
-
if (evalLoopEnabled && reviewStatus === 'changes-requested' && !awaitingReReview && !
|
|
2349
|
+
if (evalLoopEnabled && reviewStatus === 'changes-requested' && !awaitingReReview && !fixDispatched) {
|
|
2324
2350
|
const key = `fix-${project?.name || 'default'}-${prDisplayId}`;
|
|
2325
2351
|
if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
2326
2352
|
const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
|
|
@@ -2334,13 +2360,6 @@ async function discoverFromPrs(config, project) {
|
|
|
2334
2360
|
}, `Fix ${pr.id}: ${pr.title || ''} — review feedback`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2335
2361
|
if (item) {
|
|
2336
2362
|
newWork.push(item); setCooldown(key); fixDispatched = true;
|
|
2337
|
-
// Increment review→fix cycle counter
|
|
2338
|
-
try {
|
|
2339
|
-
mutatePullRequests(projectPrPath(project), prs => {
|
|
2340
|
-
const target = shared.findPrRecord(prs, pr, project);
|
|
2341
|
-
if (target) target._reviewFixCycles = (target._reviewFixCycles || 0) + 1;
|
|
2342
|
-
});
|
|
2343
|
-
} catch (e) { log('warn', 'increment review-fix cycles: ' + e.message); }
|
|
2344
2363
|
}
|
|
2345
2364
|
}
|
|
2346
2365
|
|
|
@@ -2354,23 +2373,6 @@ async function discoverFromPrs(config, project) {
|
|
|
2354
2373
|
const autoFixBuilds = config.engine?.autoFixBuilds ?? ENGINE_DEFAULTS.autoFixBuilds;
|
|
2355
2374
|
const fixThrottled = isAdoProject ? isAdoThrottled() : isGhThrottled();
|
|
2356
2375
|
if (autoFixBuilds && pr.status === PR_STATUS.ACTIVE && pr.buildStatus === 'failing') {
|
|
2357
|
-
const maxBuildFix = config.engine?.maxBuildFixAttempts ?? ENGINE_DEFAULTS.maxBuildFixAttempts;
|
|
2358
|
-
|
|
2359
|
-
// Check if max retry cap reached — escalate to human instead of dispatching another fix
|
|
2360
|
-
if ((pr.buildFixAttempts || 0) >= maxBuildFix) {
|
|
2361
|
-
if (!pr.buildFixEscalated) {
|
|
2362
|
-
try {
|
|
2363
|
-
const prPath = projectPrPath(project);
|
|
2364
|
-
mutatePullRequests(prPath, prs => {
|
|
2365
|
-
const target = shared.findPrRecord(prs, pr, project);
|
|
2366
|
-
if (target) target.buildFixEscalated = true;
|
|
2367
|
-
});
|
|
2368
|
-
} catch (e) { log('warn', 'mark build fix escalated: ' + e.message); }
|
|
2369
|
-
log('warn', `PR ${pr.id}: build fix escalated after ${pr.buildFixAttempts} attempts — suspending auto-dispatch`);
|
|
2370
|
-
}
|
|
2371
|
-
continue;
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
2376
|
const key = `build-fix-${project?.name || 'default'}-${prDisplayId}`;
|
|
2375
2377
|
if (fixThrottled || isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
2376
2378
|
|
|
@@ -2423,10 +2425,11 @@ async function discoverFromPrs(config, project) {
|
|
|
2423
2425
|
const prBranch = ensurePrBranchForDispatch(project, pr, 'build-fix');
|
|
2424
2426
|
if (!prBranch) continue;
|
|
2425
2427
|
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2428
|
+
const reviewNote = [
|
|
2429
|
+
`Build is failing: ${pr.buildFailReason || 'check CI pipeline for details'}.`,
|
|
2430
|
+
'Inspect the live PR checks/build logs yourself, decide the root cause, fix it, run the relevant local validation, and push.',
|
|
2431
|
+
pr.url ? `PR URL: ${pr.url}` : '',
|
|
2432
|
+
].filter(Boolean).join('\n');
|
|
2430
2433
|
|
|
2431
2434
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
2432
2435
|
pr_id: pr.id, pr_branch: prBranch,
|
|
@@ -2434,17 +2437,15 @@ async function discoverFromPrs(config, project) {
|
|
|
2434
2437
|
}, `Fix build failure on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2435
2438
|
if (item) {
|
|
2436
2439
|
newWork.push(item); setCooldown(key); fixDispatched = true;
|
|
2437
|
-
// Increment build fix attempts counter
|
|
2438
2440
|
try {
|
|
2439
2441
|
const prPath = projectPrPath(project);
|
|
2440
2442
|
mutatePullRequests(prPath, prs => {
|
|
2441
2443
|
const target = shared.findPrRecord(prs, pr, project);
|
|
2442
2444
|
if (target) {
|
|
2443
|
-
target.buildFixAttempts = (target.buildFixAttempts || 0) + 1;
|
|
2444
2445
|
target._buildFixPushedAt = ts();
|
|
2445
2446
|
}
|
|
2446
2447
|
});
|
|
2447
|
-
} catch (e) { log('warn', '
|
|
2448
|
+
} catch (e) { log('warn', 'mark build fix dispatched: ' + e.message); }
|
|
2448
2449
|
}
|
|
2449
2450
|
|
|
2450
2451
|
if (pr.agent && !pr._buildFailNotified) {
|
|
@@ -2501,7 +2502,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2501
2502
|
if (!prBranch) continue;
|
|
2502
2503
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
2503
2504
|
pr_id: pr.id, pr_branch: prBranch,
|
|
2504
|
-
review_note: `This PR has merge conflicts with the target branch.
|
|
2505
|
+
review_note: `This PR has merge conflicts with the target branch. Inspect the live PR and repository history, choose the safest merge/rebase/update strategy, resolve all conflicts, validate the result, and push the branch.`,
|
|
2505
2506
|
}, `Fix merge conflicts on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2506
2507
|
if (item) {
|
|
2507
2508
|
newWork.push(item);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1652",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|
package/playbooks/fix.md
CHANGED
|
@@ -80,7 +80,7 @@ Your task is complete when each review finding has either been fixed or answered
|
|
|
80
80
|
|
|
81
81
|
## Completion
|
|
82
82
|
|
|
83
|
-
After finishing,
|
|
83
|
+
After finishing, write the JSON completion report described in the shared rules. Also output this structured completion block as a compatibility fallback:
|
|
84
84
|
|
|
85
85
|
```completion
|
|
86
86
|
status: done | partial | failed
|
|
@@ -91,4 +91,4 @@ failure_class: N/A
|
|
|
91
91
|
pending: <any remaining work, or none>
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
Replace the values with your actual results.
|
|
94
|
+
Replace the values with your actual results.
|