@hecer/yoke 1.18.0 → 1.20.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 +46 -0
- package/README.md +86 -910
- package/bench/RESULTS.md +30 -0
- package/bench/analyze-codex-comparison.mjs +22 -0
- package/bench/compare-codex.mjs +71 -0
- package/bench/fixtures/independent-utils/.yoke/prd.yaml +30 -0
- package/bench/fixtures/independent-utils/bench-verify.mjs +4 -0
- package/bench/fixtures/independent-utils/package.json +1 -0
- package/bench/fixtures/independent-utils/src/chunk.mjs +1 -0
- package/bench/fixtures/independent-utils/src/range.mjs +1 -0
- package/bench/fixtures/independent-utils/src/unique.mjs +1 -0
- package/bench/fixtures/independent-utils/tests/STORY-1.test.mjs +8 -0
- package/bench/fixtures/independent-utils/tests/STORY-2.test.mjs +8 -0
- package/bench/fixtures/independent-utils/tests/STORY-3.test.mjs +8 -0
- package/bench/probe-goal-integration.mjs +93 -0
- package/bench/probe-parallel-gate-context.mjs +29 -0
- package/bench/results/codex-comparison-2026-09-29.json +305 -0
- package/bench/results/goal-integration-probes-2026-09-29.json +107 -0
- package/bench/results/native-goal-validation-2026-09-30.json +122 -0
- package/bench/results/parallel-validation-2026-09-30.json +86 -0
- package/canon/manifest.yaml +1 -1
- package/dist/agents/process-record-identity.js +33 -0
- 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/check/command.js +218 -0
- package/dist/cli.js +11 -7
- 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 +88 -7
- package/dist/goals/codex-native.js +340 -0
- package/dist/goals/command.js +230 -29
- package/dist/loop/cleanup.js +10 -1
- package/dist/loop/dispatcher.js +82 -10
- package/dist/loop/git.js +1 -1
- package/dist/loop/parallel-adapters.js +53 -7
- package/dist/loop/parallel-command.js +8 -1
- package/dist/loop/recovery.js +81 -2
- package/dist/loop/reporter.js +3 -0
- package/dist/loop/resource-pool.js +21 -15
- package/dist/loop/run-command.js +91 -25
- package/dist/loop/run-state.js +116 -0
- package/dist/prd/explore.js +12 -4
- package/dist/retrofit/config.js +29 -0
- package/dist/retrofit/gitignore.js +3 -0
- package/docs/CODEX-COMPARISON-2026-09-29.md +27 -0
- package/docs/CONTINUOUS-EXPLORATION.md +6 -2
- package/docs/GOALS-RESOURCE-AUDIT-2026-09-29.md +92 -0
- package/docs/GOALS.md +109 -0
- package/docs/HARNESSES.md +1 -1
- package/docs/RELEASE-VALIDATION-1.20.0.md +100 -0
- package/docs/SOL-PI.md +41 -0
- package/docs/VERIFIED-PROJECTS.md +4 -4
- package/docs/code-intelligence-stdio-fix-2026-09-27.md +8 -0
- package/docs/parallel-execution.md +15 -0
- package/docs/superpowers/plans/2026-09-30-goals-resources-release.md +78 -0
- package/gemini-extension.json +1 -1
- package/package.json +5 -1
package/dist/goals/command.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { loadConfig } from "../retrofit/config.js";
|
|
2
2
|
import { makeAsyncAdaptiveRunner } from "../routing/router.js";
|
|
3
3
|
import { resolvePlanner } from '../routing/planning.js';
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
-
import { acceptanceProtectionProblem,
|
|
8
|
+
import { acceptanceProtectionProblem, checkProjectAsync, loadAcceptance, protectAcceptance } from '../check/command.js';
|
|
9
9
|
import { acquireLock, releaseLock, isPidAlive } from '../loop/lock.js';
|
|
10
10
|
import { reapProviderProcesses, isProviderTreeAlive } from '../loop/cleanup.js';
|
|
11
11
|
import { killProcessTreeForCleanup } from '../loop/watchdog.js';
|
|
@@ -13,8 +13,43 @@ import { buildProviderInvocation, startProviderProcess } from '../agents/provide
|
|
|
13
13
|
import { AgentSchema } from '../agents/contracts.js';
|
|
14
14
|
import { appendEvent } from '../observability/events.js';
|
|
15
15
|
import { statePath } from '../workspace/state.js';
|
|
16
|
+
import { acquireSharedWorker } from '../loop/resource-pool.js';
|
|
17
|
+
import { recordGoalRun } from '../loop/run-state.js';
|
|
18
|
+
import { executeCodexGoal, synchronizeNativeGoal, NativeGoalUnavailableError } from './codex-native.js';
|
|
16
19
|
const Attempt = z.object({ provider: AgentSchema, model: z.string().optional(), startedAt: z.string(), durationMs: z.number().nonnegative(), success: z.boolean(), summary: z.string(), checkId: z.string(), inputTokens: z.number().nonnegative().optional(), outputTokens: z.number().nonnegative().optional() });
|
|
17
|
-
const Goal = z.object({
|
|
20
|
+
const Goal = z.object({
|
|
21
|
+
version: z.literal(1), id: z.string().uuid(), objective: z.string().trim().min(1).max(16000),
|
|
22
|
+
status: z.enum(['active', 'running', 'paused', 'blocked', 'complete']), createdAt: z.string(), updatedAt: z.string(),
|
|
23
|
+
maxAttempts: z.number().int().min(1).max(20), maxMinutes: z.number().positive().max(1440),
|
|
24
|
+
maxWallMinutes: z.number().positive().max(10080).optional(), wallDurationMs: z.number().nonnegative().default(0),
|
|
25
|
+
tokenBudget: z.number().int().positive().optional(), attempts: z.array(Attempt), reason: z.string().optional(), lastCheck: z.string().optional(),
|
|
26
|
+
acceptanceBinding: z.object({ criterionIds: z.array(z.string()).min(1), manifestDigest: z.string().regex(/^[a-f0-9]{64}$/), objectiveDigest: z.string().regex(/^[a-f0-9]{64}$/) }).strict().optional(),
|
|
27
|
+
nativeBinding: z.object({ provider: z.literal('codex'), threadId: z.string().min(1), objectiveRevision: z.string().regex(/^[a-f0-9]{64}$/), modelProvider: z.string().optional(), model: z.string().optional(), usageInvalid: z.literal(true).optional(), usageBaseline: z.object({ inputTokens: z.number().int().nonnegative().safe(), outputTokens: z.number().int().nonnegative().safe(), cachedInputTokens: z.number().int().nonnegative().safe().optional(), cacheWriteInputTokens: z.number().int().nonnegative().safe().optional(), reasoningOutputTokens: z.number().int().nonnegative().safe().optional() }).strict().optional() }).strict().optional(),
|
|
28
|
+
nativeTokensUsed: z.number().nonnegative().optional(),
|
|
29
|
+
pendingRunStartedAt: z.string().datetime().optional(),
|
|
30
|
+
pendingAttempt: z.object({ provider: AgentSchema, model: z.string().optional(), startedAt: z.string().datetime() }).optional(),
|
|
31
|
+
}).strict();
|
|
32
|
+
const digest = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
33
|
+
function acceptanceBinding(root, objective, ids) {
|
|
34
|
+
const manifest = loadAcceptance(root);
|
|
35
|
+
if (!manifest || !ids.length || new Set(ids).size !== ids.length || ids.some(id => !manifest.criteria.some(c => c.id === id && c.commands.length)))
|
|
36
|
+
throw new Error('Bind a nonempty unique list of executable acceptance criteria');
|
|
37
|
+
return { criterionIds: ids, manifestDigest: digest(manifest), objectiveDigest: digest(objective) };
|
|
38
|
+
}
|
|
39
|
+
export function bindProjectGoal(root, ids) {
|
|
40
|
+
const lock = acquireLock(root);
|
|
41
|
+
if (!lock.acquired)
|
|
42
|
+
throw new Error('Project is busy');
|
|
43
|
+
try {
|
|
44
|
+
const goal = readProjectGoal(root);
|
|
45
|
+
if (!goal)
|
|
46
|
+
throw new Error('No project goal');
|
|
47
|
+
return save(root, { ...goal, acceptanceBinding: acceptanceBinding(root, goal.objective, ids) });
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
releaseLock(root, lock.ownerToken);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
18
53
|
const goalPath = (root) => statePath(root, 'goal.json');
|
|
19
54
|
function save(root, goal) {
|
|
20
55
|
const parsed = Goal.parse({ ...goal, updatedAt: new Date().toISOString() });
|
|
@@ -50,7 +85,7 @@ export function createProjectGoal(root, objective, limits = {}) {
|
|
|
50
85
|
const pause = statePath(root, 'goal.pause');
|
|
51
86
|
if (existsSync(pause))
|
|
52
87
|
unlinkSync(pause);
|
|
53
|
-
return save(root, Goal.parse({ version: 1, id: randomUUID(), objective, status: 'active', createdAt: now, updatedAt: now, maxAttempts: limits.maxAttempts ?? 3, maxMinutes: limits.maxMinutes ?? 30, tokenBudget: limits.tokenBudget, attempts: [] }));
|
|
88
|
+
return save(root, Goal.parse({ version: 1, id: randomUUID(), objective, status: 'active', createdAt: now, updatedAt: now, maxAttempts: limits.maxAttempts ?? 3, maxMinutes: limits.maxMinutes ?? 30, maxWallMinutes: limits.maxWallMinutes, tokenBudget: limits.tokenBudget, attempts: [], acceptanceBinding: limits.acceptanceIds ? acceptanceBinding(root, objective.trim(), limits.acceptanceIds) : undefined }));
|
|
54
89
|
}
|
|
55
90
|
finally {
|
|
56
91
|
releaseLock(root, lock.ownerToken);
|
|
@@ -77,7 +112,7 @@ export function budgetProjectGoal(root, limits) {
|
|
|
77
112
|
const goal = readProjectGoal(root);
|
|
78
113
|
if (!goal)
|
|
79
114
|
throw new Error('No project goal');
|
|
80
|
-
return save(root, { ...goal, maxAttempts: limits.maxAttempts ?? goal.maxAttempts, maxMinutes: limits.maxMinutes ?? goal.maxMinutes, tokenBudget: limits.clearTokenBudget ? undefined : limits.tokenBudget ?? goal.tokenBudget });
|
|
115
|
+
return save(root, { ...goal, maxAttempts: limits.maxAttempts ?? goal.maxAttempts, maxMinutes: limits.maxMinutes ?? goal.maxMinutes, maxWallMinutes: limits.maxWallMinutes ?? goal.maxWallMinutes, tokenBudget: limits.clearTokenBudget ? undefined : limits.tokenBudget ?? goal.tokenBudget });
|
|
81
116
|
}
|
|
82
117
|
finally {
|
|
83
118
|
releaseLock(root, lock.ownerToken);
|
|
@@ -101,6 +136,8 @@ export function goalHandoff(root) {
|
|
|
101
136
|
async function executeAgent(input) {
|
|
102
137
|
const handle = startProviderProcess(input.provider, buildProviderInvocation(input.provider, input.prompt, input.root, 'safe', input.selection), { signal: input.signal, idleTimeoutMs: 20 * 60_000 });
|
|
103
138
|
const result = await handle.completion;
|
|
139
|
+
if (handle.recordPath && existsSync(handle.recordPath))
|
|
140
|
+
throw Object.assign(new Error('Provider process cleanup could not be confirmed'), { cleanupUnconfirmed: true });
|
|
104
141
|
return { success: result.kind === 'succeeded', summary: result.kind === 'succeeded' ? 'Agent finished; independently checked below' : `${result.kind}: ${result.stderr.slice(-3000)}`, ...result.telemetry.tokens, tokens: result.telemetry.tokens };
|
|
105
142
|
}
|
|
106
143
|
export async function runProjectGoal(root, options = {}) {
|
|
@@ -108,10 +145,62 @@ export async function runProjectGoal(root, options = {}) {
|
|
|
108
145
|
if (!lock.acquired)
|
|
109
146
|
throw new Error('Project is busy; goal and story loops share one lock');
|
|
110
147
|
let goal = null;
|
|
148
|
+
let runStarted;
|
|
149
|
+
let wallAtStart = 0;
|
|
150
|
+
let wallTimer;
|
|
151
|
+
let pauseTimer;
|
|
152
|
+
let cleanupUnconfirmed = false;
|
|
153
|
+
let resolvedSelection;
|
|
154
|
+
let nativeEnabled = false;
|
|
155
|
+
const controller = new AbortController();
|
|
156
|
+
const tokensUsed = () => goal.attempts.reduce((sum, a) => sum + (a.inputTokens ?? 0) + (a.outputTokens ?? 0), 0);
|
|
157
|
+
const finish = async (status, reason, report) => {
|
|
158
|
+
if (status === 'paused' && existsSync(statePath(root, 'goal.pause')))
|
|
159
|
+
unlinkSync(statePath(root, 'goal.pause'));
|
|
160
|
+
if (goal.nativeBinding && nativeEnabled) {
|
|
161
|
+
const nativeStatus = status === 'complete' ? 'complete' : status === 'paused' ? 'paused' : /token/i.test(reason) ? 'budgetLimited' : 'blocked';
|
|
162
|
+
try {
|
|
163
|
+
const lease = await acquireSharedWorker({ targetDir: root, storyId: goal.id, provider: 'codex', role: 'integration', signal: AbortSignal.timeout(30_000) });
|
|
164
|
+
let retain = false;
|
|
165
|
+
try {
|
|
166
|
+
await synchronizeNativeGoal({ root, binding: goal.nativeBinding, objective: goal.objective, status: nativeStatus, bare: resolvedSelection?.bare, signal: status === 'complete' ? AbortSignal.any([controller.signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000) });
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const cleanup = error;
|
|
170
|
+
retain = cleanup.cleanupUnconfirmed === true;
|
|
171
|
+
if (retain)
|
|
172
|
+
void cleanup.cleanupConfirmed?.then(() => lease.release()).catch(() => undefined);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
if (!retain)
|
|
177
|
+
await lease.release();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
status = 'blocked';
|
|
182
|
+
reason += `; native state synchronization failed: ${error.message}`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (status === 'complete' && controller.signal.aborted) {
|
|
186
|
+
status = existsSync(statePath(root, 'goal.pause')) ? 'paused' : 'blocked';
|
|
187
|
+
reason = `${String(controller.signal.reason)}; verified work retained`;
|
|
188
|
+
}
|
|
189
|
+
if (status === 'paused' && existsSync(statePath(root, 'goal.pause')))
|
|
190
|
+
unlinkSync(statePath(root, 'goal.pause'));
|
|
191
|
+
goal = save(root, { ...goal, status, reason, lastCheck: report?.id ?? goal.lastCheck });
|
|
192
|
+
return goal;
|
|
193
|
+
};
|
|
111
194
|
try {
|
|
112
195
|
goal = readProjectGoal(root);
|
|
113
196
|
if (!goal)
|
|
114
197
|
throw new Error('Set a goal first');
|
|
198
|
+
if (options.expectedGoalId && goal.id !== options.expectedGoalId) {
|
|
199
|
+
goal = null;
|
|
200
|
+
throw new Error('Saved goal identity changed before resume');
|
|
201
|
+
}
|
|
202
|
+
if (goal.pendingRunStartedAt)
|
|
203
|
+
goal = save(root, { ...goal, wallDurationMs: goal.wallDurationMs + Math.max(0, Date.now() - Date.parse(goal.pendingRunStartedAt)), pendingRunStartedAt: undefined });
|
|
115
204
|
// Exclusive project lock held. Reconcile only this project's recorded trees;
|
|
116
205
|
// unknown ownership stays blocked rather than racing an orphaned worker.
|
|
117
206
|
reapProviderProcesses(root, isPidAlive, isProviderTreeAlive, killProcessTreeForCleanup);
|
|
@@ -126,6 +215,10 @@ export async function runProjectGoal(root, options = {}) {
|
|
|
126
215
|
const manifest = loadAcceptance(root);
|
|
127
216
|
if (!manifest?.criteria.length || manifest.criteria.some(c => c.commands.length === 0) || !manifest.protected.length)
|
|
128
217
|
return save(root, { ...goal, status: 'blocked', reason: 'Map every acceptance criterion to an executable command and explicitly protect its test infrastructure before running a goal' });
|
|
218
|
+
if (!goal.acceptanceBinding)
|
|
219
|
+
return save(root, { ...goal, status: 'blocked', reason: 'Bind this objective explicitly with goal bind --criteria=id1,id2 before running it' });
|
|
220
|
+
if (goal.acceptanceBinding.manifestDigest !== digest(manifest) || goal.acceptanceBinding.objectiveDigest !== digest(goal.objective))
|
|
221
|
+
return save(root, { ...goal, status: 'blocked', reason: 'Acceptance binding changed; review the objective and acceptance criteria before rebinding' });
|
|
129
222
|
try {
|
|
130
223
|
protectAcceptance(root);
|
|
131
224
|
}
|
|
@@ -133,85 +226,193 @@ export async function runProjectGoal(root, options = {}) {
|
|
|
133
226
|
if (error.code !== 'EEXIST')
|
|
134
227
|
throw error;
|
|
135
228
|
}
|
|
136
|
-
const provider = options.provider ?? 'codex';
|
|
137
|
-
const rawExecute = options.execute ?? executeAgent;
|
|
138
229
|
const config = loadConfig(root);
|
|
230
|
+
const provider = options.provider ?? config?.runner?.agent ?? 'codex';
|
|
231
|
+
const configured = config?.runner;
|
|
232
|
+
const selection = { provider: configured?.provider, model: configured?.model, reasoningEffort: configured?.reasoningEffort, variant: configured?.variant, bare: configured?.bare, ...Object.fromEntries(Object.entries(options.selection ?? {}).filter(([, value]) => value !== undefined)), nativeMultiAgent: false };
|
|
233
|
+
resolvedSelection = selection;
|
|
234
|
+
const native = options.native ?? config?.goals?.nativeCodex ?? false;
|
|
235
|
+
nativeEnabled = native;
|
|
236
|
+
if (native && provider === 'codex' && selection.bare) {
|
|
237
|
+
goal = save(root, { ...goal, status: 'blocked', reason: 'Native Codex app-server does not support bare startup; use --no-native-goal to retain bare execution or explicitly disable runner.bare' });
|
|
238
|
+
return goal;
|
|
239
|
+
}
|
|
240
|
+
recordGoalRun(root, goal.id, lock.ownerToken, { provider, selection, native });
|
|
241
|
+
wallAtStart = goal.wallDurationMs;
|
|
242
|
+
runStarted = Date.now();
|
|
243
|
+
if (goal.maxWallMinutes !== undefined) {
|
|
244
|
+
const remainingWall = goal.maxWallMinutes * 60_000 - wallAtStart;
|
|
245
|
+
if (remainingWall <= 0)
|
|
246
|
+
return await finish('blocked', 'Wall-time budget exhausted; unfinished work retained');
|
|
247
|
+
wallTimer = setTimeout(() => controller.abort('Wall-time budget exhausted'), remainingWall);
|
|
248
|
+
}
|
|
249
|
+
goal = save(root, { ...goal, pendingRunStartedAt: new Date(runStarted).toISOString() });
|
|
250
|
+
pauseTimer = setInterval(() => { if (existsSync(statePath(root, 'goal.pause')))
|
|
251
|
+
controller.abort('Pause requested'); }, 100);
|
|
252
|
+
let attemptAgentMs = 0;
|
|
253
|
+
const admitted = async (input, operation) => {
|
|
254
|
+
const queued = Date.now();
|
|
255
|
+
const lease = await acquireSharedWorker({ targetDir: root, storyId: goal.id, provider: input.provider, role: 'implementation', signal: input.signal });
|
|
256
|
+
const started = Date.now();
|
|
257
|
+
const remaining = goal.maxMinutes * 60_000 - goal.attempts.reduce((sum, a) => sum + a.durationMs, 0) - attemptAgentMs;
|
|
258
|
+
const timer = setTimeout(() => controller.abort('Goal agent-time budget exceeded'), Math.max(0, remaining));
|
|
259
|
+
try {
|
|
260
|
+
appendEvent(root, { runId: goal.id, timestamp: new Date().toISOString(), type: 'phase-ended', phase: 'resource-wait-implementation', durationMs: started - queued });
|
|
261
|
+
if (remaining <= 0 || input.signal.aborted)
|
|
262
|
+
throw new Error('Goal execution cancelled or agent-time budget exhausted');
|
|
263
|
+
return await operation();
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
const cleanup = error;
|
|
267
|
+
cleanupUnconfirmed ||= cleanup.cleanupUnconfirmed === true;
|
|
268
|
+
if (cleanup.cleanupUnconfirmed)
|
|
269
|
+
void cleanup.cleanupConfirmed?.then(() => lease.release()).catch(() => undefined);
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
clearTimeout(timer);
|
|
274
|
+
attemptAgentMs += Date.now() - started;
|
|
275
|
+
if (!cleanupUnconfirmed)
|
|
276
|
+
await lease.release();
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
const rawExecute = async (input) => {
|
|
280
|
+
return admitted(input, async () => {
|
|
281
|
+
if (options.execute)
|
|
282
|
+
return await options.execute(input);
|
|
283
|
+
if (native && input.provider === 'codex') {
|
|
284
|
+
let binding = goal.nativeBinding;
|
|
285
|
+
if (binding && (binding.model !== input.selection.model || binding.modelProvider !== input.selection.provider))
|
|
286
|
+
binding = undefined;
|
|
287
|
+
try {
|
|
288
|
+
const result = await executeCodexGoal({ ...input, objective: goal.objective, tokenBudget: goal.tokenBudget === undefined ? undefined : goal.tokenBudget - tokensUsed(), binding, onBinding: (nativeBinding) => { goal = save(root, { ...goal, nativeBinding }); }, onUsageUpdate: (usage) => {
|
|
289
|
+
if (goal.tokenBudget !== undefined && usage.inputTokens !== undefined && usage.outputTokens !== undefined && tokensUsed() + usage.inputTokens + usage.outputTokens > goal.tokenBudget)
|
|
290
|
+
controller.abort('Token budget exceeded during native execution');
|
|
291
|
+
} });
|
|
292
|
+
goal = save(root, { ...goal, nativeTokensUsed: result.nativeTokensUsed });
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
if (!(error instanceof NativeGoalUnavailableError))
|
|
297
|
+
throw error;
|
|
298
|
+
goal = save(root, { ...goal, nativeBinding: undefined, nativeTokensUsed: undefined });
|
|
299
|
+
appendEvent(root, { runId: goal.id, timestamp: new Date().toISOString(), type: 'status', data: { nativeGoal: 'unavailable', fallback: 'codex-exec' } });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return await executeAgent(input);
|
|
303
|
+
});
|
|
304
|
+
};
|
|
139
305
|
const execute = async (input) => {
|
|
140
306
|
if (!config?.routing?.enabled || config.routing.strategy !== "capability")
|
|
141
307
|
return rawExecute(input);
|
|
142
308
|
const routed = makeAsyncAdaptiveRunner({ parent: provider, parentSelection: input.selection, projectRoot: root, workers: config.routing.workers, strategy: "capability", maxCandidates: config.routing.maxCandidates, maxAttempts: Math.min(goal.maxAttempts, config.routing.maxAttempts ?? 5),
|
|
143
309
|
planner: resolvePlanner(config, provider, input.selection), fallback: config.routing.fallback, maxTier: config.routing.maxTier,
|
|
144
310
|
captureRoute: async (agent, context, prompt, selection) => {
|
|
145
|
-
|
|
146
|
-
|
|
311
|
+
return admitted({ ...input, provider: agent }, async () => {
|
|
312
|
+
const handle = startProviderProcess(agent, buildProviderInvocation(agent, prompt, root, "read-only", { ...selection, nativeMultiAgent: false }), { signal: input.signal, idleTimeoutMs: 20 * 60_000 });
|
|
313
|
+
const run = await handle.completion;
|
|
314
|
+
if (handle.recordPath && existsSync(handle.recordPath))
|
|
315
|
+
throw Object.assign(new Error('Planner process cleanup could not be confirmed'), { cleanupUnconfirmed: true });
|
|
316
|
+
return { success: run.kind === "succeeded", summary: run.kind, output: run.stdout, tokens: run.telemetry.tokens };
|
|
317
|
+
});
|
|
147
318
|
},
|
|
148
319
|
makeWorker: (agent, selection) => async (context) => {
|
|
149
|
-
const result = await rawExecute({ ...input, provider: agent, selection, prompt: input.prompt + "\nPlanner approach:\n" + (context.story.assessment?.approach ?? "") });
|
|
320
|
+
const result = await rawExecute({ ...input, provider: agent, selection: { ...selection, nativeMultiAgent: false }, prompt: input.prompt + "\nPlanner approach:\n" + (context.story.assessment?.approach ?? "") });
|
|
150
321
|
return { ...result, infrastructureFailure: !result.success, tokens: result.tokens ?? (result.inputTokens !== undefined && result.outputTokens !== undefined ? { inputTokens: result.inputTokens, outputTokens: result.outputTokens, model: result.model } : undefined) };
|
|
151
322
|
}, });
|
|
152
323
|
const result = await routed({ targetDir: root, story: { id: goal.id, title: goal.objective, priority: 1, passes: false, agent: provider, acceptance: manifest.criteria.map(c => c.text) } });
|
|
153
324
|
const last = result.tokens?.calls?.at(-1);
|
|
154
325
|
return { ...result, provider: last?.provider ?? provider, model: last?.actualModel, inputTokens: result.tokens?.measurementComplete ? result.tokens.inputTokens : undefined, outputTokens: result.tokens?.measurementComplete ? result.tokens.outputTokens : undefined };
|
|
155
326
|
};
|
|
156
|
-
let report =
|
|
327
|
+
let report = await checkProjectAsync(root, { signal: controller.signal });
|
|
328
|
+
if (report.cleanupUnconfirmed) {
|
|
329
|
+
goal = save(root, { ...goal, status: 'blocked', reason: 'Verification cleanup could not be confirmed; retained resource ownership requires inspection', lastCheck: report.id });
|
|
330
|
+
return goal;
|
|
331
|
+
}
|
|
157
332
|
while (report.status !== 'passed') {
|
|
158
333
|
const protectionProblem = acceptanceProtectionProblem(root);
|
|
159
334
|
if (protectionProblem)
|
|
160
|
-
return
|
|
335
|
+
return await finish('blocked', protectionProblem, report);
|
|
161
336
|
const pause = statePath(root, 'goal.pause');
|
|
162
337
|
if (existsSync(pause)) {
|
|
163
338
|
unlinkSync(pause);
|
|
164
|
-
return
|
|
339
|
+
return await finish('paused', 'Paused at a safe attempt boundary', report);
|
|
165
340
|
}
|
|
341
|
+
if (controller.signal.aborted)
|
|
342
|
+
return await finish('blocked', String(controller.signal.reason), report);
|
|
166
343
|
const remaining = goal.maxMinutes * 60_000 - goal.attempts.reduce((sum, a) => sum + a.durationMs, 0);
|
|
167
344
|
if (remaining <= 0 || goal.attempts.length >= goal.maxAttempts)
|
|
168
|
-
return
|
|
345
|
+
return await finish('blocked', 'Execution budget exhausted; unfinished work retained', report);
|
|
169
346
|
if (goal.tokenBudget !== undefined && goal.attempts.length > 0) {
|
|
170
347
|
const unknown = goal.attempts.some(a => a.inputTokens === undefined || a.outputTokens === undefined);
|
|
171
348
|
const used = goal.attempts.reduce((sum, a) => sum + (a.inputTokens ?? 0) + (a.outputTokens ?? 0), 0);
|
|
172
349
|
if (unknown || used >= goal.tokenBudget)
|
|
173
|
-
return
|
|
350
|
+
return await finish('blocked', unknown ? 'Token usage unknown; cannot safely start another budgeted attempt' : 'Token budget exhausted', report);
|
|
174
351
|
}
|
|
175
352
|
const started = Date.now();
|
|
176
|
-
goal = save(root, { ...goal, status: 'running', lastCheck: report.id, reason: undefined, pendingAttempt: { provider, model:
|
|
177
|
-
|
|
178
|
-
const timer = setTimeout(() => controller.abort('Goal time budget exceeded'), remaining);
|
|
353
|
+
goal = save(root, { ...goal, status: 'running', lastCheck: report.id, reason: undefined, pendingAttempt: { provider, model: selection.model, startedAt: new Date(started).toISOString() } });
|
|
354
|
+
attemptAgentMs = 0;
|
|
179
355
|
let result;
|
|
180
356
|
try {
|
|
181
|
-
result = await execute({ root, provider, selection
|
|
357
|
+
result = await execute({ root, provider, selection, signal: controller.signal, prompt: `${goalHandoff(root)}\nCurrent independent findings:\n${JSON.stringify(report.criteria).slice(0, 12000)}` });
|
|
182
358
|
}
|
|
183
359
|
catch (error) {
|
|
184
360
|
result = { success: false, summary: error.message };
|
|
185
361
|
}
|
|
186
|
-
|
|
187
|
-
|
|
362
|
+
const agentDurationMs = attemptAgentMs;
|
|
363
|
+
if (cleanupUnconfirmed) {
|
|
364
|
+
goal.attempts.push({ provider, model: selection.model, startedAt: new Date(started).toISOString(), durationMs: agentDurationMs, success: false, summary: result.summary, checkId: report.id });
|
|
365
|
+
goal = save(root, { ...goal, pendingAttempt: undefined, status: 'blocked', reason: 'Provider cleanup could not be confirmed; process record and resource reservation retained' });
|
|
366
|
+
return goal;
|
|
188
367
|
}
|
|
189
|
-
const agentDurationMs = Date.now() - started;
|
|
190
368
|
const checkStarted = Date.now();
|
|
191
|
-
report =
|
|
369
|
+
report = await checkProjectAsync(root, { signal: controller.signal });
|
|
192
370
|
if (!result.routing?.blocked)
|
|
193
371
|
result.routing?.recordOutcome(report.status === 'passed');
|
|
194
372
|
appendEvent(root, { runId: goal.id, timestamp: new Date().toISOString(), type: 'phase-ended', phase: 'verify', durationMs: Date.now() - checkStarted, outcome: report.status });
|
|
195
|
-
goal.attempts.push({ provider: result.provider ?? provider, model: result.model ??
|
|
373
|
+
goal.attempts.push({ provider: result.provider ?? provider, model: result.model ?? selection.model, startedAt: new Date(started).toISOString(), durationMs: agentDurationMs, success: result.success, summary: result.summary.slice(0, 8000), checkId: report.id, inputTokens: result.inputTokens, outputTokens: result.outputTokens });
|
|
196
374
|
const usageAvailable = result.inputTokens !== undefined && result.outputTokens !== undefined;
|
|
197
375
|
appendEvent(root, { runId: goal.id, timestamp: new Date().toISOString(), type: 'tokens', attemptId: `${goal.id}:${goal.attempts.length}`, durationMs: agentDurationMs, data: { ...result.tokens, provider: result.provider ?? provider, role: 'parent', model: result.model, inputTokens: result.inputTokens, outputTokens: result.outputTokens, usageAvailable } });
|
|
198
376
|
appendEvent(root, { runId: goal.id, timestamp: new Date().toISOString(), type: 'attempt-ended', attemptId: `${goal.id}:${goal.attempts.length}`, durationMs: Date.now() - started, outcome: report.status, data: { provider, model: result.model, usageAvailable } });
|
|
199
377
|
if (report.status === 'passed')
|
|
200
378
|
appendEvent(root, { runId: goal.id, timestamp: new Date().toISOString(), type: 'accepted', attemptId: `${goal.id}:${goal.attempts.length}` });
|
|
201
379
|
goal = save(root, { ...goal, lastCheck: report.id, pendingAttempt: undefined });
|
|
380
|
+
if (report.cleanupUnconfirmed) {
|
|
381
|
+
goal = save(root, { ...goal, status: 'blocked', reason: 'Verification cleanup could not be confirmed; process record and resource reservation retained' });
|
|
382
|
+
return goal;
|
|
383
|
+
}
|
|
202
384
|
if (result.routing?.blocked)
|
|
203
|
-
return
|
|
385
|
+
return await finish('blocked', result.summary, report);
|
|
204
386
|
if (controller.signal.aborted)
|
|
205
|
-
return
|
|
387
|
+
return await finish(existsSync(statePath(root, 'goal.pause')) ? 'paused' : 'blocked', `${String(controller.signal.reason)}; work retained`, report);
|
|
388
|
+
if (goal.tokenBudget !== undefined && (goal.attempts.some(a => a.inputTokens === undefined || a.outputTokens === undefined) || tokensUsed() > goal.tokenBudget))
|
|
389
|
+
return await finish('blocked', 'Token budget exhausted or usage unknown; work retained', report);
|
|
206
390
|
}
|
|
207
|
-
|
|
391
|
+
if (controller.signal.aborted)
|
|
392
|
+
return await finish(existsSync(statePath(root, 'goal.pause')) ? 'paused' : 'blocked', String(controller.signal.reason), report);
|
|
393
|
+
if (goal.tokenBudget !== undefined && goal.attempts.length && (goal.attempts.some(a => a.inputTokens === undefined || a.outputTokens === undefined) || tokensUsed() > goal.tokenBudget))
|
|
394
|
+
return await finish('blocked', 'Token budget exhausted or usage unknown; work retained', report);
|
|
395
|
+
return await finish('complete', 'All bound acceptance criteria and regression checks passed on the current workspace', report);
|
|
208
396
|
}
|
|
209
397
|
catch (error) {
|
|
210
398
|
if (goal)
|
|
211
|
-
save(root, { ...goal, status: 'blocked', reason: error.message });
|
|
399
|
+
goal = save(root, { ...goal, status: 'blocked', reason: error.message });
|
|
212
400
|
throw error;
|
|
213
401
|
}
|
|
214
402
|
finally {
|
|
215
|
-
|
|
403
|
+
if (wallTimer)
|
|
404
|
+
clearTimeout(wallTimer);
|
|
405
|
+
if (pauseTimer)
|
|
406
|
+
clearInterval(pauseTimer);
|
|
407
|
+
try {
|
|
408
|
+
if (goal && runStarted !== undefined) {
|
|
409
|
+
goal.wallDurationMs = wallAtStart + Math.max(0, Date.now() - runStarted);
|
|
410
|
+
goal.pendingRunStartedAt = undefined;
|
|
411
|
+
save(root, goal);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
releaseLock(root, lock.ownerToken);
|
|
416
|
+
}
|
|
216
417
|
}
|
|
217
418
|
}
|
package/dist/loop/cleanup.js
CHANGED
|
@@ -7,6 +7,7 @@ import { acquireTakeoverLease, acquireTakeoverRecoveryLease, lockPath, takeoverL
|
|
|
7
7
|
import { killProcessForCleanup, killProcessTreeForCleanup } from './watchdog.js';
|
|
8
8
|
import { cleanupClaims } from './claims.js';
|
|
9
9
|
import { clearStatus } from './reporter.js';
|
|
10
|
+
import { discardParallelRecoveryRecords } from './recovery.js';
|
|
10
11
|
// Reap orphaned runners PROJECT-SCOPED: kill only pids recorded in this project's
|
|
11
12
|
// .yoke/runner.pid files (main dir + each worktree). Never by process-name or
|
|
12
13
|
// command-line pattern — that takes down runners belonging to OTHER projects,
|
|
@@ -217,8 +218,16 @@ export function runLoopCleanup(targetDir, opts = {}) {
|
|
|
217
218
|
}
|
|
218
219
|
if (killed > 0)
|
|
219
220
|
console.log(`Killed ${killed} orphaned process tree(s) from project-scoped Yoke records.`);
|
|
220
|
-
if (opts.removeWorktrees && failed === 0)
|
|
221
|
+
if (opts.removeWorktrees && failed === 0) {
|
|
221
222
|
cleanupClaims(targetDir);
|
|
223
|
+
try {
|
|
224
|
+
discardParallelRecoveryRecords(targetDir);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
console.error(`Failed to discard parallel recovery records: ${error.message}`);
|
|
228
|
+
failed++;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
222
231
|
const lockFile = lockPath(targetDir);
|
|
223
232
|
if (existsSync(lockFile)) {
|
|
224
233
|
rmSync(lockFile, { force: true });
|
package/dist/loop/dispatcher.js
CHANGED
|
@@ -34,18 +34,32 @@ async function gateResult(gates, path, worker) {
|
|
|
34
34
|
if (criteria.length > 0 && !gates.verifyCriterion)
|
|
35
35
|
return { passed: false, summary: 'criterion verifier is not configured' };
|
|
36
36
|
for (const criterion of criteria) {
|
|
37
|
-
const result = gates.verifyCriterion?.(path, story, criterion);
|
|
37
|
+
const result = withStoryContext(story.id, () => gates.verifyCriterion?.(path, story, criterion));
|
|
38
38
|
if (!result?.passed)
|
|
39
39
|
return { passed: false, summary: result?.summary ?? 'criterion verification failed' };
|
|
40
40
|
}
|
|
41
41
|
for (const gate of [gates.verify, gates.design, gates.perf, gates.audit]) {
|
|
42
42
|
if (!gate)
|
|
43
43
|
continue;
|
|
44
|
-
const result = gate(path, story);
|
|
44
|
+
const result = withStoryContext(story.id, () => gate(path, story));
|
|
45
45
|
if (!result.passed)
|
|
46
46
|
return result;
|
|
47
47
|
}
|
|
48
|
-
return gates.qualityReview?.(path, story, worker) ?? { passed: true, summary: 'integrated gates passed' };
|
|
48
|
+
return withStoryContext(story.id, () => gates.qualityReview?.(path, story, worker)) ?? { passed: true, summary: 'integrated gates passed' };
|
|
49
|
+
}
|
|
50
|
+
// All gate APIs are synchronous: never leave ambient story context set across an await.
|
|
51
|
+
function withStoryContext(storyId, gate) {
|
|
52
|
+
const previous = process.env.YOKE_STORY;
|
|
53
|
+
process.env.YOKE_STORY = storyId;
|
|
54
|
+
try {
|
|
55
|
+
return gate();
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
if (previous === undefined)
|
|
59
|
+
delete process.env.YOKE_STORY;
|
|
60
|
+
else
|
|
61
|
+
process.env.YOKE_STORY = previous;
|
|
62
|
+
}
|
|
49
63
|
}
|
|
50
64
|
export function createDispatcher(options) {
|
|
51
65
|
const dispatcherId = options.dispatcherId ?? randomUUID();
|
|
@@ -125,6 +139,11 @@ export function createDispatcher(options) {
|
|
|
125
139
|
if (claims.release(input) === false)
|
|
126
140
|
throw new Error('claim release failed');
|
|
127
141
|
};
|
|
142
|
+
const cleanupRetained = createWorkerCleanup({
|
|
143
|
+
cleanupProcess: options.worktrees.cleanupProcess,
|
|
144
|
+
removeWorktree: () => undefined,
|
|
145
|
+
releaseClaim: releaseCandidateClaim,
|
|
146
|
+
});
|
|
128
147
|
const persistPass = (input) => {
|
|
129
148
|
if (!options.prdPath)
|
|
130
149
|
return;
|
|
@@ -135,6 +154,8 @@ export function createDispatcher(options) {
|
|
|
135
154
|
};
|
|
136
155
|
const enqueue = (input, result) => {
|
|
137
156
|
let reason = 'integrated verification failed';
|
|
157
|
+
let retained = false;
|
|
158
|
+
let recoveryBaseCommit = input.worktree.baseCommit;
|
|
138
159
|
const integrationQueuedAt = Date.now();
|
|
139
160
|
let integrationStartedAt;
|
|
140
161
|
const integrationController = new AbortController();
|
|
@@ -160,6 +181,7 @@ export function createDispatcher(options) {
|
|
|
160
181
|
reason = rebase.reason;
|
|
161
182
|
throw new Error(reason);
|
|
162
183
|
}
|
|
184
|
+
recoveryBaseCommit = rebase.expectedHead;
|
|
163
185
|
return rebase.expectedHead;
|
|
164
186
|
},
|
|
165
187
|
verify: async () => {
|
|
@@ -169,7 +191,9 @@ export function createDispatcher(options) {
|
|
|
169
191
|
}
|
|
170
192
|
const gate = await gateResult(options.gates, input.worktree.path, input);
|
|
171
193
|
reason = gate.summary;
|
|
172
|
-
|
|
194
|
+
if (!gate.passed)
|
|
195
|
+
throw new Error(gate.summary);
|
|
196
|
+
return true;
|
|
173
197
|
},
|
|
174
198
|
integrate: async (expectedHead) => {
|
|
175
199
|
if (cancellationReason)
|
|
@@ -200,6 +224,15 @@ export function createDispatcher(options) {
|
|
|
200
224
|
return;
|
|
201
225
|
case 'reopened':
|
|
202
226
|
reopened.push(input.story.id);
|
|
227
|
+
reason = merge.reason;
|
|
228
|
+
if (options.worktrees.retain) {
|
|
229
|
+
// Retain first, then report/release. Recovery evidence must precede cleanup.
|
|
230
|
+
retained = true;
|
|
231
|
+
options.worktrees.retain({ ...input, worktree: { ...input.worktree, baseCommit: recoveryBaseCommit } }, reason);
|
|
232
|
+
failed.push(input.story.id);
|
|
233
|
+
failureReasons.set(input.story.id, `${reason}; candidate retained at ${input.worktree.path}`);
|
|
234
|
+
}
|
|
235
|
+
options.onReopened?.(input.story, { reason, worktree: input.worktree.path, baseCommit: recoveryBaseCommit, ownerToken: input.ownerToken });
|
|
203
236
|
result.routing.recordOutcome?.(false);
|
|
204
237
|
return;
|
|
205
238
|
default: {
|
|
@@ -217,7 +250,10 @@ export function createDispatcher(options) {
|
|
|
217
250
|
options.onIntegrationMetrics?.(input, Date.now() - integrationQueuedAt, integrationStartedAt === undefined ? undefined : Date.now() - integrationStartedAt);
|
|
218
251
|
}
|
|
219
252
|
finally {
|
|
220
|
-
|
|
253
|
+
if (retained)
|
|
254
|
+
cleanupRetained(input);
|
|
255
|
+
else
|
|
256
|
+
cleanup(input);
|
|
221
257
|
reportProgress();
|
|
222
258
|
}
|
|
223
259
|
});
|
|
@@ -227,12 +263,22 @@ export function createDispatcher(options) {
|
|
|
227
263
|
const launch = (story) => {
|
|
228
264
|
const provider = providerFor(story);
|
|
229
265
|
const ownerToken = ids();
|
|
230
|
-
|
|
266
|
+
let worktree;
|
|
267
|
+
try {
|
|
268
|
+
worktree = options.worktrees.create({ story, dispatcherId, ownerToken, provider });
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
failed.push(story.id);
|
|
272
|
+
failureReasons.set(story.id, `worktree setup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
273
|
+
reportProgress();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
231
276
|
const controller = new AbortController();
|
|
232
277
|
const candidateRace = Boolean(options.candidateCount && options.candidateCount > 1 && options.candidateCoordinator);
|
|
233
278
|
const input = { story, worktree, provider, cancellation: { signal: controller.signal }, dispatcherId, ownerToken, ...(candidateRace ? { candidateRace: true } : {}) };
|
|
234
279
|
if (!claims.acquire(input)) {
|
|
235
|
-
|
|
280
|
+
if (!worktree.recovered)
|
|
281
|
+
options.worktrees.remove(input);
|
|
236
282
|
return;
|
|
237
283
|
}
|
|
238
284
|
iterations += 1;
|
|
@@ -241,6 +287,19 @@ export function createDispatcher(options) {
|
|
|
241
287
|
areas.add(story.area);
|
|
242
288
|
const candidateDispatch = candidateRace;
|
|
243
289
|
const activeWorker = { input, controller, task: Promise.resolve(), phase: options.acquireResource ? 'waiting-resource' : 'implementing' };
|
|
290
|
+
// Durable admission reporting must succeed before the worker microtask can start.
|
|
291
|
+
active.set(story.id, activeWorker);
|
|
292
|
+
try {
|
|
293
|
+
reportProgress();
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
active.delete(story.id);
|
|
297
|
+
if (worktree.recovered)
|
|
298
|
+
cleanupRetained(input);
|
|
299
|
+
else
|
|
300
|
+
cleanup(input);
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
244
303
|
const task = Promise.resolve().then(async () => {
|
|
245
304
|
const workerLease = options.acquireResource
|
|
246
305
|
? await options.acquireResource({ story, provider, role: 'implementation', units: candidateRace ? options.candidateCount : 1, signal: controller.signal })
|
|
@@ -248,6 +307,13 @@ export function createDispatcher(options) {
|
|
|
248
307
|
activeWorker.phase = 'implementing';
|
|
249
308
|
reportProgress();
|
|
250
309
|
try {
|
|
310
|
+
if (worktree.recovered)
|
|
311
|
+
options.worktrees.cleanupProcess?.(input);
|
|
312
|
+
if (worktree.recovered)
|
|
313
|
+
return { source: 'worker', result: {
|
|
314
|
+
kind: 'candidate', storyId: story.id, worktree: worktree.path, baseCommit: worktree.baseCommit,
|
|
315
|
+
provider, summary: 'retained candidate recovered for independent integration checks', evidence: { criteria: [] }, routing: { outcome: 'pending-integration' },
|
|
316
|
+
} };
|
|
251
317
|
return await (candidateDispatch && options.candidateCoordinator
|
|
252
318
|
? Promise.resolve(options.candidateCoordinator(input)).then(coordinateCandidates).then(result => ({ source: 'candidates', result }))
|
|
253
319
|
: options.worker(input).then(result => ({ source: 'worker', result })));
|
|
@@ -312,7 +378,10 @@ export function createDispatcher(options) {
|
|
|
312
378
|
if (cancellationReason || paused || options.pause?.()) {
|
|
313
379
|
paused = true;
|
|
314
380
|
result.routing.recordOutcome?.(false);
|
|
315
|
-
|
|
381
|
+
if (input.worktree.recovered)
|
|
382
|
+
cleanupRetained(input);
|
|
383
|
+
else
|
|
384
|
+
cleanup(input);
|
|
316
385
|
reportProgress();
|
|
317
386
|
return;
|
|
318
387
|
}
|
|
@@ -322,7 +391,10 @@ export function createDispatcher(options) {
|
|
|
322
391
|
failed.push(story.id);
|
|
323
392
|
failureReasons.set(story.id, error instanceof Error ? error.message : String(error));
|
|
324
393
|
try {
|
|
325
|
-
|
|
394
|
+
if (input.worktree.recovered)
|
|
395
|
+
cleanupRetained(input);
|
|
396
|
+
else
|
|
397
|
+
cleanup(input);
|
|
326
398
|
}
|
|
327
399
|
catch (cleanupError) {
|
|
328
400
|
reportProgress();
|
|
@@ -387,7 +459,7 @@ export function createDispatcher(options) {
|
|
|
387
459
|
if (failed.length > 0) {
|
|
388
460
|
const storyId = failed[0];
|
|
389
461
|
const detail = storyId ? failureReasons.get(storyId) : undefined;
|
|
390
|
-
return { status: 'blocked', ...(storyId && detail ? { reason: `worker ${storyId} failed: ${detail}` } : {}), iterations, integrated, reopened, failed };
|
|
462
|
+
return { status: 'blocked', ...(storyId && detail ? { reason: `${reopened.includes(storyId) ? `story ${storyId} integration rejected` : `worker ${storyId} failed`}: ${detail}` } : {}), iterations, integrated, reopened, failed };
|
|
391
463
|
}
|
|
392
464
|
if (iterations >= options.maxIterations)
|
|
393
465
|
return { status: 'cap-reached', iterations, integrated, reopened, failed };
|
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.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/claims', '.yoke/worktrees', '.yoke/artifacts', '.yoke/events', '.yoke/history', '.yoke/routing', '.yoke/checks', '.yoke/integration-recovery', '.yoke/run-state.json', '.yoke/run-state.*.tmp', '.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) {
|