@hecer/yoke 1.17.0 → 1.19.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +34 -0
- package/README.md +83 -897
- package/canon/loop/loop-spec.md +20 -3
- package/canon/manifest.yaml +1 -1
- package/dist/agents/process.js +42 -11
- package/dist/agents/providers.js +11 -2
- package/dist/agents/sol-pi-runtime.js +76 -0
- package/dist/cli.js +72 -3
- package/dist/code-intelligence/adapters/graphify.js +1 -1
- package/dist/code-intelligence/adapters/mcp.js +25 -4
- package/dist/code-intelligence/adapters/serena.js +1 -1
- package/dist/code-intelligence/coordinator.js +2 -1
- package/dist/code-intelligence/evidence.js +12 -1
- package/dist/code-intelligence/mcp-client.js +2 -2
- package/dist/dashboard/page.js +1 -1
- package/dist/dashboard/panels.js +25 -3
- package/dist/dashboard/server.js +62 -1
- package/dist/loop/git.js +1 -1
- package/dist/loop/loop.js +13 -1
- package/dist/loop/prd.js +11 -0
- package/dist/loop/reporter.js +8 -6
- package/dist/loop/run-command.js +388 -15
- package/dist/prd/explore.js +441 -0
- package/dist/retrofit/config.js +28 -0
- package/docs/CONTINUOUS-EXPLORATION.md +67 -0
- package/docs/HARNESSES.md +1 -1
- package/docs/SOL-PI.md +41 -0
- package/docs/code-intelligence-stdio-fix-2026-09-27.md +8 -0
- package/docs/parallel-execution.md +12 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
package/dist/dashboard/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import { lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from '
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { parse } from 'yaml';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { configPath, defaultConfig, loadConfig, resolveSolPiSettings, saveConfig, SolPiSettingsSchema } from '../retrofit/config.js';
|
|
7
8
|
import { listProjects } from './registry.js';
|
|
8
9
|
import { dashboardPage } from './page.js';
|
|
9
10
|
import { readEvents } from '../observability/events.js';
|
|
@@ -99,6 +100,28 @@ function writePauseSignal(root) {
|
|
|
99
100
|
throw error;
|
|
100
101
|
}
|
|
101
102
|
}
|
|
103
|
+
function assertSafeConfigTarget(root) {
|
|
104
|
+
const directory = join(root, '.yoke');
|
|
105
|
+
const file = configPath(root);
|
|
106
|
+
try {
|
|
107
|
+
const stat = lstatSync(directory);
|
|
108
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
109
|
+
throw new Error('Unsafe project settings target');
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error.code !== 'ENOENT')
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const stat = lstatSync(file);
|
|
117
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
118
|
+
throw new Error('Unsafe project settings target');
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error.code !== 'ENOENT')
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
102
125
|
function projectIsRunning(root) {
|
|
103
126
|
if (activeResumes.has(root))
|
|
104
127
|
return true;
|
|
@@ -213,13 +236,51 @@ export async function startDashboard(options = {}) {
|
|
|
213
236
|
send(200, workspaceAnalytics(listProjects(), period, sort));
|
|
214
237
|
return;
|
|
215
238
|
}
|
|
216
|
-
const match = /^\/api\/projects\/([a-f0-9]{32})(\/pause|\/resume|\/notes|\/changes|\/analytics|\/history|\/events)?$/u.exec(path);
|
|
239
|
+
const match = /^\/api\/projects\/([a-f0-9]{32})(\/pause|\/resume|\/notes|\/changes|\/settings|\/analytics|\/history|\/events)?$/u.exec(path);
|
|
217
240
|
if (match) {
|
|
218
241
|
const project = listProjects().find(project => project.id === match[1]);
|
|
219
242
|
if (!project) {
|
|
220
243
|
send(404, { error: 'Unknown project' });
|
|
221
244
|
return;
|
|
222
245
|
}
|
|
246
|
+
if (req.method === 'GET' && match[2] === '/settings') {
|
|
247
|
+
if (project.error) {
|
|
248
|
+
send(409, { error: project.error });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
assertSafeConfigTarget(project.root);
|
|
252
|
+
const config = loadConfig(project.root) ?? defaultConfig('0.0.0');
|
|
253
|
+
send(200, resolveSolPiSettings(config));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (req.method === 'POST' && match[2] === '/settings') {
|
|
257
|
+
let settings;
|
|
258
|
+
try {
|
|
259
|
+
settings = SolPiSettingsSchema.parse(await requestBody(req, {}));
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
send(error.message === 'Dashboard request body too large' ? 413 : 400, { error: error.message === 'Dashboard request body too large' ? error.message : 'Invalid project settings payload' });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (project.error) {
|
|
266
|
+
send(409, { error: project.error });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
assertSafeConfigTarget(project.root);
|
|
271
|
+
const config = loadConfig(project.root) ?? defaultConfig('0.0.0');
|
|
272
|
+
saveConfig(project.root, { ...config, solpi: settings });
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (error.message === 'Unsafe project settings target') {
|
|
276
|
+
send(409, { error: error.message });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
send(200, settings);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
223
284
|
if (req.method === 'GET' && match[2] === '/analytics') {
|
|
224
285
|
if (project.error) {
|
|
225
286
|
send(409, { error: project.error });
|
package/dist/loop/git.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { sanitizeCommitMessage } from './identity.js';
|
|
3
|
-
export const RUNTIME_PATHS = [".yoke/supervision", ".yoke/provider-processes", '.yoke/artifacts', '.yoke/events', '.yoke/history', '.yoke/routing', '.yoke/checks', '.yoke/goal.json', '.yoke/goal.pause', '.yoke/loop-status.json', '.yoke/loop.lock', '.yoke/loop.lock.takeover', '.yoke/loop.lock.takeover.recovery', '.yoke/runner.pid', '.yoke/story-durations.json'];
|
|
3
|
+
export const RUNTIME_PATHS = [".yoke/supervision", ".yoke/provider-processes", '.yoke/artifacts', '.yoke/events', '.yoke/history', '.yoke/routing', '.yoke/checks', '.yoke/goal.json', '.yoke/goal.pause', '.yoke/loop.pause', '.yoke/loop-status.json', '.yoke/loop.lock', '.yoke/loop.lock.takeover', '.yoke/loop.lock.takeover.recovery', '.yoke/runner.pid', '.yoke/story-durations.json'];
|
|
4
4
|
// Directory pathspecs include descendants and also exclude the directory entry.
|
|
5
5
|
export const RUNTIME_EXCLUDES = RUNTIME_PATHS.map(path => `:(exclude)${path}`);
|
|
6
6
|
export function stageImplementation(dir) {
|
package/dist/loop/loop.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { knownInfrastructureFailure } from "../routing/capability.js";
|
|
2
|
-
import { existsSync, unlinkSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, unlinkSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { acceptanceProtectionProblem } from '../check/command.js';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { isAcceptanceCriterion, loadPrd, savePrd, selectNextStory, allPass, progress, storyPathSegment } from './prd.js';
|
|
@@ -102,6 +102,18 @@ function runQualityReview(opts, executionDir, story, reporter) {
|
|
|
102
102
|
export function pauseFilePath(targetDir) {
|
|
103
103
|
return join(targetDir, '.yoke', 'loop.pause');
|
|
104
104
|
}
|
|
105
|
+
/** Request a pause at the next safe story or exploration boundary. */
|
|
106
|
+
export function requestLoopPause(targetDir) {
|
|
107
|
+
const file = pauseFilePath(targetDir);
|
|
108
|
+
mkdirSync(join(targetDir, '.yoke'), { recursive: true });
|
|
109
|
+
try {
|
|
110
|
+
writeFileSync(file, `${new Date().toISOString()}\n`, { flag: 'wx' });
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error.code !== 'EEXIST')
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
105
117
|
function consumePause(targetDir) {
|
|
106
118
|
const file = pauseFilePath(targetDir);
|
|
107
119
|
if (!existsSync(file))
|
package/dist/loop/prd.js
CHANGED
|
@@ -11,6 +11,15 @@ export const AcceptanceCriterionSchema = z.object({
|
|
|
11
11
|
text: z.string().min(1),
|
|
12
12
|
verify: z.array(z.string().min(1)).min(1),
|
|
13
13
|
});
|
|
14
|
+
export const ExplorationOriginSchema = z.object({
|
|
15
|
+
version: z.literal(1),
|
|
16
|
+
rationale: z.string().min(1).max(2_000),
|
|
17
|
+
expectedBenefit: z.string().min(1).max(2_000),
|
|
18
|
+
confidence: z.number().min(0.8).max(1),
|
|
19
|
+
risk: z.enum(['low', 'medium']),
|
|
20
|
+
evidence: z.array(z.object({ path: z.string().min(1).max(500), observation: z.string().min(1).max(1_000) }).strict()).min(1).max(6),
|
|
21
|
+
discoveredAt: z.string().datetime(),
|
|
22
|
+
}).strict();
|
|
14
23
|
export function criterionCommandProblem(criterion) {
|
|
15
24
|
const normalizedId = criterion.id.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
16
25
|
for (const raw of criterion.verify) {
|
|
@@ -46,6 +55,8 @@ export const StorySchema = z.object({
|
|
|
46
55
|
agent: AgentSchema.optional(),
|
|
47
56
|
/** Inbox request that created this story. Used for idempotent append-only intake. */
|
|
48
57
|
sourceChange: z.string().min(1).optional(),
|
|
58
|
+
/** Evidence retained for stories created by opt-in continuous exploration. */
|
|
59
|
+
exploration: ExplorationOriginSchema.optional(),
|
|
49
60
|
quality: StoryQualityDeclarationSchema.optional(),
|
|
50
61
|
assessment: AssessmentSchema.optional(),
|
|
51
62
|
/** Binding to the task, upstream contracts and approved planning brief. */
|
package/dist/loop/reporter.js
CHANGED
|
@@ -220,10 +220,12 @@ export function makeReporter(dir, opts = {}, now = () => new Date()) {
|
|
|
220
220
|
persist({ ...base, state: 'running', phase: undefined, story: story.id, storyTitle: story.title,
|
|
221
221
|
progress, ...(eta ? { eta } : {}), updatedAt: now().toISOString() }, 'story-done', `✓ ${story.id} done${took} — ${progress.passed}/${progress.total} (${percentOf(progress)}%)${hint}`);
|
|
222
222
|
},
|
|
223
|
-
phase(phase) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
223
|
+
phase(phase, reason, progress) {
|
|
224
|
+
const status = current ?? emptyStatus(now().toISOString());
|
|
225
|
+
const base = phase === 'exploring' || phase === 'waiting-exploration' || phase === 'waiting-recovery'
|
|
226
|
+
? (({ story: _story, storyTitle: _storyTitle, ...withoutStory }) => withoutStory)(status)
|
|
227
|
+
: status;
|
|
228
|
+
persist({ ...base, state: 'running', phase, ...(progress ? { progress } : {}), ...(reason ? { reason } : { reason: undefined }), updatedAt: now().toISOString() }, phase, ` · ${phase}…`);
|
|
227
229
|
},
|
|
228
230
|
blocked(reason) {
|
|
229
231
|
const base = current ?? emptyStatus(now().toISOString());
|
|
@@ -237,9 +239,9 @@ export function makeReporter(dir, opts = {}, now = () => new Date()) {
|
|
|
237
239
|
persist({ ...withoutParallel(current ?? emptyStatus(now().toISOString())), state: 'cap-reached', phase: undefined,
|
|
238
240
|
progress, updatedAt: now().toISOString() }, 'cap-reached', `◾ iteration cap reached — ${progress.passed}/${progress.total}`);
|
|
239
241
|
},
|
|
240
|
-
paused(progress) {
|
|
242
|
+
paused(progress, reason) {
|
|
241
243
|
persist({ ...withoutParallel(current ?? emptyStatus(now().toISOString())), state: 'paused', phase: undefined,
|
|
242
|
-
progress, updatedAt: now().toISOString() }, 'paused', `⏸ loop paused — ${progress.passed}/${progress.total}`);
|
|
244
|
+
progress, ...(reason ? { reason } : {}), updatedAt: now().toISOString() }, 'paused', `⏸ loop paused — ${progress.passed}/${progress.total}`);
|
|
243
245
|
},
|
|
244
246
|
quality(status) {
|
|
245
247
|
const base = current ?? emptyStatus(now().toISOString());
|
package/dist/loop/run-command.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { roleSelection } from "../routing/capability.js";
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
3
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
4
4
|
import { loadConfig, saveConfig, defaultConfig, resolveOutputPolicy, resolveVerifyCommand } from '../retrofit/config.js';
|
|
5
|
-
import { loadPrd, progress } from './prd.js';
|
|
6
|
-
import { runLoop } from './loop.js';
|
|
5
|
+
import { allPass, loadPrd, progress, selectNextStory } from './prd.js';
|
|
6
|
+
import { pauseFilePath, requestLoopPause, runLoop } from './loop.js';
|
|
7
7
|
import { commitPaths, realGitOps } from './git.js';
|
|
8
8
|
import { makeRunner, makeReviewRunner, isAgentAvailable } from './runner.js';
|
|
9
9
|
import { commandVerifier, commandsVerifier, retryingVerifier } from './verify.js';
|
|
@@ -26,6 +26,7 @@ import { designVerifier } from '../scan/gate.js';
|
|
|
26
26
|
import { prepareIsolatedWorktree } from './recovery.js';
|
|
27
27
|
import { AGENT_LIST, SUPPORTED_AGENTS } from '../agents/catalog.js';
|
|
28
28
|
import { MAX_PROJECT_WORKERS, sharedPoolStatus, withSharedWorkerSync } from './resource-pool.js';
|
|
29
|
+
import { runPrdExplore } from '../prd/explore.js';
|
|
29
30
|
export const DEFAULT_IDLE_MINUTES = 20;
|
|
30
31
|
const STALE_MINUTES = 20; // a running status older than this likely means the loop died
|
|
31
32
|
export function relativeTime(fromIso, now) {
|
|
@@ -103,8 +104,13 @@ export function loopStatus(targetDir, now = () => new Date()) {
|
|
|
103
104
|
const candidate = worker.candidateId ? ` candidate ${worker.candidateId} · ${worker.worktree ?? 'worktree unknown'} · ${worker.lifecycle ?? 'working'}` : '';
|
|
104
105
|
lines.push(` worker ${worker.story} "${worker.storyTitle}" (${worker.provider}${worker.model ? `/${worker.model}` : ''})${candidate} · ${worker.phase ?? 'working'}${quality}`);
|
|
105
106
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
107
|
+
const nowMs = now().getTime();
|
|
108
|
+
const ageMs = nowMs - Date.parse(st.updatedAt);
|
|
109
|
+
const providerProgressMs = Math.max(0, ...((st.supervision ?? [])
|
|
110
|
+
.filter(process => process.liveness === 'alive' && process.lastProgressAt)
|
|
111
|
+
.map(process => Date.parse(process.lastProgressAt))
|
|
112
|
+
.filter(Number.isFinite)));
|
|
113
|
+
if (st.state === 'running' && ageMs > STALE_MINUTES * 60_000 && nowMs - providerProgressMs > STALE_MINUTES * 60_000) {
|
|
108
114
|
lines.push(` ⚠ possibly stuck — no update in ${relativeTime(st.updatedAt, now())}`);
|
|
109
115
|
}
|
|
110
116
|
return lines.join('\n');
|
|
@@ -113,7 +119,368 @@ export function resolveIdleMs(flagMinutes, configMinutes) {
|
|
|
113
119
|
const minutes = flagMinutes ?? configMinutes ?? DEFAULT_IDLE_MINUTES;
|
|
114
120
|
return minutes > 0 ? minutes * 60_000 : 0;
|
|
115
121
|
}
|
|
122
|
+
const DEFAULT_EXPLORE_INTERVAL_MINUTES = 30;
|
|
123
|
+
const EXPLORE_RETRY_BASE_MS = 30_000;
|
|
124
|
+
const EXPLORE_RETRY_MAX_MS = 15 * 60_000;
|
|
125
|
+
function consumeExplorePause(targetDir) {
|
|
126
|
+
const path = pauseFilePath(targetDir);
|
|
127
|
+
if (!existsSync(path))
|
|
128
|
+
return false;
|
|
129
|
+
try {
|
|
130
|
+
unlinkSync(path);
|
|
131
|
+
}
|
|
132
|
+
catch { /* a pause request still wins if cleanup fails */ }
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
function currentProgress(targetDir) {
|
|
136
|
+
try {
|
|
137
|
+
return progress(loadPrd(prdPath(targetDir)));
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return { passed: 0, total: 0 };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function allCurrentStoriesPass(targetDir) {
|
|
144
|
+
try {
|
|
145
|
+
return allPass(loadPrd(prdPath(targetDir)));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function waitForExplorePause(targetDir, durationMs, heartbeat, limitDeadline) {
|
|
152
|
+
const waitDeadline = Date.now() + durationMs;
|
|
153
|
+
let nextHeartbeat = Date.now() + 5 * 60_000;
|
|
154
|
+
while (Date.now() < waitDeadline && (limitDeadline === undefined || Date.now() < limitDeadline)) {
|
|
155
|
+
if (consumeExplorePause(targetDir))
|
|
156
|
+
return true;
|
|
157
|
+
if (Date.now() >= nextHeartbeat) {
|
|
158
|
+
heartbeat?.();
|
|
159
|
+
nextHeartbeat = Date.now() + 5 * 60_000;
|
|
160
|
+
}
|
|
161
|
+
const untilWaitEnds = waitDeadline - Date.now();
|
|
162
|
+
const untilLimit = limitDeadline === undefined ? untilWaitEnds : limitDeadline - Date.now();
|
|
163
|
+
await new Promise(resolve => setTimeout(resolve, Math.max(1, Math.min(1_000, untilWaitEnds, untilLimit))));
|
|
164
|
+
}
|
|
165
|
+
return consumeExplorePause(targetDir);
|
|
166
|
+
}
|
|
167
|
+
function retryProviders(targetDir, preferred, available = isAgentAvailable) {
|
|
168
|
+
let config;
|
|
169
|
+
try {
|
|
170
|
+
config = loadConfig(targetDir);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
config = null;
|
|
174
|
+
}
|
|
175
|
+
const primary = resolveRunnerAgent(config, preferred, detectHostAgent());
|
|
176
|
+
return [...new Set([primary, ...(config?.agents ?? [])])].filter(agent => available(agent));
|
|
177
|
+
}
|
|
178
|
+
function retryReviewers(preferred, implementer, available) {
|
|
179
|
+
return [...new Set([...(preferred ? [preferred] : []), ...SUPPORTED_AGENTS])].filter(agent => agent !== implementer && available(agent));
|
|
180
|
+
}
|
|
181
|
+
function nextProvider(providers, current) {
|
|
182
|
+
if (!providers.length)
|
|
183
|
+
return current;
|
|
184
|
+
const index = current ? providers.indexOf(current) : -1;
|
|
185
|
+
return providers[(index + 1 + providers.length) % providers.length];
|
|
186
|
+
}
|
|
187
|
+
function retryDelay(failures) {
|
|
188
|
+
return Math.min(EXPLORE_RETRY_MAX_MS, EXPLORE_RETRY_BASE_MS * 2 ** Math.min(10, Math.max(0, failures - 1)));
|
|
189
|
+
}
|
|
190
|
+
function scheduleExploreLimitPause(targetDir, deadline) {
|
|
191
|
+
if (deadline === undefined)
|
|
192
|
+
return () => { };
|
|
193
|
+
let timer;
|
|
194
|
+
const check = () => {
|
|
195
|
+
const remaining = deadline - Date.now();
|
|
196
|
+
if (remaining <= 0) {
|
|
197
|
+
requestLoopPause(targetDir);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
timer = setTimeout(check, Math.min(remaining, 2_147_483_647));
|
|
201
|
+
timer.unref?.();
|
|
202
|
+
};
|
|
203
|
+
check();
|
|
204
|
+
return () => { if (timer)
|
|
205
|
+
clearTimeout(timer); };
|
|
206
|
+
}
|
|
207
|
+
function reportExploration(targetDir, summary, json, nextStep) {
|
|
208
|
+
const say = json ? (line) => console.error(line) : (line) => console.log(line);
|
|
209
|
+
let next = nextStep;
|
|
210
|
+
try {
|
|
211
|
+
if (!next) {
|
|
212
|
+
const story = selectNextStory(loadPrd(prdPath(targetDir)));
|
|
213
|
+
if (story)
|
|
214
|
+
next = `${story.id}: ${story.title}`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch { /* the persistent loop status keeps the parse failure visible */ }
|
|
218
|
+
say(`Exploration: ${summary}`);
|
|
219
|
+
say(`Next: ${next ?? 'scan the project again for evidenced improvements'}`);
|
|
220
|
+
}
|
|
221
|
+
async function runContinuousExploration(targetDir, options) {
|
|
222
|
+
if (options.isolate === false) {
|
|
223
|
+
console.error('--explore requires isolated story worktrees so failed work can be resumed safely.');
|
|
224
|
+
return 2;
|
|
225
|
+
}
|
|
226
|
+
const intervalMinutes = options.exploreIntervalMinutes ?? DEFAULT_EXPLORE_INTERVAL_MINUTES;
|
|
227
|
+
if (!Number.isInteger(intervalMinutes) || intervalMinutes < 1 || intervalMinutes > 1_440) {
|
|
228
|
+
console.error('--explore-interval must be an integer from 1 to 1440 minutes');
|
|
229
|
+
return 2;
|
|
230
|
+
}
|
|
231
|
+
if (options.exploreLimitMs !== undefined && (!Number.isSafeInteger(options.exploreLimitMs) || options.exploreLimitMs <= 0 || Date.now() + options.exploreLimitMs > 8_640_000_000_000_000)) {
|
|
232
|
+
console.error('--explore-limit must be a positive, supported duration');
|
|
233
|
+
return 2;
|
|
234
|
+
}
|
|
235
|
+
const limitDeadline = options.exploreLimitMs === undefined ? undefined : Date.now() + options.exploreLimitMs;
|
|
236
|
+
const reporter = options.reporter ?? makeReporter(targetDir, { json: options.json, quiet: true });
|
|
237
|
+
let config;
|
|
238
|
+
try {
|
|
239
|
+
config = loadConfig(targetDir);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
config = null;
|
|
243
|
+
}
|
|
244
|
+
const defaultImplementationAgent = options.agent ?? resolveRunnerAgent(config, undefined, detectHostAgent());
|
|
245
|
+
const defaultExplorer = options.agent ?? config?.planning?.agent ?? defaultImplementationAgent;
|
|
246
|
+
const available = options.isAvailable ?? isAgentAvailable;
|
|
247
|
+
const reviewRequested = !options.reviewRunner && Boolean(options.review || options.reviewer);
|
|
248
|
+
const innerOptions = { ...options, isolate: true, explore: true, explorationSupervisor: true, reporter, quiet: true };
|
|
249
|
+
let usedIterations = 0;
|
|
250
|
+
let retryCount = 0;
|
|
251
|
+
let explorationFailures = 0;
|
|
252
|
+
let noActionScans = 0;
|
|
253
|
+
let implementationAgent = defaultImplementationAgent;
|
|
254
|
+
let reviewerAgent = options.reviewer ?? (reviewRequested ? SUPPORTED_AGENTS.find(agent => agent !== defaultImplementationAgent && available(agent)) : undefined);
|
|
255
|
+
let explorationAgent = defaultExplorer;
|
|
256
|
+
let retryWorktree = false;
|
|
257
|
+
const safeBatchSize = () => {
|
|
258
|
+
if (retryWorktree)
|
|
259
|
+
return 1;
|
|
260
|
+
const parallelSetting = options.parallelAuto ? 'auto' : options.parallel ?? config?.loop.parallel ?? 'auto';
|
|
261
|
+
if (parallelSetting !== 'auto')
|
|
262
|
+
return Math.max(1, parallelSetting);
|
|
263
|
+
try {
|
|
264
|
+
const pending = loadPrd(prdPath(targetDir)).filter(story => !story.passes);
|
|
265
|
+
if (!config?.actions?.length && pending.length > 1 && pending.every(story => story.writes?.length)) {
|
|
266
|
+
return Math.min(sharedPoolStatus().limit, pending.length);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch { /* the inner command reports the underlying configuration or PRD error */ }
|
|
270
|
+
return 1;
|
|
271
|
+
};
|
|
272
|
+
const say = options.json ? (line) => console.error(line) : (line) => console.log(line);
|
|
273
|
+
reporter.phase('exploring', 'continuous exploration supervisor started', currentProgress(targetDir));
|
|
274
|
+
const limitReached = () => limitDeadline !== undefined && Date.now() >= limitDeadline;
|
|
275
|
+
const stopForTimeLimit = () => {
|
|
276
|
+
consumeExplorePause(targetDir);
|
|
277
|
+
const reason = 'the configured exploration time limit elapsed; stopped at a safe boundary';
|
|
278
|
+
reporter.paused(currentProgress(targetDir), reason);
|
|
279
|
+
say('Exploration time limit elapsed; the loop stopped at a safe boundary. Next: yoke loop run --explore .');
|
|
280
|
+
return 3;
|
|
281
|
+
};
|
|
282
|
+
const scanForWork = async (focus) => {
|
|
283
|
+
if (limitReached())
|
|
284
|
+
return 'limit';
|
|
285
|
+
const explorer = retryProviders(targetDir, explorationAgent, available);
|
|
286
|
+
const result = runPrdExplore(targetDir, {
|
|
287
|
+
runner: explorationAgent,
|
|
288
|
+
timeoutMinutes: options.timeoutMinutes,
|
|
289
|
+
isAvailable: options.isAvailable ?? isAgentAvailable,
|
|
290
|
+
onUsage: usage => reporter.addTokens(usage),
|
|
291
|
+
pause: () => consumeExplorePause(targetDir) || limitReached(),
|
|
292
|
+
...(focus ? { focus } : {}),
|
|
293
|
+
});
|
|
294
|
+
if (result.kind === 'paused') {
|
|
295
|
+
if (limitReached())
|
|
296
|
+
return 'limit';
|
|
297
|
+
reporter.paused(currentProgress(targetDir));
|
|
298
|
+
say('Exploration paused at a safe boundary.');
|
|
299
|
+
return 'paused';
|
|
300
|
+
}
|
|
301
|
+
if (result.kind === 'added') {
|
|
302
|
+
explorationFailures = 0;
|
|
303
|
+
noActionScans = 0;
|
|
304
|
+
explorationAgent = defaultExplorer;
|
|
305
|
+
const titles = result.tasks.map(task => `${task.id}: ${task.title}`).join('; ');
|
|
306
|
+
reporter.phase('exploring', `accepted tasks are committed; implementation is next: ${titles}`, currentProgress(targetDir));
|
|
307
|
+
reportExploration(targetDir, `added ${titles}`, options.json ?? false);
|
|
308
|
+
return 'continue';
|
|
309
|
+
}
|
|
310
|
+
if (result.kind === 'none') {
|
|
311
|
+
explorationFailures = 0;
|
|
312
|
+
noActionScans++;
|
|
313
|
+
if (noActionScans % 3 === 0)
|
|
314
|
+
explorationAgent = nextProvider(explorer, explorationAgent) ?? defaultExplorer;
|
|
315
|
+
const wait = intervalMinutes * 60_000;
|
|
316
|
+
const waitingReason = `${focus ? `${focus}; ` : ''}${result.summary}; scanning again in ${intervalMinutes} minute(s)`;
|
|
317
|
+
reporter.phase('waiting-exploration', waitingReason, currentProgress(targetDir));
|
|
318
|
+
reportExploration(targetDir, 'no new task met the evidence and quality gates', options.json ?? false, `scan again in ${intervalMinutes} minute(s)`);
|
|
319
|
+
return await waitForExplorePause(targetDir, wait, () => reporter.phase('waiting-exploration', waitingReason, currentProgress(targetDir)), limitDeadline) ? 'paused' : 'continue';
|
|
320
|
+
}
|
|
321
|
+
explorationFailures++;
|
|
322
|
+
explorationAgent = nextProvider(explorer, result.provider ?? explorationAgent) ?? defaultExplorer;
|
|
323
|
+
const wait = retryDelay(explorationFailures);
|
|
324
|
+
const waitingReason = `${focus ? `${focus}; ` : ''}${result.summary}; retrying exploration in ${Math.ceil(wait / 1_000)} seconds`;
|
|
325
|
+
reporter.phase('waiting-recovery', waitingReason, currentProgress(targetDir));
|
|
326
|
+
reportExploration(targetDir, 'the explorer could not produce an admissible task', options.json ?? false, `retry in ${Math.ceil(wait / 1_000)} seconds`);
|
|
327
|
+
return await waitForExplorePause(targetDir, wait, () => reporter.phase('waiting-recovery', waitingReason, currentProgress(targetDir)), limitDeadline) ? 'paused' : 'continue';
|
|
328
|
+
};
|
|
329
|
+
const advanceRecoveryProviders = () => {
|
|
330
|
+
implementationAgent = nextProvider(retryProviders(targetDir, implementationAgent, available), implementationAgent) ?? implementationAgent;
|
|
331
|
+
if (reviewRequested)
|
|
332
|
+
reviewerAgent = nextProvider(retryReviewers(reviewerAgent, implementationAgent, available), reviewerAgent) ?? reviewerAgent;
|
|
333
|
+
};
|
|
334
|
+
for (;;) {
|
|
335
|
+
if (consumeExplorePause(targetDir)) {
|
|
336
|
+
reporter.paused(currentProgress(targetDir));
|
|
337
|
+
say(`Exploration paused. Next: yoke loop run --explore .`);
|
|
338
|
+
return 3;
|
|
339
|
+
}
|
|
340
|
+
if (limitReached())
|
|
341
|
+
return stopForTimeLimit();
|
|
342
|
+
const beforeStatus = readStatus(targetDir);
|
|
343
|
+
const priorProgress = currentProgress(targetDir);
|
|
344
|
+
const remaining = options.maxIterations === undefined ? undefined : Math.max(0, options.maxIterations - usedIterations);
|
|
345
|
+
if (remaining === 0) {
|
|
346
|
+
reporter.capReached(currentProgress(targetDir));
|
|
347
|
+
say(`Exploration reached --max=${options.maxIterations}. Next: yoke loop run --explore .`);
|
|
348
|
+
return 1;
|
|
349
|
+
}
|
|
350
|
+
let resultCode;
|
|
351
|
+
let unexpectedFailure;
|
|
352
|
+
const batchSize = limitDeadline === undefined ? undefined : safeBatchSize();
|
|
353
|
+
const batchLimit = batchSize === undefined ? remaining : Math.min(remaining ?? batchSize, batchSize);
|
|
354
|
+
const cancelTimePause = scheduleExploreLimitPause(targetDir, limitDeadline);
|
|
355
|
+
try {
|
|
356
|
+
resultCode = await Promise.resolve(runLoopCommand(targetDir, {
|
|
357
|
+
...innerOptions,
|
|
358
|
+
agent: implementationAgent,
|
|
359
|
+
...(reviewRequested && reviewerAgent ? { reviewer: reviewerAgent } : {}),
|
|
360
|
+
...(batchLimit !== undefined ? { maxIterations: batchLimit } : {}),
|
|
361
|
+
...(retryWorktree ? { resumeWorktree: true, parallel: 1, candidates: 1 } : {}),
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
resultCode = 1;
|
|
366
|
+
unexpectedFailure = error instanceof Error ? error.message : String(error);
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
cancelTimePause();
|
|
370
|
+
}
|
|
371
|
+
const afterStatus = readStatus(targetDir);
|
|
372
|
+
const changedStatus = afterStatus?.updatedAt !== beforeStatus?.updatedAt;
|
|
373
|
+
if (changedStatus && priorProgress.passed < priorProgress.total)
|
|
374
|
+
usedIterations += Math.max(0, afterStatus?.iteration ?? 0);
|
|
375
|
+
if (unexpectedFailure) {
|
|
376
|
+
retryCount++;
|
|
377
|
+
advanceRecoveryProviders();
|
|
378
|
+
const wait = retryDelay(retryCount);
|
|
379
|
+
const reason = `the loop stopped unexpectedly: ${unexpectedFailure}; retrying with ${implementationAgent} in ${Math.ceil(wait / 1_000)} seconds`;
|
|
380
|
+
reporter.phase('waiting-recovery', reason, currentProgress(targetDir));
|
|
381
|
+
say(`The loop stopped unexpectedly. Next: retry with ${implementationAgent} in ${Math.ceil(wait / 1_000)} seconds or pause with yoke loop pause .`);
|
|
382
|
+
if (await waitForExplorePause(targetDir, wait, () => reporter.phase('waiting-recovery', reason, currentProgress(targetDir)), limitDeadline)) {
|
|
383
|
+
reporter.paused(currentProgress(targetDir));
|
|
384
|
+
return 3;
|
|
385
|
+
}
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (resultCode === 3) {
|
|
389
|
+
if (limitReached())
|
|
390
|
+
return stopForTimeLimit();
|
|
391
|
+
say('Exploration paused. Next: yoke loop run --explore .');
|
|
392
|
+
return 3;
|
|
393
|
+
}
|
|
394
|
+
if (limitReached())
|
|
395
|
+
return stopForTimeLimit();
|
|
396
|
+
if (options.maxIterations !== undefined && usedIterations >= options.maxIterations) {
|
|
397
|
+
if (resultCode === 0) {
|
|
398
|
+
reporter.complete(currentProgress(targetDir));
|
|
399
|
+
say(`Current backlog complete; --max=${options.maxIterations} stopped further exploration. Next: yoke loop run --explore .`);
|
|
400
|
+
return 0;
|
|
401
|
+
}
|
|
402
|
+
reporter.capReached(currentProgress(targetDir));
|
|
403
|
+
say(`Exploration reached --max=${options.maxIterations} with work remaining. Next: yoke loop run --explore .`);
|
|
404
|
+
return 1;
|
|
405
|
+
}
|
|
406
|
+
if (resultCode === 0) {
|
|
407
|
+
retryCount = 0;
|
|
408
|
+
retryWorktree = false;
|
|
409
|
+
const availableProviders = retryProviders(targetDir, defaultImplementationAgent, options.isAvailable ?? isAgentAvailable);
|
|
410
|
+
implementationAgent = availableProviders.includes(defaultImplementationAgent) ? defaultImplementationAgent : availableProviders[0] ?? defaultImplementationAgent;
|
|
411
|
+
reporter.phase('exploring', 'the current accepted backlog is complete; scanning for the next verified improvement', currentProgress(targetDir));
|
|
412
|
+
const scan = await scanForWork();
|
|
413
|
+
if (scan === 'paused')
|
|
414
|
+
return 3;
|
|
415
|
+
if (scan === 'limit')
|
|
416
|
+
return stopForTimeLimit();
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (afterStatus?.state === 'cap-reached') {
|
|
420
|
+
if (limitDeadline !== undefined) {
|
|
421
|
+
retryCount = 0;
|
|
422
|
+
retryWorktree = false;
|
|
423
|
+
reporter.phase('exploring', 'the current safe task batch is complete; continuing the accepted backlog', currentProgress(targetDir));
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
say(`The current backlog is incomplete at the requested iteration cap. Next: yoke loop run --explore .`);
|
|
427
|
+
return 1;
|
|
428
|
+
}
|
|
429
|
+
let pendingDecision = false;
|
|
430
|
+
try {
|
|
431
|
+
pendingDecision = Boolean(readPendingDecision(targetDir));
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
pendingDecision = true;
|
|
435
|
+
}
|
|
436
|
+
if (pendingDecision || afterStatus?.reason?.includes('ambiguous acceptance criteria')) {
|
|
437
|
+
retryWorktree = false;
|
|
438
|
+
const wait = Math.min(intervalMinutes * 60_000, 60_000);
|
|
439
|
+
reporter.phase('waiting-recovery', `story ${afterStatus?.story ?? 'unknown'} needs a human decision before it can continue`, currentProgress(targetDir));
|
|
440
|
+
say(`Story ${afterStatus?.story ?? 'unknown'} is waiting for a human decision. Next: answer it or run yoke loop pause .`);
|
|
441
|
+
if (await waitForExplorePause(targetDir, wait, undefined, limitDeadline)) {
|
|
442
|
+
reporter.paused(currentProgress(targetDir));
|
|
443
|
+
return 3;
|
|
444
|
+
}
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (afterStatus?.reason?.startsWith('integrated completion gate failed') && allCurrentStoriesPass(targetDir)) {
|
|
448
|
+
retryWorktree = false;
|
|
449
|
+
reporter.phase('exploring', `all planned tasks pass, but ${afterStatus.reason}; looking for work that can resolve the completion gate`, currentProgress(targetDir));
|
|
450
|
+
const scan = await scanForWork(`The integrated completion gate is still failing: ${afterStatus.reason}`);
|
|
451
|
+
if (scan === 'paused')
|
|
452
|
+
return 3;
|
|
453
|
+
if (scan === 'limit')
|
|
454
|
+
return stopForTimeLimit();
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (resultCode === 2) {
|
|
458
|
+
retryCount++;
|
|
459
|
+
advanceRecoveryProviders();
|
|
460
|
+
const wait = retryDelay(retryCount);
|
|
461
|
+
reporter.phase('waiting-recovery', `the loop could not start; retrying with ${implementationAgent}${reviewerAgent ? ` and reviewer ${reviewerAgent}` : ''} in ${Math.ceil(wait / 1_000)} seconds`, currentProgress(targetDir));
|
|
462
|
+
say(`The unfinished loop could not start. Next: retry with ${implementationAgent}${reviewerAgent ? ` and reviewer ${reviewerAgent}` : ''} in ${Math.ceil(wait / 1_000)} seconds or pause with yoke loop pause .`);
|
|
463
|
+
if (await waitForExplorePause(targetDir, wait, undefined, limitDeadline)) {
|
|
464
|
+
reporter.paused(currentProgress(targetDir));
|
|
465
|
+
return 3;
|
|
466
|
+
}
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
retryCount++;
|
|
470
|
+
advanceRecoveryProviders();
|
|
471
|
+
retryWorktree = true;
|
|
472
|
+
const wait = retryDelay(retryCount);
|
|
473
|
+
reporter.phase('waiting-recovery', `story ${afterStatus?.story ?? 'unknown'} remains incomplete: ${afterStatus?.reason ?? 'no passing completion status'}; retrying with ${implementationAgent ?? 'the configured runner'} in ${Math.ceil(wait / 1_000)} seconds`, currentProgress(targetDir));
|
|
474
|
+
say(`The current task remains unfinished; its isolated work is retained. Next: retry with ${implementationAgent ?? 'the configured runner'} in ${Math.ceil(wait / 1_000)} seconds.`);
|
|
475
|
+
if (await waitForExplorePause(targetDir, wait, undefined, limitDeadline)) {
|
|
476
|
+
reporter.paused(currentProgress(targetDir));
|
|
477
|
+
return 3;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
116
481
|
export function runLoopCommand(targetDir, opts) {
|
|
482
|
+
if (opts.explore && !opts.explorationSupervisor)
|
|
483
|
+
return runContinuousExploration(targetDir, opts);
|
|
117
484
|
let parallel = opts.parallel ?? 1;
|
|
118
485
|
const candidates = opts.candidates ?? 1;
|
|
119
486
|
if (opts.resumeWorktree && (opts.isolate === false || parallel !== 1 || candidates !== 1)) {
|
|
@@ -257,7 +624,8 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
257
624
|
}
|
|
258
625
|
if (commitIdentity) {
|
|
259
626
|
const announce = opts.json ? console.error : console.log;
|
|
260
|
-
|
|
627
|
+
if (!opts.quiet)
|
|
628
|
+
announce(`Commits: ${commitIdentity.authorName} <${commitIdentity.authorEmail}> · co-authors: ${commitIdentity.allowCoAuthors ? 'allowed' : 'disabled'}`);
|
|
261
629
|
}
|
|
262
630
|
const idleMs = resolveIdleMs(opts.timeoutMinutes, config.loop.timeoutMinutes);
|
|
263
631
|
const qualityOverrides = {
|
|
@@ -268,7 +636,8 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
268
636
|
...(opts.candidates !== undefined ? { candidates: opts.candidates } : {}),
|
|
269
637
|
};
|
|
270
638
|
if (qualityOverrides.qualityUnbounded) {
|
|
271
|
-
|
|
639
|
+
if (!opts.quiet)
|
|
640
|
+
console.error('WARNING: Quality repair limits are unbounded for this invocation. Mechanical gates, watchdog, isolation, and commit safety remain active.');
|
|
272
641
|
}
|
|
273
642
|
const configuredCriticAgent = config.quality?.critic?.agent ?? config.quality?.criticAgent ?? config.agents.find(agent => agent !== runnerAgent) ?? runnerAgent;
|
|
274
643
|
const configuredCriticModel = config.quality?.critic?.model ?? config.quality?.criticModel ?? (configuredCriticAgent === runnerAgent ? config.runner?.model : undefined);
|
|
@@ -292,7 +661,8 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
292
661
|
const repairAgent = config.quality?.repair?.agent ?? config.quality?.repairAgent ?? runnerAgent;
|
|
293
662
|
const limit = resolved.limits.unbounded ? 'unbounded' : `${resolved.limits.maxRounds ?? 3} rounds/${resolved.limits.maxMinutes ?? 60} minutes`;
|
|
294
663
|
const announce = opts.json ? console.error : console.log;
|
|
295
|
-
|
|
664
|
+
if (!opts.quiet)
|
|
665
|
+
announce(`Quality: ${resolved.policy} · critic: ${criticAgent}${configuredCriticModel ? `/${configuredCriticModel}` : '/provider-default'} · repair: ${repairAgent}${config.quality?.repair?.model ?? config.quality?.repairModel ? `/${config.quality?.repair?.model ?? config.quality?.repairModel}` : '/provider-default'} · permissions: read-only critic/safe repair · budget: ${limit}`);
|
|
296
666
|
}
|
|
297
667
|
const permissions = opts.permissions ?? config.runner?.permissions ?? 'safe';
|
|
298
668
|
const routingRequested = opts.routing ?? config.routing?.enabled ?? true;
|
|
@@ -417,7 +787,8 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
417
787
|
})
|
|
418
788
|
: makeRunner(runnerAgent, idleMs, runnerOpts);
|
|
419
789
|
const announce = opts.json ? console.error : console.log;
|
|
420
|
-
|
|
790
|
+
if (!opts.quiet)
|
|
791
|
+
announce(`Runner: ${runnerAgent} · permissions: ${permissions} · routing: ${routingEnabled ? 'on' : routingRequested ? 'auto (parent; no worker profiles)' : 'off'} · workers: ${parallel} local / ${sharedLimit} shared maximum · isolation: ${isolate || parallel > 1 ? 'on' : 'off'} · cwd: ${targetDir}`);
|
|
421
792
|
}
|
|
422
793
|
if (config.actions?.length) {
|
|
423
794
|
if (parallel > 1 || candidates > 1) {
|
|
@@ -496,7 +867,7 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
496
867
|
if (lock.stalePid !== undefined) {
|
|
497
868
|
console.warn(`Took over a stale loop lock (pid ${lock.stalePid} is gone).`);
|
|
498
869
|
}
|
|
499
|
-
const reporter = opts.reporter ?? makeReporter(targetDir, { json: opts.json });
|
|
870
|
+
const reporter = opts.reporter ?? makeReporter(targetDir, { json: opts.json, quiet: opts.quiet });
|
|
500
871
|
executionReporter = reporter;
|
|
501
872
|
const buildResume = (storyId, requestId) => buildTrustedDecisionResumeState({
|
|
502
873
|
storyId,
|
|
@@ -597,11 +968,13 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
597
968
|
return resumeCode;
|
|
598
969
|
// In json mode stdout belongs to the NDJSON stream — route the narrative summary to stderr.
|
|
599
970
|
const say = opts.json ? (line) => console.error(line) : (line) => console.log(line);
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
971
|
+
if (!opts.quiet) {
|
|
972
|
+
say(`Loop ${result.status} after ${result.iterations} iteration(s): ${result.finalProgress.passed}/${result.finalProgress.total} stories pass`);
|
|
973
|
+
if (result.reason)
|
|
974
|
+
say(`Reason: ${result.reason}`);
|
|
975
|
+
if (result.reason && /api key|please run \/login|not logged in|auth/i.test(result.reason)) {
|
|
976
|
+
say('Hint: the agent CLI has no credentials in this environment. Set ANTHROPIC_API_KEY, GEMINI_API_KEY, or OPENAI_API_KEY, or log the agent in for headless use.');
|
|
977
|
+
}
|
|
605
978
|
}
|
|
606
979
|
// Exit codes: 0 complete · 1 blocked/cap-reached · 2 config error (handled above) · 3 paused (loop.pause consumed at a story boundary)
|
|
607
980
|
if (result.status === 'complete')
|