@hecer/yoke 1.21.1 → 1.23.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 +48 -0
- package/README.md +8 -1
- package/TODOS.md +6 -0
- package/bench/analyze-codex-comparison.mjs +90 -17
- package/bench/compare-codex.mjs +159 -36
- package/bench/result-schema.mjs +132 -0
- package/canon/manifest.yaml +1 -1
- package/canon/skills/visual-verification/SKILL.md +25 -2
- package/canon/tools/codex-rtk-hook.mjs +6 -16
- package/dist/agents/pi-telemetry.js +2 -1
- package/dist/agents/process-streams.js +12 -64
- package/dist/agents/provider-selection.js +12 -0
- package/dist/agents/telemetry.js +52 -52
- package/dist/change/inbox.js +8 -3
- package/dist/check/command.js +69 -17
- package/dist/check/delivery.js +121 -0
- package/dist/cli.js +91 -3
- package/dist/code-intelligence/adapters/mcp.js +1 -0
- package/dist/code-intelligence/budgets.js +138 -0
- package/dist/code-intelligence/contracts.js +2 -0
- package/dist/code-intelligence/coordinator.js +159 -85
- package/dist/code-intelligence/evidence.js +87 -34
- package/dist/code-intelligence/index.js +1 -0
- package/dist/code-intelligence/mcp-client.js +25 -6
- package/dist/code-intelligence/mcp-server.js +14 -11
- package/dist/code-intelligence/preflight.js +71 -0
- package/dist/dashboard/analytics.js +5 -3
- package/dist/goals/command.js +183 -53
- package/dist/goals/usage.js +87 -0
- package/dist/loop/cache-isolation.js +36 -0
- package/dist/loop/candidate-cleanup.js +47 -17
- package/dist/loop/candidates.js +17 -11
- package/dist/loop/dispatcher.js +89 -26
- package/dist/loop/failure.js +104 -0
- package/dist/loop/gate-snapshot.js +19 -0
- package/dist/loop/git.js +1 -1
- package/dist/loop/loop.js +124 -70
- package/dist/loop/parallel-adapters.js +57 -6
- package/dist/loop/parallel-command.js +49 -7
- package/dist/loop/proof-retention.js +70 -0
- package/dist/loop/recovery.js +23 -5
- package/dist/loop/reporter.js +22 -5
- package/dist/loop/run-command.js +101 -47
- package/dist/loop/runner.js +6 -5
- package/dist/loop/worker.js +152 -91
- package/dist/observability/history.js +2 -1
- package/dist/observability/invocation.js +42 -0
- package/dist/observability/local-report.js +120 -0
- package/dist/observability/usage.js +19 -0
- package/dist/prd/command.js +20 -7
- package/dist/prd/decompose.js +5 -2
- package/dist/retrofit/config.js +29 -2
- package/dist/retrofit/gitignore.js +12 -0
- package/dist/retrofit/planners/codex.js +20 -20
- package/dist/routing/attempts.js +241 -0
- package/dist/routing/capability.js +13 -9
- package/dist/routing/optimization.js +73 -0
- package/dist/routing/registry.js +7 -1
- package/dist/routing/router.js +282 -127
- package/dist/setup/command.js +8 -2
- package/dist/smoke/command.js +387 -85
- package/dist/update/check.js +1 -1
- package/docs/BENCHMARK-MANIFEST.md +131 -0
- package/docs/CODE-INTELLIGENCE.md +43 -1
- package/docs/CODEX-COMPARISON-2026-09-29.md +15 -0
- package/docs/DELIVERY-JOURNEYS.md +206 -0
- package/docs/ECONOMIC-ROUTING.md +180 -0
- package/docs/GOALS.md +61 -4
- package/docs/RELEASE-VALIDATION-1.22.0.md +115 -0
- package/docs/RELEASE-VALIDATION-1.23.0.md +39 -0
- package/docs/benchmarks/2026-10-04-efficiency/ANALYSE.md +182 -0
- package/docs/benchmarks/2026-10-04-efficiency/compare-help.py +55 -0
- package/docs/benchmarks/2026-10-04-efficiency/manifest.json +125 -0
- package/docs/benchmarks/2026-10-04-efficiency/provenance-analysis.json +90 -0
- package/docs/benchmarks/2026-10-04-efficiency/provenance-design.json +90 -0
- package/docs/benchmarks/2026-10-04-efficiency/provenance-original-report.json +90 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/DEVELOPMENT_ANALYSIS.md +142 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/RESULT.md +21 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/commands.jsonl +26 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/environment.json +31 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/final-yoke-smoke.json +40 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/model-purpose-hints.csv +19 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/observations.jsonl +21 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/observer-command-phases.csv +12 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/roles.csv +5 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/shell-categories.csv +8 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/stories.csv +8 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/summary.json +469 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-history.jsonl +104 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-1.log +58 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-2.log +29 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-3.log +5 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-4.log +12 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-phases.csv +10 -0
- package/docs/benchmarks/2026-10-04-efficiency/regression-comparison.json +104 -0
- package/docs/parallel-execution.md +37 -9
- package/docs/superpowers/plans/2026-10-04-yoke-1.23-efficiency-prd.json +11 -0
- package/docs/superpowers/plans/2026-10-04-yoke-1.23-efficiency.md +83 -0
- package/docs/superpowers/specs/2026-10-04-yoke-1.23-efficiency-design.md +120 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { cacheIsolationProblem } from './cache-isolation.js';
|
|
1
2
|
import { existsSync, rmSync } from 'node:fs';
|
|
3
|
+
import { observeFailure, clearFailureProgress } from './failure.js';
|
|
2
4
|
import { acceptanceProtectionProblem } from '../check/command.js';
|
|
3
5
|
import { makeAsyncAdaptiveRunner } from '../routing/router.js';
|
|
4
6
|
import { resolvePlanner } from '../routing/planning.js';
|
|
@@ -12,12 +14,25 @@ import { loadPrd, progress } from './prd.js';
|
|
|
12
14
|
import { makeAsyncRunner } from './runner.js';
|
|
13
15
|
import { runStoryWorker } from './worker.js';
|
|
14
16
|
import { acquireSharedWorker, sharedPoolStatus } from './resource-pool.js';
|
|
17
|
+
import { providerTelemetryUsage } from '../observability/usage.js';
|
|
15
18
|
export async function runParallelLoopCommand(input) {
|
|
16
|
-
const originalVerify = input.verify;
|
|
19
|
+
const originalVerify = input.verify, originalCriterion = input.verifyCriterion;
|
|
20
|
+
const cacheProblem = (path) => !input.git && path !== input.targetDir ? cacheIsolationProblem(path) : undefined;
|
|
17
21
|
input = { ...input, verify: path => {
|
|
18
|
-
const problem = acceptanceProtectionProblem(path, input.targetDir);
|
|
22
|
+
const problem = cacheProblem(path) ?? acceptanceProtectionProblem(path, input.targetDir);
|
|
19
23
|
return problem ? { passed: false, summary: problem } : originalVerify(path);
|
|
24
|
+
}, verifyCriterion: (path, story, criterion) => {
|
|
25
|
+
const problem = cacheProblem(path);
|
|
26
|
+
return problem ? { passed: false, summary: problem } : originalCriterion(path, story, criterion);
|
|
20
27
|
} };
|
|
28
|
+
for (const name of ['design', 'perf', 'audit']) {
|
|
29
|
+
const gate = input[name];
|
|
30
|
+
if (gate)
|
|
31
|
+
input = { ...input, [name]: (path) => {
|
|
32
|
+
const problem = cacheProblem(path);
|
|
33
|
+
return problem ? { passed: false, summary: problem } : gate(path);
|
|
34
|
+
} };
|
|
35
|
+
}
|
|
21
36
|
const adapters = makeParallelAdapters(input.targetDir, input.identity, input.git);
|
|
22
37
|
if (!adapters.git.isClean(input.targetDir)) {
|
|
23
38
|
input.reporter.blocked('target working tree is not clean');
|
|
@@ -96,6 +111,8 @@ export async function runParallelLoopCommand(input) {
|
|
|
96
111
|
baseCommit: workerInput.worktree.baseCommit,
|
|
97
112
|
provider: workerInput.provider,
|
|
98
113
|
runner,
|
|
114
|
+
feedback: workerInput.worktree.recovery?.feedback,
|
|
115
|
+
failureRoot: input.targetDir,
|
|
99
116
|
verify: input.verify,
|
|
100
117
|
design: input.design,
|
|
101
118
|
verifyCriterion: input.verifyCriterion,
|
|
@@ -126,11 +143,31 @@ export async function runParallelLoopCommand(input) {
|
|
|
126
143
|
const result = await dispatcher.run();
|
|
127
144
|
const finalProgress = progress(loadPrd(input.prdPath));
|
|
128
145
|
if (result.status === 'complete' && input.completion) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
146
|
+
const previous = process.env.YOKE_PHASE;
|
|
147
|
+
process.env.YOKE_PHASE = 'completion';
|
|
148
|
+
let reason;
|
|
149
|
+
try {
|
|
150
|
+
const gate = input.completion(input.targetDir);
|
|
151
|
+
if (!gate.passed)
|
|
152
|
+
reason = `integrated system did not verify: ${gate.summary}`;
|
|
153
|
+
else if (!adapters.git.isClean(input.targetDir))
|
|
154
|
+
reason = 'completion command left source or final assets dirty; preserve changes and move rerun proofs to ignored runtime paths before resuming';
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
reason = `integrated completion gate failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
if (previous === undefined)
|
|
161
|
+
delete process.env.YOKE_PHASE;
|
|
162
|
+
else
|
|
163
|
+
process.env.YOKE_PHASE = previous;
|
|
164
|
+
}
|
|
165
|
+
if (reason) {
|
|
166
|
+
const observed = observeFailure({ root: input.targetDir, directory: input.targetDir, stage: 'completion', summary: reason });
|
|
167
|
+
input.reporter.blocked(observed.action === 'retry' ? reason : observed.feedback, observed.failure);
|
|
132
168
|
return 1;
|
|
133
169
|
}
|
|
170
|
+
clearFailureProgress(input.targetDir);
|
|
134
171
|
}
|
|
135
172
|
if (result.status === 'complete')
|
|
136
173
|
input.reporter.complete(finalProgress);
|
|
@@ -139,7 +176,7 @@ export async function runParallelLoopCommand(input) {
|
|
|
139
176
|
else if (result.status === 'cap-reached')
|
|
140
177
|
input.reporter.capReached(finalProgress);
|
|
141
178
|
else
|
|
142
|
-
input.reporter.blocked(result.reason ?? `parallel dispatcher ${result.status}
|
|
179
|
+
input.reporter.blocked(result.reason ?? `parallel dispatcher ${result.status}`, result.failure);
|
|
143
180
|
return result.status === 'complete' ? 0 : result.status === 'paused' ? 3 : 1;
|
|
144
181
|
}
|
|
145
182
|
function candidateCoordinatorInput(input, adapters, worker, pause) {
|
|
@@ -182,6 +219,8 @@ function candidateDefinitions(input, worker, candidateCount, pause) {
|
|
|
182
219
|
worker: {
|
|
183
220
|
provider: worker.provider,
|
|
184
221
|
runner,
|
|
222
|
+
failureRoot: input.targetDir,
|
|
223
|
+
failureScope: candidateId,
|
|
185
224
|
verify: input.verify,
|
|
186
225
|
design: input.design,
|
|
187
226
|
verifyCriterion: input.verifyCriterion,
|
|
@@ -290,6 +329,9 @@ function asyncRunner(input, provider, signal, workerId) {
|
|
|
290
329
|
strategy: input.routing.strategy,
|
|
291
330
|
maxCandidates: input.routing.maxCandidates,
|
|
292
331
|
maxAttempts: input.routing.maxAttempts,
|
|
332
|
+
optimization: input.routing.optimization,
|
|
333
|
+
accountingScope: input.accountingScope,
|
|
334
|
+
executionPolicyKey: input.executionPolicyKey,
|
|
293
335
|
planner: resolvePlanner({ planning: input.planning }, input.runnerAgent, input.selection),
|
|
294
336
|
assessmentPolicy: input.routing.assessmentPolicy,
|
|
295
337
|
fallback: input.routing.fallback,
|
|
@@ -338,7 +380,7 @@ function asyncRunner(input, provider, signal, workerId) {
|
|
|
338
380
|
};
|
|
339
381
|
}
|
|
340
382
|
export function providerProcessResultToAgentResult(agent, storyId, result) {
|
|
341
|
-
const tokens = result.telemetry
|
|
383
|
+
const tokens = providerTelemetryUsage(result.telemetry);
|
|
342
384
|
const telemetry = tokens ? { tokens } : {};
|
|
343
385
|
switch (result.kind) {
|
|
344
386
|
case 'succeeded': return { success: true, summary: `${agent} implemented ${storyId}`, ...telemetry };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, relative, resolve, isAbsolute } from 'node:path';
|
|
4
|
+
import { workspaceFingerprint } from '../workspace/fingerprint.js';
|
|
5
|
+
import { storyPathSegment } from './prd.js';
|
|
6
|
+
const hash = (data) => createHash('sha256').update(data).digest('hex');
|
|
7
|
+
function safePath(root, path) {
|
|
8
|
+
const rel = relative(resolve(root), resolve(path));
|
|
9
|
+
if (isAbsolute(rel) || rel === '..' || rel.startsWith('../') || rel.startsWith('..\\'))
|
|
10
|
+
throw new Error('Proof path escaped workspace');
|
|
11
|
+
let current = root;
|
|
12
|
+
for (const segment of rel.split(/[\\/]/u).filter(Boolean)) {
|
|
13
|
+
current = join(current, segment);
|
|
14
|
+
try {
|
|
15
|
+
if (lstatSync(current).isSymbolicLink())
|
|
16
|
+
throw new Error(`Proof path must not contain a link: ${current}`);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code !== 'ENOENT')
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Copy selected proof into an immutable runtime snapshot; errors preserve the candidate. */
|
|
25
|
+
export function retainRuntimeProof(directory, storyId, targetDirectory) {
|
|
26
|
+
const entries = [];
|
|
27
|
+
const walk = (dir, base, prefix = '') => {
|
|
28
|
+
safePath(directory, dir);
|
|
29
|
+
if (!existsSync(dir))
|
|
30
|
+
return;
|
|
31
|
+
for (const name of readdirSync(dir).sort()) {
|
|
32
|
+
const file = join(dir, name);
|
|
33
|
+
safePath(directory, file);
|
|
34
|
+
const stat = lstatSync(file);
|
|
35
|
+
if (stat.isDirectory())
|
|
36
|
+
walk(file, base, prefix);
|
|
37
|
+
else if (stat.isFile()) {
|
|
38
|
+
const bytes = readFileSync(file);
|
|
39
|
+
entries.push({ path: (prefix + relative(base, file)).replace(/\\/gu, '/'), sha256: hash(bytes), bytes });
|
|
40
|
+
}
|
|
41
|
+
else
|
|
42
|
+
throw new Error(`Unsupported proof entry: ${file}`);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const artifacts = join(directory, '.yoke/artifacts'), proof = join(directory, '.yoke/proof');
|
|
46
|
+
walk(artifacts, artifacts);
|
|
47
|
+
walk(proof, proof, 'proof/');
|
|
48
|
+
if (!entries.length)
|
|
49
|
+
return;
|
|
50
|
+
const config = ['config.yaml', 'acceptance.yaml', 'prd.yaml'].map(name => {
|
|
51
|
+
const path = join(directory, '.yoke', name);
|
|
52
|
+
safePath(directory, path);
|
|
53
|
+
return [name, existsSync(path) ? hash(readFileSync(path)) : 'missing'];
|
|
54
|
+
});
|
|
55
|
+
const manifest = JSON.stringify({ version: 1, source: workspaceFingerprint(directory), config: hash(JSON.stringify(config)), environment: hash(JSON.stringify({ platform: process.platform, arch: process.arch, versions: process.versions })), files: entries.map(({ path, sha256 }) => ({ path, sha256 })) }, null, 2) + '\n';
|
|
56
|
+
const destination = join(targetDirectory, '.yoke/proof', storyPathSegment(storyId), 'runtime-artifacts', hash(manifest));
|
|
57
|
+
for (const { path, sha256, bytes } of [...entries, { path: 'manifest.json', sha256: hash(manifest), bytes: Buffer.from(manifest) }]) {
|
|
58
|
+
const copied = join(destination, path);
|
|
59
|
+
safePath(targetDirectory, copied);
|
|
60
|
+
mkdirSync(dirname(copied), { recursive: true });
|
|
61
|
+
if (existsSync(copied)) {
|
|
62
|
+
if (hash(readFileSync(copied)) !== sha256)
|
|
63
|
+
throw new Error(`Retained proof hash mismatch: ${path}`);
|
|
64
|
+
}
|
|
65
|
+
else
|
|
66
|
+
writeFileSync(copied, bytes, { mode: 0o600, flag: 'wx' });
|
|
67
|
+
if (hash(readFileSync(copied)) !== sha256)
|
|
68
|
+
throw new Error(`Proof copy hash mismatch: ${path}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
package/dist/loop/recovery.js
CHANGED
|
@@ -52,10 +52,12 @@ export function prepareIsolatedWorktree(directory, worktree, resume) {
|
|
|
52
52
|
// Record is outside the worker checkout and binds reuse to its source state.
|
|
53
53
|
writeFileSync(record, JSON.stringify({ version: 1, root, worktree: wt, base, prdHash }), { mode: 0o600 });
|
|
54
54
|
}
|
|
55
|
-
const
|
|
55
|
+
const LegacyParallelRecovery = z.object({
|
|
56
56
|
version: z.literal(1), root: z.string().max(4096), storyId: z.string().max(1024), worktree: z.string().max(4096), baseCommit: z.string().max(128),
|
|
57
57
|
prdHash: z.string().length(64), ownerToken: z.string().min(1).max(256), reason: z.string().max(16384), state: z.literal('retained'), recordedAt: z.string().datetime(),
|
|
58
58
|
}).strict();
|
|
59
|
+
const CurrentParallelRecovery = LegacyParallelRecovery.extend({ version: z.literal(2), phase: z.enum(['implementation', 'integration']) });
|
|
60
|
+
const ParallelRecovery = z.discriminatedUnion('version', [LegacyParallelRecovery, CurrentParallelRecovery]);
|
|
59
61
|
export function parallelAcceptanceDigest(directory) {
|
|
60
62
|
// Accepted sibling stories change only passes. Their acceptance contracts remain protected.
|
|
61
63
|
const contract = loadPrd(join(directory, '.yoke', 'prd.yaml')).map(({ passes: _passes, ...story }) => story);
|
|
@@ -84,7 +86,7 @@ export function discardParallelRecoveryRecords(directory) {
|
|
|
84
86
|
export function retainParallelWorktree(directory, file, input) {
|
|
85
87
|
const root = realpathSync(directory);
|
|
86
88
|
const worktree = realpathSync(input.worktree);
|
|
87
|
-
const record =
|
|
89
|
+
const record = CurrentParallelRecovery.parse({ version: 2, root, ...input, phase: input.phase ?? 'integration', reason: input.reason.slice(0, 16384), worktree, state: 'retained', recordedAt: new Date().toISOString() });
|
|
88
90
|
const safe = parallelRecordPath(directory, file);
|
|
89
91
|
mkdirSync(dirname(safe), { recursive: true });
|
|
90
92
|
const temp = statePath(directory, 'integration-recovery', `${randomUUID()}.tmp`);
|
|
@@ -107,6 +109,8 @@ export function recoverParallelWorktree(directory, file, storyId) {
|
|
|
107
109
|
if (stat.size > 65536)
|
|
108
110
|
throw new Error('Parallel recovery record is too large');
|
|
109
111
|
const saved = ParallelRecovery.parse(JSON.parse(readFileSync(safe, 'utf8')));
|
|
112
|
+
// Existing records describe independently checked candidates awaiting integration.
|
|
113
|
+
const phase = saved.version === 1 ? 'integration' : saved.phase;
|
|
110
114
|
if (!existsSync(saved.worktree))
|
|
111
115
|
throw new Error(`Retained candidate is missing: ${saved.worktree}; resolve its recovery record before retrying`);
|
|
112
116
|
const root = realpathSync(directory);
|
|
@@ -117,8 +121,22 @@ export function recoverParallelWorktree(directory, file, storyId) {
|
|
|
117
121
|
if (saved.storyId !== storyId || pathIdentity(saved.root) !== pathIdentity(root) || pathIdentity(actual) !== pathIdentity(saved.worktree) || isAbsolute(rel) || rel !== expectedName)
|
|
118
122
|
throw new Error('Retained candidate ownership or path binding is invalid');
|
|
119
123
|
const git = (args, cwd = root) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
const stale = () => new Error(`Retained candidate is stale against target or PRD: ${actual}; reconcile it before retrying`);
|
|
125
|
+
if (saved.prdHash !== parallelAcceptanceDigest(root))
|
|
126
|
+
throw stale();
|
|
127
|
+
if (saved.baseCommit !== git(['rev-parse', 'HEAD'])) {
|
|
128
|
+
if (phase === 'integration')
|
|
129
|
+
throw stale();
|
|
130
|
+
// Siblings may have integrated while this worker was still incomplete. Reuse
|
|
131
|
+
// its original checkout only for a forward target; integration still rebases
|
|
132
|
+
// and independently verifies the combined tree before accepting the story.
|
|
133
|
+
try {
|
|
134
|
+
git(['merge-base', '--is-ancestor', saved.baseCommit, 'HEAD']);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
throw stale();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
122
140
|
const common = realpathSync(resolve(root, git(['rev-parse', '--git-common-dir'])));
|
|
123
141
|
if (pathIdentity(realpathSync(resolve(actual, git(['rev-parse', '--git-common-dir'], actual)))) !== pathIdentity(common))
|
|
124
142
|
throw new Error('Retained candidate belongs to another repository');
|
|
@@ -126,5 +144,5 @@ export function recoverParallelWorktree(directory, file, storyId) {
|
|
|
126
144
|
if (!registered.some(path => pathIdentity(path) === pathIdentity(actual)))
|
|
127
145
|
throw new Error('Retained candidate is not a registered worktree');
|
|
128
146
|
git(['merge-base', '--is-ancestor', saved.baseCommit, 'HEAD'], actual);
|
|
129
|
-
return { path: actual, baseCommit: saved.baseCommit, recovered: true, ownerToken: saved.ownerToken };
|
|
147
|
+
return { path: actual, baseCommit: saved.baseCommit, recovered: true, ownerToken: saved.ownerToken, recovery: { phase, feedback: saved.reason } };
|
|
130
148
|
}
|
package/dist/loop/reporter.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { appendEvent } from '../observability/events.js';
|
|
6
6
|
import { estimateDurations, validDuration } from '../estimation/durations.js';
|
|
7
|
+
import { recordActiveRoutingUsage, markActiveRoutingUsageIncomplete } from '../routing/attempts.js';
|
|
7
8
|
export const LOG_CAP_BYTES = 256 * 1024;
|
|
8
9
|
// Append a line to .yoke/loop.log, keeping the file bounded: once it exceeds
|
|
9
10
|
// capBytes, truncate to the recent tail (starting at a line boundary) so the log
|
|
@@ -225,15 +226,15 @@ export function makeReporter(dir, opts = {}, now = () => new Date()) {
|
|
|
225
226
|
const base = phase === 'exploring' || phase === 'waiting-exploration' || phase === 'waiting-recovery'
|
|
226
227
|
? (({ story: _story, storyTitle: _storyTitle, ...withoutStory }) => withoutStory)(status)
|
|
227
228
|
: status;
|
|
228
|
-
persist({ ...base, state: 'running', phase, ...(progress ? { progress } : {}), ...(reason ? { reason } : { reason: undefined }), updatedAt: now().toISOString() }, phase, ` · ${phase}…`);
|
|
229
|
+
persist({ ...base, state: 'running', phase, failure: undefined, ...(progress ? { progress } : {}), ...(reason ? { reason } : { reason: undefined }), updatedAt: now().toISOString() }, phase, ` · ${phase}…`);
|
|
229
230
|
},
|
|
230
|
-
blocked(reason) {
|
|
231
|
+
blocked(reason, failure) {
|
|
231
232
|
const base = current ?? emptyStatus(now().toISOString());
|
|
232
|
-
persist({ ...withoutParallel(base), state: 'blocked', reason, updatedAt: now().toISOString() }, 'blocked', `■ blocked on ${base.story ?? '?'}: ${reason}`);
|
|
233
|
+
persist({ ...withoutParallel(base), state: 'blocked', reason, failure, updatedAt: now().toISOString() }, 'blocked', `■ blocked on ${base.story ?? '?'}: ${reason}`);
|
|
233
234
|
},
|
|
234
235
|
complete(progress) {
|
|
235
236
|
persist({ ...withoutParallel(current ?? emptyStatus(now().toISOString())), state: 'complete', phase: undefined,
|
|
236
|
-
progress, reason: undefined, updatedAt: now().toISOString() }, 'complete', `✔ loop complete — ${progress.passed}/${progress.total}`);
|
|
237
|
+
progress, reason: undefined, failure: undefined, updatedAt: now().toISOString() }, 'complete', `✔ loop complete — ${progress.passed}/${progress.total}`);
|
|
237
238
|
},
|
|
238
239
|
capReached(progress) {
|
|
239
240
|
persist({ ...withoutParallel(current ?? emptyStatus(now().toISOString())), state: 'cap-reached', phase: undefined,
|
|
@@ -334,7 +335,7 @@ export function makeReporter(dir, opts = {}, now = () => new Date()) {
|
|
|
334
335
|
return;
|
|
335
336
|
}
|
|
336
337
|
const { integrator: _integrator, ...withoutIntegrator } = parallel;
|
|
337
|
-
persist({ ...base, parallel: withoutIntegrator, updatedAt: now().toISOString() }, 'parallel-integrator', ' ·
|
|
338
|
+
persist({ ...base, parallel: withoutIntegrator, updatedAt: now().toISOString() }, 'parallel-integrator', ' · integrator idle');
|
|
338
339
|
},
|
|
339
340
|
addTokens(usage) {
|
|
340
341
|
if (![usage.inputTokens, usage.outputTokens].every(value => Number.isFinite(value) && value >= 0))
|
|
@@ -345,6 +346,22 @@ export function makeReporter(dir, opts = {}, now = () => new Date()) {
|
|
|
345
346
|
if (value !== undefined && (!Number.isFinite(value) || value < 0))
|
|
346
347
|
delete usage[key];
|
|
347
348
|
}
|
|
349
|
+
if (!usage.calls?.length && !usage.callId)
|
|
350
|
+
usage.callId = randomUUID();
|
|
351
|
+
const costRole = usage.role && ['reviewer', 'critic', 'repair', 'quality-critic', 'quality-repair', 'candidate-selection'].includes(usage.role);
|
|
352
|
+
if (usage.storyId && (usage.routingAttemptId || (costRole && !usage.calls?.length))) {
|
|
353
|
+
try {
|
|
354
|
+
recordActiveRoutingUsage(dir, usage.storyId, usage);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Accounting failures must disqualify economic evidence without losing
|
|
358
|
+
// the independent event below or interrupting a useful implementation.
|
|
359
|
+
try {
|
|
360
|
+
markActiveRoutingUsageIncomplete(dir, usage.storyId);
|
|
361
|
+
}
|
|
362
|
+
catch { /* Router finalization also fails closed on corrupt accounting. */ }
|
|
363
|
+
}
|
|
364
|
+
}
|
|
348
365
|
const calls = usage.calls?.length ? usage.calls : [{ usageAvailable: usage.measurementComplete !== false, totalCostUsd: usage.totalCostUsd }];
|
|
349
366
|
measuredCalls += calls.filter(call => call.usageAvailable !== false).length;
|
|
350
367
|
unknownCalls += calls.filter(call => call.usageAvailable === false).length;
|
package/dist/loop/run-command.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { roleSelection } from "../routing/capability.js";
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { existsSync, unlinkSync } from 'node:fs';
|
|
4
5
|
import { loadConfig, saveConfig, defaultConfig, resolveOutputPolicy, resolveVerifyCommand } from '../retrofit/config.js';
|
|
@@ -31,6 +32,15 @@ import { MAX_PROJECT_WORKERS, sharedPoolStatus, withSharedWorkerSync } from './r
|
|
|
31
32
|
import { runPrdExplore } from '../prd/explore.js';
|
|
32
33
|
export const DEFAULT_IDLE_MINUTES = 20;
|
|
33
34
|
const STALE_MINUTES = 20; // a running status older than this likely means the loop died
|
|
35
|
+
const nativeReporters = new WeakSet();
|
|
36
|
+
function executionPolicyFingerprint(policy) {
|
|
37
|
+
const canonical = JSON.stringify(policy, (_key, value) => {
|
|
38
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
39
|
+
return value;
|
|
40
|
+
return Object.fromEntries(Object.keys(value).sort().map(key => [key, value[key]]));
|
|
41
|
+
});
|
|
42
|
+
return createHash('sha256').update(canonical).digest('hex');
|
|
43
|
+
}
|
|
34
44
|
export function relativeTime(fromIso, now) {
|
|
35
45
|
const ms = Math.max(0, now.getTime() - Date.parse(fromIso));
|
|
36
46
|
const s = Math.floor(ms / 1000);
|
|
@@ -66,7 +76,12 @@ export function loopStatus(targetDir, now = () => new Date(), opts) {
|
|
|
66
76
|
if (opts?.compact) {
|
|
67
77
|
if (!st)
|
|
68
78
|
return `state=${enabled ? 'enabled' : 'disabled'} prd="${prog}"`;
|
|
69
|
-
|
|
79
|
+
const workers = [...(st.parallel?.workers ?? [])].sort((a, b) => a.story.localeCompare(b.story));
|
|
80
|
+
const work = (worker) => `${encodeURIComponent(worker.story)}:${worker.phase ?? 'working'}`;
|
|
81
|
+
const integrator = st.parallel?.integrator;
|
|
82
|
+
const story = st.story ?? integrator?.story ?? workers[0]?.story ?? 'none';
|
|
83
|
+
const parallel = st.parallel ? ` workers=${workers.map(work).join(',') || 'none'} integrator=${integrator ? work(integrator) : 'none'} waiting=${st.parallel.waitingWorkers ?? 0} queued=${st.parallel.queuedIntegrations ?? st.parallel.queuedCandidates}` : '';
|
|
84
|
+
return `state=${st.state} story=${encodeURIComponent(story)} progress=${st.progress.passed}/${st.progress.total} phase=${st.phase ?? (st.parallel ? 'parallel' : st.state)}${parallel} updated=${relativeTime(st.updatedAt, now())}`;
|
|
70
85
|
}
|
|
71
86
|
const sharedPoolLine = () => {
|
|
72
87
|
try {
|
|
@@ -241,6 +256,8 @@ async function runContinuousExploration(targetDir, options) {
|
|
|
241
256
|
}
|
|
242
257
|
const limitDeadline = options.savedRun?.exploreDeadline ?? (options.exploreLimitMs === undefined ? undefined : Date.now() + options.exploreLimitMs);
|
|
243
258
|
const reporter = options.reporter ?? makeReporter(targetDir, { json: options.json, quiet: true });
|
|
259
|
+
if (!options.reporter)
|
|
260
|
+
nativeReporters.add(reporter);
|
|
244
261
|
let config;
|
|
245
262
|
try {
|
|
246
263
|
config = loadConfig(targetDir);
|
|
@@ -261,6 +278,7 @@ async function runContinuousExploration(targetDir, options) {
|
|
|
261
278
|
let reviewerAgent = options.reviewer ?? (reviewRequested ? SUPPORTED_AGENTS.find(agent => agent !== defaultImplementationAgent && available(agent)) : undefined);
|
|
262
279
|
let explorationAgent = defaultExplorer;
|
|
263
280
|
let retryWorktree = false;
|
|
281
|
+
let retryFeedback;
|
|
264
282
|
const safeBatchSize = () => {
|
|
265
283
|
if (retryWorktree)
|
|
266
284
|
return 1;
|
|
@@ -365,6 +383,7 @@ async function runContinuousExploration(targetDir, options) {
|
|
|
365
383
|
resultCode = await Promise.resolve(runLoopCommand(targetDir, {
|
|
366
384
|
...innerOptions,
|
|
367
385
|
agent: implementationAgent,
|
|
386
|
+
recoveryFeedback: retryFeedback,
|
|
368
387
|
...(reviewRequested && reviewerAgent ? { reviewer: reviewerAgent } : {}),
|
|
369
388
|
...(batchLimit !== undefined ? { maxIterations: batchLimit } : {}),
|
|
370
389
|
...(retryWorktree ? { resumeWorktree: true, parallel: 1, candidates: 1 } : {}),
|
|
@@ -455,7 +474,11 @@ async function runContinuousExploration(targetDir, options) {
|
|
|
455
474
|
}
|
|
456
475
|
continue;
|
|
457
476
|
}
|
|
458
|
-
if (afterStatus?.
|
|
477
|
+
if (afterStatus?.failure?.kind === 'no-progress') {
|
|
478
|
+
reporter.blocked(afterStatus.reason ?? 'Automatic continuation stopped after unchanged failures', afterStatus.failure);
|
|
479
|
+
return 1;
|
|
480
|
+
}
|
|
481
|
+
if (afterStatus?.failure?.kind === 'completion-failed' && allCurrentStoriesPass(targetDir)) {
|
|
459
482
|
retryWorktree = false;
|
|
460
483
|
reporter.phase('exploring', `all planned tasks pass, but ${afterStatus.reason}; looking for work that can resolve the completion gate`, currentProgress(targetDir));
|
|
461
484
|
const scan = await scanForWork(`The integrated completion gate is still failing: ${afterStatus.reason}`);
|
|
@@ -478,6 +501,7 @@ async function runContinuousExploration(targetDir, options) {
|
|
|
478
501
|
continue;
|
|
479
502
|
}
|
|
480
503
|
retryCount++;
|
|
504
|
+
retryFeedback = afterStatus?.reason;
|
|
481
505
|
advanceRecoveryProviders();
|
|
482
506
|
retryWorktree = (afterStatus?.parallel?.reopened ?? 0) === 0;
|
|
483
507
|
const wait = retryDelay(retryCount);
|
|
@@ -579,9 +603,10 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
579
603
|
}
|
|
580
604
|
}
|
|
581
605
|
const outputPolicy = resolveOutputPolicy(config);
|
|
606
|
+
const configuredVerifyCommand = opts.verify ? undefined : resolveVerifyCommand(targetDir, config);
|
|
582
607
|
let verify = opts.verify;
|
|
583
608
|
if (!verify) {
|
|
584
|
-
const command =
|
|
609
|
+
const command = configuredVerifyCommand;
|
|
585
610
|
if (!command) {
|
|
586
611
|
console.error('No verify command configured. Set verify.command in .yoke/config.yaml (e.g. "npm test") so the loop can confirm tests pass before marking work done.');
|
|
587
612
|
return 2;
|
|
@@ -752,6 +777,73 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
752
777
|
selection: runnerSelection,
|
|
753
778
|
commit: (_path, request) => commitPaths(targetDir, ['.yoke/prd.yaml'], `yoke: plan change ${request.id}`, commitIdentity),
|
|
754
779
|
}));
|
|
780
|
+
const reviewerProviders = !opts.reviewRunner && (opts.review || opts.reviewer) ? SUPPORTED_AGENTS.filter(available) : [];
|
|
781
|
+
let review = opts.reviewRunner;
|
|
782
|
+
let reviewProvider = 'unknown';
|
|
783
|
+
if (!review && (opts.review || opts.reviewer)) {
|
|
784
|
+
const reviewerAgent = opts.reviewer ?? reviewerProviders.find(agent => agent !== runnerAgent);
|
|
785
|
+
if (!reviewerAgent) {
|
|
786
|
+
if (!opts.allowSelfReview) {
|
|
787
|
+
console.error('No independent reviewer CLI is available. Install or select a second agent, or pass --allow-self-review explicitly.');
|
|
788
|
+
return 2;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const resolvedReviewer = reviewerAgent ?? runnerAgent;
|
|
792
|
+
reviewProvider = resolvedReviewer;
|
|
793
|
+
if (resolvedReviewer === runnerAgent && !opts.allowSelfReview) {
|
|
794
|
+
console.error(`Reviewer "${resolvedReviewer}" is also the implementer. Pick another agent or pass --allow-self-review explicitly.`);
|
|
795
|
+
return 2;
|
|
796
|
+
}
|
|
797
|
+
if (!available(resolvedReviewer)) {
|
|
798
|
+
console.error(`Reviewer agent CLI "${resolvedReviewer}" was not found on PATH. Install it, or pick another with --reviewer=<${AGENT_LIST}>.`);
|
|
799
|
+
return 2;
|
|
800
|
+
}
|
|
801
|
+
review = context => {
|
|
802
|
+
const implementer = readStatus(targetDir)?.routingDecisions?.[context.story.id]?.provider ?? runnerAgent;
|
|
803
|
+
const selectedReviewer = !opts.reviewer && resolvedReviewer === implementer
|
|
804
|
+
? reviewerProviders.find(agent => agent !== implementer) ?? resolvedReviewer : resolvedReviewer;
|
|
805
|
+
if (selectedReviewer === implementer && !opts.allowSelfReview)
|
|
806
|
+
return { success: false, summary: "Independent review requires a provider distinct from the routed implementer", reviewOutcome: { kind: "infrastructure", summary: "Routed implementation and reviewer share a provider" } };
|
|
807
|
+
reviewProvider = selectedReviewer;
|
|
808
|
+
return makeReviewRunner(selectedReviewer, idleMs, undefined, routingEnabled ? roleSelection(targetDir, config, context.story, selectedReviewer, "reviewer") : undefined)(context);
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
if (review) {
|
|
812
|
+
const reviewRunner = review;
|
|
813
|
+
review = context => {
|
|
814
|
+
const started = Date.now();
|
|
815
|
+
let result;
|
|
816
|
+
try {
|
|
817
|
+
result = reviewRunner(context);
|
|
818
|
+
return result;
|
|
819
|
+
}
|
|
820
|
+
finally {
|
|
821
|
+
executionReporter?.addTokens({ inputTokens: 0, outputTokens: 0, measurementComplete: result?.tokens !== undefined, ...result?.tokens, provider: reviewProvider, role: 'reviewer', storyId: context.story.id, durationMs: Date.now() - started });
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
// Only native, attributable roles justify comparisons of complete attempts.
|
|
826
|
+
// Concurrent candidates have ambiguous story-wide role attribution and remain
|
|
827
|
+
// conservative until their costs can be joined to an individual alternative.
|
|
828
|
+
const completeAccounting = candidates === 1 && !opts.runner && !opts.verify && !opts.design && !opts.perf && !opts.audit
|
|
829
|
+
&& !opts.reviewRunner && !opts.qualityRuntime && !opts.git && !opts.intake
|
|
830
|
+
&& (!opts.reporter || nativeReporters.has(opts.reporter));
|
|
831
|
+
const accountingScope = completeAccounting ? 'execution-attempt' : undefined;
|
|
832
|
+
const executionPolicyKey = completeAccounting ? executionPolicyFingerprint({
|
|
833
|
+
version: 1,
|
|
834
|
+
mode: useParallelDispatcher ? candidates > 1 ? 'candidates' : 'parallel' : 'serial',
|
|
835
|
+
parallel, candidates, isolate: useParallelDispatcher || isolate,
|
|
836
|
+
verify: { command: configuredVerifyCommand, retries: config.verify?.retries ?? 1, requireCriteria: config.verify?.requireCriteria ?? false },
|
|
837
|
+
design: design ? { max: config.design?.max } : false,
|
|
838
|
+
perf: perf ? { command: config.perf?.command, retries: config.perf?.retries ?? 1 } : false,
|
|
839
|
+
audit: audit ? config.audit : false,
|
|
840
|
+
completion: completion ? { command: config.completion?.command, retries: config.completion?.retries ?? 1 } : false,
|
|
841
|
+
quality: quality ? { defaults: config.quality, overrides: qualityOverrides, limits: quality.repairLimits, runnerAgent, runner: config.runner, agents: config.agents } : false,
|
|
842
|
+
review: review ? { provider: reviewProvider, explicitProvider: opts.reviewer, eligibleProviders: reviewerProviders, allowSelfReview: opts.allowSelfReview ?? false } : false,
|
|
843
|
+
roleRouting: routingEnabled && (quality || review) ? config.routing : false,
|
|
844
|
+
permissions: { implementation: permissions, orchestrator: 'read-only', reviewer: 'read-only', critic: 'read-only', repair: 'safe' },
|
|
845
|
+
idleMs, ambiguityPolicy,
|
|
846
|
+
}) : undefined;
|
|
755
847
|
let runner = opts.runner;
|
|
756
848
|
if (!runner) {
|
|
757
849
|
const requiredProviders = useParallelDispatcher
|
|
@@ -783,6 +875,9 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
783
875
|
strategy: config.routing.strategy,
|
|
784
876
|
maxCandidates: config.routing.maxCandidates,
|
|
785
877
|
maxAttempts: config.routing.maxAttempts,
|
|
878
|
+
optimization: config.routing.optimization,
|
|
879
|
+
accountingScope,
|
|
880
|
+
executionPolicyKey,
|
|
786
881
|
planner: resolvePlanner(config, runnerAgent, runnerSelection),
|
|
787
882
|
assessmentPolicy: config.routing.assessmentPolicy,
|
|
788
883
|
fallback: config.routing.fallback,
|
|
@@ -805,50 +900,6 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
805
900
|
}
|
|
806
901
|
runner = makeActionRunner(config.actions, runner);
|
|
807
902
|
}
|
|
808
|
-
let review = opts.reviewRunner;
|
|
809
|
-
let reviewProvider = 'unknown';
|
|
810
|
-
if (!review && (opts.review || opts.reviewer)) {
|
|
811
|
-
const reviewerAgent = opts.reviewer ?? SUPPORTED_AGENTS.find(agent => agent !== runnerAgent && available(agent));
|
|
812
|
-
if (!reviewerAgent) {
|
|
813
|
-
if (!opts.allowSelfReview) {
|
|
814
|
-
console.error('No independent reviewer CLI is available. Install or select a second agent, or pass --allow-self-review explicitly.');
|
|
815
|
-
return 2;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
const resolvedReviewer = reviewerAgent ?? runnerAgent;
|
|
819
|
-
reviewProvider = resolvedReviewer;
|
|
820
|
-
if (resolvedReviewer === runnerAgent && !opts.allowSelfReview) {
|
|
821
|
-
console.error(`Reviewer "${resolvedReviewer}" is also the implementer. Pick another agent or pass --allow-self-review explicitly.`);
|
|
822
|
-
return 2;
|
|
823
|
-
}
|
|
824
|
-
if (!available(resolvedReviewer)) {
|
|
825
|
-
console.error(`Reviewer agent CLI "${resolvedReviewer}" was not found on PATH. Install it, or pick another with --reviewer=<${AGENT_LIST}>.`);
|
|
826
|
-
return 2;
|
|
827
|
-
}
|
|
828
|
-
review = context => {
|
|
829
|
-
const implementer = readStatus(targetDir)?.routingDecisions?.[context.story.id]?.provider ?? runnerAgent;
|
|
830
|
-
const selectedReviewer = !opts.reviewer && resolvedReviewer === implementer
|
|
831
|
-
? SUPPORTED_AGENTS.find(agent => agent !== implementer && available(agent)) ?? resolvedReviewer : resolvedReviewer;
|
|
832
|
-
if (selectedReviewer === implementer && !opts.allowSelfReview)
|
|
833
|
-
return { success: false, summary: "Independent review requires a provider distinct from the routed implementer", reviewOutcome: { kind: "infrastructure", summary: "Routed implementation and reviewer share a provider" } };
|
|
834
|
-
reviewProvider = selectedReviewer;
|
|
835
|
-
return makeReviewRunner(selectedReviewer, idleMs, undefined, routingEnabled ? roleSelection(targetDir, config, context.story, selectedReviewer, "reviewer") : undefined)(context);
|
|
836
|
-
};
|
|
837
|
-
}
|
|
838
|
-
if (review) {
|
|
839
|
-
const reviewRunner = review;
|
|
840
|
-
review = context => {
|
|
841
|
-
const started = Date.now();
|
|
842
|
-
let result;
|
|
843
|
-
try {
|
|
844
|
-
result = reviewRunner(context);
|
|
845
|
-
return result;
|
|
846
|
-
}
|
|
847
|
-
finally {
|
|
848
|
-
executionReporter?.addTokens({ inputTokens: 0, outputTokens: 0, measurementComplete: result?.tokens !== undefined, ...result?.tokens, provider: reviewProvider, role: 'reviewer', storyId: context.story.id, durationMs: Date.now() - started });
|
|
849
|
-
}
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
903
|
if (!useParallelDispatcher) {
|
|
853
904
|
if (runner) {
|
|
854
905
|
const unpooled = runner;
|
|
@@ -951,6 +1002,8 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
951
1002
|
providers: parallelProviders,
|
|
952
1003
|
affinityProviders: parallelAffinityProviders,
|
|
953
1004
|
routing: routingEnabled ? config.routing : undefined,
|
|
1005
|
+
accountingScope,
|
|
1006
|
+
executionPolicyKey,
|
|
954
1007
|
planning: config.planning,
|
|
955
1008
|
isAvailable: available,
|
|
956
1009
|
onAmbiguity: ambiguityPolicy,
|
|
@@ -976,6 +1029,7 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
976
1029
|
prdPath: path,
|
|
977
1030
|
targetDir,
|
|
978
1031
|
runner,
|
|
1032
|
+
feedback: opts.recoveryFeedback,
|
|
979
1033
|
git,
|
|
980
1034
|
commitIdentity,
|
|
981
1035
|
verify,
|
package/dist/loop/runner.js
CHANGED
|
@@ -9,6 +9,7 @@ import { loadContext, formatForPrompt, contextDir } from '../context/context.js'
|
|
|
9
9
|
import { contextPacket } from '../context/packet.js';
|
|
10
10
|
import { buildProviderInvocation, startProviderProcess } from '../agents/providers.js';
|
|
11
11
|
import { parseProviderResult, parseProviderTelemetry } from '../agents/telemetry.js';
|
|
12
|
+
import { providerTelemetryUsage } from '../observability/usage.js';
|
|
12
13
|
import { formatReviewContract, formatReviewStdoutContract, parseReviewVerdict } from '../review/verdict.js';
|
|
13
14
|
import { prepareWindowsInvocation } from '../agents/windows-launch.js';
|
|
14
15
|
import { readSupervision } from '../agents/supervision.js';
|
|
@@ -31,7 +32,7 @@ export function buildClaudePrompt(story, context, onAmbiguity = 'resolve', perfC
|
|
|
31
32
|
];
|
|
32
33
|
if (context)
|
|
33
34
|
lines.push('', context);
|
|
34
|
-
lines.push('', `Story ${story.id}: ${story.title}`, 'Acceptance criteria (Definition of Done):', criteria, ...(story.assessment ? ['Planner approach:', story.assessment.approach] : []), '', "When done, ensure the project's full test suite passes.", 'Do NOT commit — the loop commits on your behalf after verifying.', '', 'Working rules:', '- Add nothing beyond what the story requires: no extra features, abstractions, comments, or defensive code for cases that cannot happen.', '- Do not create summary, plan, or analysis documents — only files the story itself needs.', '- If a check fails, fix the root cause; never bypass it (e.g. --no-verify) or pass by weakening tests.', '- Report the outcome faithfully: if a criterion is unmet or tests fail, say so plainly instead of claiming success.', '- Never ask questions or wait for input — you run unattended and nobody can answer.', onAmbiguity === 'abort'
|
|
35
|
+
lines.push('', `Story ${story.id}: ${story.title}`, 'Acceptance criteria (Definition of Done):', criteria, ...(story.assessment ? ['Planner approach:', story.assessment.approach] : []), '', "When done, ensure the project's full test suite passes.", 'Do NOT commit — the loop commits on your behalf after verifying.', '', 'Working rules:', '- Use worktree-local writable dependency/runtime caches. Do not link node_modules to a target checkout when tools write .vite-temp or other caches there. Shared package download caches are allowed; preserve the configured sandbox. Report offline cache misses and network failures explicitly instead of blindly retrying.', '- Add nothing beyond what the story requires: no extra features, abstractions, comments, or defensive code for cases that cannot happen.', '- Do not create summary, plan, or analysis documents — only files the story itself needs.', '- If a check fails, fix the root cause; never bypass it (e.g. --no-verify) or pass by weakening tests.', '- Report the outcome faithfully: if a criterion is unmet or tests fail, say so plainly instead of claiming success.', '- Never ask questions or wait for input — you run unattended and nobody can answer.', onAmbiguity === 'abort'
|
|
35
36
|
? '- If an acceptance criterion is genuinely undecidable, do NOT guess: write the open question(s) to .yoke/ambiguity.md, change nothing else, and stop.'
|
|
36
37
|
: onAmbiguity === 'critical'
|
|
37
38
|
? [
|
|
@@ -243,7 +244,7 @@ function processFailureSummary(error) {
|
|
|
243
244
|
export function runCapturedAgent(agent, inv) {
|
|
244
245
|
try {
|
|
245
246
|
const output = runCliCapture(inv);
|
|
246
|
-
return { success: true, output, summary: 'exited 0', tokens: parseProviderTelemetry(agent, output.split(/\r?\n/))
|
|
247
|
+
return { success: true, output, summary: 'exited 0', tokens: providerTelemetryUsage(parseProviderTelemetry(agent, output.split(/\r?\n/))) };
|
|
247
248
|
}
|
|
248
249
|
catch (error) {
|
|
249
250
|
const partial = error.stdout;
|
|
@@ -252,7 +253,7 @@ export function runCapturedAgent(agent, inv) {
|
|
|
252
253
|
success: false,
|
|
253
254
|
output,
|
|
254
255
|
summary: error.message,
|
|
255
|
-
tokens: output ? parseProviderTelemetry(agent, output.split(/\r?\n/))
|
|
256
|
+
tokens: output ? providerTelemetryUsage(parseProviderTelemetry(agent, output.split(/\r?\n/))) : undefined,
|
|
256
257
|
};
|
|
257
258
|
}
|
|
258
259
|
}
|
|
@@ -316,12 +317,12 @@ export function makeRunner(agent, idleTimeoutMs = 0, opts = {}) {
|
|
|
316
317
|
try {
|
|
317
318
|
const out = capture(inv);
|
|
318
319
|
const telemetry = parseProviderTelemetry(agent, out.split(/\r?\n/));
|
|
319
|
-
return { success: true, summary: `${agent} implemented ${ctx.story.id}`, tokens: attributed(telemetry
|
|
320
|
+
return { success: true, summary: `${agent} implemented ${ctx.story.id}`, tokens: attributed(providerTelemetryUsage(telemetry)) };
|
|
320
321
|
}
|
|
321
322
|
catch (e) {
|
|
322
323
|
// Salvage usage from whatever the agent streamed before dying — those tokens were spent.
|
|
323
324
|
const partial = e.stdout;
|
|
324
|
-
const tokens = partial == null ? undefined : parseProviderTelemetry(agent, String(partial).split(/\r?\n/))
|
|
325
|
+
const tokens = partial == null ? undefined : providerTelemetryUsage(parseProviderTelemetry(agent, String(partial).split(/\r?\n/)));
|
|
325
326
|
const reason = readSupervision(ctx.targetDir, new Date(started).toISOString())[0]?.reason;
|
|
326
327
|
return { success: false, infrastructureFailure: true, summary: `${agent} failed on ${ctx.story.id}: ${reason ?? e.message}`, tokens: attributed(tokens) };
|
|
327
328
|
}
|