@hecer/yoke 1.20.0 → 1.22.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 +6 -1
- 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/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 +86 -8
- package/dist/code-intelligence/budgets.js +138 -0
- package/dist/code-intelligence/contracts.js +2 -0
- package/dist/code-intelligence/coordinator.js +156 -84
- package/dist/code-intelligence/evidence.js +87 -34
- package/dist/code-intelligence/mcp-client.js +10 -2
- package/dist/code-intelligence/mcp-server.js +10 -10
- package/dist/dashboard/analytics.js +5 -3
- package/dist/goals/command.js +183 -53
- package/dist/goals/usage.js +87 -0
- package/dist/loop/candidate-cleanup.js +47 -17
- package/dist/loop/candidates.js +17 -11
- package/dist/loop/cleanup.js +100 -0
- 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 +100 -66
- package/dist/loop/parallel-adapters.js +22 -4
- package/dist/loop/parallel-command.js +32 -5
- package/dist/loop/recovery.js +23 -5
- package/dist/loop/reporter.js +21 -4
- package/dist/loop/run-command.js +102 -48
- package/dist/loop/runner.js +5 -4
- package/dist/loop/worker.js +145 -91
- package/dist/observability/history.js +1 -1
- package/dist/observability/invocation.js +42 -0
- package/dist/observability/usage.js +17 -0
- package/dist/prd/command.js +20 -7
- package/dist/prd/decompose.js +5 -2
- package/dist/retrofit/command.js +22 -0
- package/dist/retrofit/config.js +26 -1
- package/dist/retrofit/gitignore.js +2 -0
- package/dist/retrofit/planners/claude.js +4 -4
- package/dist/retrofit/wsl.js +23 -3
- 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 +280 -127
- package/dist/setup/command.js +54 -6
- package/dist/smoke/command.js +302 -74
- 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 +199 -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/parallel-execution.md +37 -9
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
package/dist/loop/worker.js
CHANGED
|
@@ -3,6 +3,9 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { acceptanceProtectionProblem } from "../check/command.js";
|
|
5
5
|
import { isAcceptanceCriterion } from './prd.js';
|
|
6
|
+
import { observeFailure } from './failure.js';
|
|
7
|
+
import { consumeAmbiguity } from './loop.js';
|
|
8
|
+
import { gateIdentity, snapshotGates, reuseGates } from './gate-snapshot.js';
|
|
6
9
|
import { runQualityRepairLoop } from '../quality/loop.js';
|
|
7
10
|
function emptyEvidence() { return { criteria: [] }; }
|
|
8
11
|
function errorMessage(error) {
|
|
@@ -40,6 +43,7 @@ function reviewOutcome(result) {
|
|
|
40
43
|
return { kind: 'malformed', summary: result.summary };
|
|
41
44
|
}
|
|
42
45
|
function runMechanicalGates(input, context, evidence) {
|
|
46
|
+
evidence.criteria = [];
|
|
43
47
|
const criteria = context.story.acceptance.filter(isAcceptanceCriterion);
|
|
44
48
|
if (criteria.length === 0) {
|
|
45
49
|
if (input.requireCriterionEvidence)
|
|
@@ -170,8 +174,13 @@ export async function runStoryWorker(input) {
|
|
|
170
174
|
return finalResult(input, { ...baseResult(input, evidence, preflight.summary), kind: 'mechanical-failure', stage: 'quality' });
|
|
171
175
|
}
|
|
172
176
|
let implementation;
|
|
177
|
+
let verified;
|
|
178
|
+
let failure;
|
|
173
179
|
try {
|
|
174
|
-
|
|
180
|
+
const outcome = await runWorkerImplementation(input, context, evidence);
|
|
181
|
+
implementation = outcome.result;
|
|
182
|
+
verified = outcome.gates;
|
|
183
|
+
failure = outcome.failure;
|
|
175
184
|
}
|
|
176
185
|
catch (error) {
|
|
177
186
|
return finalResult(input, {
|
|
@@ -185,110 +194,155 @@ export async function runStoryWorker(input) {
|
|
|
185
194
|
if (implementation.infrastructureFailure)
|
|
186
195
|
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
187
196
|
if (implementation.infrastructureFailure || implementation.routing?.blocked)
|
|
188
|
-
return finalResult(input, { ...baseResult(input, evidence, implementation.summary), kind: "mechanical-failure", stage: "implementation" });
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
...baseResult(input, evidence,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
197
|
+
return finalResult(input, { ...baseResult(input, evidence, implementation.summary), kind: "mechanical-failure", stage: "implementation", ...(failure ? { failure } : {}) });
|
|
198
|
+
try {
|
|
199
|
+
const afterImplementationCancellation = cancellationReason(input.cancellation);
|
|
200
|
+
if (afterImplementationCancellation) {
|
|
201
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
202
|
+
return finalResult(input, { ...baseResult(input, evidence, afterImplementationCancellation), kind: 'cancelled' });
|
|
203
|
+
}
|
|
204
|
+
const ambiguity = consumeAmbiguity(context.targetDir);
|
|
205
|
+
if (ambiguity) {
|
|
206
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
207
|
+
return finalResult(input, {
|
|
208
|
+
...baseResult(input, evidence, `story ${context.story.id} stopped: ambiguous acceptance criteria — ${ambiguity}`),
|
|
209
|
+
kind: 'paused', reason: 'ambiguity',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const decision = input.beforeGates?.(context);
|
|
213
|
+
if (decision) {
|
|
214
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
215
|
+
return finalResult(input, { ...baseResult(input, evidence, decision), kind: 'paused', reason: 'decision' });
|
|
216
|
+
}
|
|
217
|
+
if (input.pause?.()) {
|
|
218
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
219
|
+
return finalResult(input, { ...baseResult(input, evidence, 'worker paused before acceptance checks'), kind: 'paused' });
|
|
220
|
+
}
|
|
221
|
+
const gates = reuseGates(context.targetDir, context.story, verified) ?? runMechanicalGates(input, context, evidence);
|
|
222
|
+
if (gates.kind === 'cancelled') {
|
|
223
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
224
|
+
return finalResult(input, { ...baseResult(input, evidence, gates.summary), kind: 'cancelled' });
|
|
225
|
+
}
|
|
226
|
+
if (gates.kind === 'failed') {
|
|
227
|
+
implementation.routing?.recordOutcome(false);
|
|
228
|
+
const observed = input.failureRoot ? observeFailure({ root: input.failureRoot, scope: input.failureScope, directory: context.targetDir, story: context.story, stage: gates.stage, summary: gates.summary }) : undefined;
|
|
229
|
+
return finalResult(input, { ...baseResult(input, evidence, observed?.feedback ?? gates.summary), kind: 'mechanical-failure', stage: gates.stage, ...(observed ? { failure: observed.failure } : {}) });
|
|
230
|
+
}
|
|
231
|
+
const summary = implementation.success
|
|
232
|
+
? implementation.summary
|
|
233
|
+
: `${implementation.summary} (runner exited non-zero but verify is green)`;
|
|
234
|
+
if (!qualityEnabled && !input.review) {
|
|
235
|
+
return finalResult(input, {
|
|
236
|
+
...baseResult(input, evidence, summary),
|
|
237
|
+
kind: 'candidate',
|
|
238
|
+
routing: { outcome: 'pending-integration', ...(implementation.routing ? { recordOutcome: implementation.routing.recordOutcome } : {}) },
|
|
239
|
+
...(implementation.tokens ? { tokens: implementation.tokens } : {}),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const beforeQualityCancellation = cancellationReason(input.cancellation);
|
|
243
|
+
if (beforeQualityCancellation) {
|
|
244
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
245
|
+
return finalResult(input, { ...baseResult(input, evidence, beforeQualityCancellation), kind: 'cancelled' });
|
|
246
|
+
}
|
|
247
|
+
const qualityStage = qualityEnabled ? input.qualityStage : undefined;
|
|
248
|
+
const review = input.review;
|
|
249
|
+
let qualityFailure;
|
|
250
|
+
const quality = runQualityRepairLoop({
|
|
251
|
+
...(qualityStage
|
|
252
|
+
? { quality: (round) => {
|
|
253
|
+
input.reporter?.phase('comparing');
|
|
254
|
+
return qualityStage(context, round);
|
|
255
|
+
} }
|
|
256
|
+
: {}),
|
|
257
|
+
...(review
|
|
258
|
+
? { review: () => {
|
|
259
|
+
input.reporter?.phase('reviewing');
|
|
260
|
+
return reviewOutcome(review(context));
|
|
261
|
+
} }
|
|
262
|
+
: {}),
|
|
263
|
+
repair: request => {
|
|
264
|
+
if (!input.repair)
|
|
265
|
+
return { kind: 'blocked', summary: 'repair callback is not configured' };
|
|
266
|
+
input.reporter?.phase('repairing');
|
|
267
|
+
const observed = input.failureRoot ? observeFailure({ root: input.failureRoot, scope: input.failureScope, directory: context.targetDir, story: context.story, stage: 'quality', summary: JSON.stringify(request.finding) }) : undefined;
|
|
268
|
+
if (observed?.action === 'blocked') {
|
|
269
|
+
qualityFailure = observed.failure;
|
|
270
|
+
return { kind: 'blocked', summary: observed.feedback };
|
|
271
|
+
}
|
|
272
|
+
const targetedRequest = observed?.action === 'diagnose' ? { ...request, finding: { ...request.finding, message: observed.feedback } } : request;
|
|
273
|
+
const repair = input.repair(context, targetedRequest);
|
|
274
|
+
return repair.success ? { kind: 'repaired' } : { kind: 'blocked', summary: repair.summary };
|
|
275
|
+
},
|
|
276
|
+
rerunGates: () => {
|
|
277
|
+
const rerun = runMechanicalGates(input, context, evidence);
|
|
278
|
+
if (rerun.kind === 'passed')
|
|
279
|
+
return { kind: 'passed' };
|
|
280
|
+
if (rerun.kind === 'cancelled')
|
|
281
|
+
return rerun;
|
|
282
|
+
return { kind: 'failed', stage: rerun.stage, summary: rerun.summary };
|
|
283
|
+
},
|
|
284
|
+
...(input.repairLimits ? { limits: input.repairLimits } : {}),
|
|
285
|
+
pause: input.pause,
|
|
286
|
+
onStatus: status => {
|
|
287
|
+
const metadata = input.qualityMetadata?.(context);
|
|
288
|
+
input.reporter?.quality({ ...status, ...(metadata ?? { policy: 'blocking' }) });
|
|
289
|
+
},
|
|
214
290
|
});
|
|
291
|
+
evidence.quality = quality;
|
|
292
|
+
const afterQualityCancellation = cancellationReason(input.cancellation);
|
|
293
|
+
if (afterQualityCancellation) {
|
|
294
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
295
|
+
return finalResult(input, { ...baseResult(input, evidence, afterQualityCancellation), kind: 'cancelled' });
|
|
296
|
+
}
|
|
297
|
+
const result = { ...resultFromQuality(input, evidence, summary, quality), ...(qualityFailure ? { failure: qualityFailure } : {}) };
|
|
298
|
+
if (result.kind === 'paused' || result.kind === 'cancelled' || result.kind === 'review-failure' || result.kind === 'quality-failure')
|
|
299
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
300
|
+
else if (result.kind !== 'candidate')
|
|
301
|
+
implementation.routing?.recordOutcome(false);
|
|
302
|
+
if (result.kind === 'candidate') {
|
|
303
|
+
return finalResult(input, {
|
|
304
|
+
...result,
|
|
305
|
+
routing: { outcome: 'pending-integration', ...(implementation.routing ? { recordOutcome: implementation.routing.recordOutcome } : {}) },
|
|
306
|
+
...(implementation.tokens ? { tokens: implementation.tokens } : {}),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return finalResult(input, result);
|
|
215
310
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
const qualityStage = qualityEnabled ? input.qualityStage : undefined;
|
|
221
|
-
const review = input.review;
|
|
222
|
-
const quality = runQualityRepairLoop({
|
|
223
|
-
...(qualityStage
|
|
224
|
-
? { quality: (round) => {
|
|
225
|
-
input.reporter?.phase('comparing');
|
|
226
|
-
return qualityStage(context, round);
|
|
227
|
-
} }
|
|
228
|
-
: {}),
|
|
229
|
-
...(review
|
|
230
|
-
? { review: () => {
|
|
231
|
-
input.reporter?.phase('reviewing');
|
|
232
|
-
return reviewOutcome(review(context));
|
|
233
|
-
} }
|
|
234
|
-
: {}),
|
|
235
|
-
repair: request => {
|
|
236
|
-
if (!input.repair)
|
|
237
|
-
return { kind: 'blocked', summary: 'repair callback is not configured' };
|
|
238
|
-
input.reporter?.phase('repairing');
|
|
239
|
-
const repair = input.repair(context, request);
|
|
240
|
-
return repair.success ? { kind: 'repaired' } : { kind: 'blocked', summary: repair.summary };
|
|
241
|
-
},
|
|
242
|
-
rerunGates: () => {
|
|
243
|
-
const rerun = runMechanicalGates(input, context, evidence);
|
|
244
|
-
if (rerun.kind === 'passed')
|
|
245
|
-
return { kind: 'passed' };
|
|
246
|
-
if (rerun.kind === 'cancelled')
|
|
247
|
-
return rerun;
|
|
248
|
-
return { kind: 'failed', stage: rerun.stage, summary: rerun.summary };
|
|
249
|
-
},
|
|
250
|
-
...(input.repairLimits ? { limits: input.repairLimits } : {}),
|
|
251
|
-
pause: input.pause,
|
|
252
|
-
onStatus: status => {
|
|
253
|
-
const metadata = input.qualityMetadata?.(context);
|
|
254
|
-
input.reporter?.quality({ ...status, ...(metadata ?? { policy: 'blocking' }) });
|
|
255
|
-
},
|
|
256
|
-
});
|
|
257
|
-
evidence.quality = quality;
|
|
258
|
-
const afterQualityCancellation = cancellationReason(input.cancellation);
|
|
259
|
-
if (afterQualityCancellation) {
|
|
260
|
-
return finalResult(input, { ...baseResult(input, evidence, afterQualityCancellation), kind: 'cancelled' });
|
|
261
|
-
}
|
|
262
|
-
const result = resultFromQuality(input, evidence, summary, quality);
|
|
263
|
-
if (result.kind !== 'candidate' && result.kind !== 'paused')
|
|
264
|
-
implementation.routing?.recordOutcome(false);
|
|
265
|
-
if (result.kind === 'candidate') {
|
|
266
|
-
return finalResult(input, {
|
|
267
|
-
...result,
|
|
268
|
-
routing: { outcome: 'pending-integration', ...(implementation.routing ? { recordOutcome: implementation.routing.recordOutcome } : {}) },
|
|
269
|
-
...(implementation.tokens ? { tokens: implementation.tokens } : {}),
|
|
270
|
-
});
|
|
311
|
+
catch (error) {
|
|
312
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
313
|
+
throw error;
|
|
271
314
|
}
|
|
272
|
-
return finalResult(input, result);
|
|
273
315
|
}
|
|
274
316
|
async function runWorkerImplementation(input, context, evidence) {
|
|
275
|
-
let feedback;
|
|
317
|
+
let feedback = input.feedback;
|
|
276
318
|
for (let attempt = 0;; attempt++) {
|
|
277
319
|
const result = await input.runner({ ...context, feedback });
|
|
278
320
|
if (!result.routing?.canRetry || result.routing.blocked || attempt >= 7 || cancellationReason(input.cancellation) || input.pause?.())
|
|
279
|
-
return result;
|
|
321
|
+
return { result };
|
|
280
322
|
if (["decision-request.yaml", "ambiguity.md", "loop.pause"].some(name => existsSync(join(context.targetDir, ".yoke", name))) || acceptanceProtectionProblem(context.targetDir))
|
|
281
|
-
return result;
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
323
|
+
return { result };
|
|
324
|
+
const before = gateIdentity(context.targetDir, context.story);
|
|
325
|
+
let gates;
|
|
326
|
+
try {
|
|
327
|
+
gates = runMechanicalGates(input, context, evidence);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
result.routing.recordOutcome(false, 'infrastructure');
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
if (gates.kind !== 'failed') {
|
|
334
|
+
return { result, ...(gates.kind === 'passed' ? { gates: snapshotGates(context.targetDir, context.story, before, gates) } : {}) };
|
|
335
|
+
}
|
|
285
336
|
if (knownInfrastructureFailure(gates.summary)) {
|
|
286
337
|
result.routing.recordOutcome(false, 'infrastructure');
|
|
287
|
-
return { ...result, success: false, infrastructureFailure: true, summary: gates.summary, routing: { ...result.routing, blocked: true, canRetry: false } };
|
|
338
|
+
return { result: { ...result, success: false, infrastructureFailure: true, summary: gates.summary, routing: { ...result.routing, blocked: true, canRetry: false } } };
|
|
288
339
|
}
|
|
289
340
|
result.routing.recordOutcome(false);
|
|
341
|
+
const observed = input.failureRoot ? observeFailure({ root: input.failureRoot, scope: input.failureScope, directory: context.targetDir, story: context.story, stage: gates.stage, summary: gates.summary }) : undefined;
|
|
342
|
+
if (observed?.action === 'blocked')
|
|
343
|
+
return { result: { ...result, success: false, summary: observed.feedback, routing: { ...result.routing, blocked: true, canRetry: false } }, failure: observed.failure };
|
|
290
344
|
if (result.tokens)
|
|
291
345
|
input.reporter?.addTokens(result.tokens);
|
|
292
|
-
feedback = gates.summary;
|
|
346
|
+
feedback = observed?.feedback ?? gates.summary;
|
|
293
347
|
}
|
|
294
348
|
}
|
|
@@ -30,7 +30,7 @@ export function archiveMeasurement(root, event) {
|
|
|
30
30
|
throw error;
|
|
31
31
|
}
|
|
32
32
|
const data = event.data ?? {};
|
|
33
|
-
const allowed = ['inputTokens', 'outputTokens', 'cachedInputTokens', 'cacheWriteInputTokens', 'reasoningOutputTokens', 'totalCostUsd', 'agent', 'provider', 'model', 'actualModel', 'requestedModel', 'variant', 'role', 'calls', 'measurementComplete', 'costMeasurementComplete', 'usageAvailable', 'prediction', 'errorMs', 'withinObservedRange', 'escalated'];
|
|
33
|
+
const allowed = ['inputTokens', 'outputTokens', 'cachedInputTokens', 'cacheWriteInputTokens', 'reasoningOutputTokens', 'totalCostUsd', 'agent', 'provider', 'model', 'actualModel', 'requestedProvider', 'requestedModel', 'requestedReasoningEffort', 'requestedVariant', 'variant', 'role', 'callId', 'parentCallId', 'calls', 'measurementComplete', 'costMeasurementComplete', 'usageAvailable', 'prediction', 'errorMs', 'withinObservedRange', 'escalated'];
|
|
34
34
|
const compact = { ...event, data: Object.fromEntries(allowed.filter(key => data[key] !== undefined).map(key => [key, data[key]])) };
|
|
35
35
|
appendFileSync(file, JSON.stringify(compact) + '\n');
|
|
36
36
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { parseProviderTelemetry } from '../agents/telemetry.js';
|
|
3
|
+
import { appendEvent } from './events.js';
|
|
4
|
+
import { providerTelemetryUsage } from './usage.js';
|
|
5
|
+
/** A single invocation boundary, including failures and injected runner seams. */
|
|
6
|
+
export function measureInvocation(options) {
|
|
7
|
+
const started = Date.now(), callId = randomUUID(), runId = options.runId ?? randomUUID();
|
|
8
|
+
let result;
|
|
9
|
+
let failure;
|
|
10
|
+
const base = { runId, storyId: options.storyId, attemptId: callId, agent: options.agent, role: options.role };
|
|
11
|
+
appendEvent(options.root, { ...base, timestamp: new Date(started).toISOString(), type: 'status', data: { callId, parentCallId: options.parentCallId, state: 'started' } });
|
|
12
|
+
try {
|
|
13
|
+
result = options.execute(options.invocation);
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
failure = error;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
let salvaged;
|
|
22
|
+
try {
|
|
23
|
+
const stdout = failure && typeof failure === 'object' && 'stdout' in failure ? failure.stdout : undefined;
|
|
24
|
+
salvaged = stdout == null ? undefined : providerTelemetryUsage(parseProviderTelemetry(options.agent, String(stdout).split(/\r?\n/u)));
|
|
25
|
+
}
|
|
26
|
+
catch { /* Failed measurement must not replace the original provider error. */ }
|
|
27
|
+
const usage = result?.tokens ?? salvaged;
|
|
28
|
+
const complete = usage !== undefined && usage.measurementComplete !== false;
|
|
29
|
+
const durationMs = Date.now() - started;
|
|
30
|
+
appendEvent(options.root, {
|
|
31
|
+
...base, timestamp: new Date().toISOString(), type: 'tokens', durationMs, outcome: result?.success ? 'succeeded' : 'failed',
|
|
32
|
+
data: {
|
|
33
|
+
...usage, callId, parentCallId: options.parentCallId,
|
|
34
|
+
provider: options.agent, role: options.role,
|
|
35
|
+
requestedProvider: options.selection?.provider, requestedModel: options.selection?.model,
|
|
36
|
+
requestedReasoningEffort: options.selection?.reasoningEffort, requestedVariant: options.selection?.variant,
|
|
37
|
+
actualModel: usage?.model, measurementComplete: complete, usageAvailable: complete,
|
|
38
|
+
costMeasurementComplete: usage?.costMeasurementComplete ?? (typeof usage?.totalCostUsd === 'number'),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Preserve known lower bounds; missing telemetry must never become a free call. */
|
|
2
|
+
export function providerTelemetryUsage(telemetry) {
|
|
3
|
+
if (!telemetry.tokens && !telemetry.partialUsage)
|
|
4
|
+
return undefined;
|
|
5
|
+
const known = { ...telemetry.partialUsage, ...telemetry.tokens };
|
|
6
|
+
if (!Object.values(known).some(value => typeof value === 'number'))
|
|
7
|
+
return undefined;
|
|
8
|
+
const model = known.model ?? (telemetry.reportedModels?.length === 1 ? telemetry.reportedModels[0] : undefined);
|
|
9
|
+
return {
|
|
10
|
+
...known,
|
|
11
|
+
...(model ? { model } : {}),
|
|
12
|
+
inputTokens: known.inputTokens ?? 0,
|
|
13
|
+
outputTokens: known.outputTokens ?? 0,
|
|
14
|
+
measurementComplete: telemetry.usageAvailable,
|
|
15
|
+
costMeasurementComplete: typeof telemetry.tokens?.totalCostUsd === 'number',
|
|
16
|
+
};
|
|
17
|
+
}
|
package/dist/prd/command.js
CHANGED
|
@@ -7,9 +7,11 @@ import { bindAssessments, preparedProblems } from './assess.js';
|
|
|
7
7
|
import { resolvePlanner } from '../routing/planning.js';
|
|
8
8
|
import { readPlanningFile } from '../routing/contracts.js';
|
|
9
9
|
import { acquireLock, releaseLock } from '../loop/lock.js';
|
|
10
|
-
import { agentInvocation, buildWatchdogInvocation,
|
|
10
|
+
import { agentInvocation, buildWatchdogInvocation, runCapturedAgent, isAgentAvailable, } from '../loop/runner.js';
|
|
11
11
|
import { resolveIdleMs } from '../loop/run-command.js';
|
|
12
12
|
import { detectHostAgent, resolveRunnerAgent } from '../agents/host.js';
|
|
13
|
+
import { measureInvocation } from '../observability/invocation.js';
|
|
14
|
+
import { statePath } from '../workspace/state.js';
|
|
13
15
|
export const PRD_TEMPLATE = `# Yoke PRD — the loop picks the lowest-priority open story each iteration.
|
|
14
16
|
# Story format (see canon/loop/prd.schema.md):
|
|
15
17
|
# - id: STORY-1
|
|
@@ -92,14 +94,25 @@ export function runPrdDraft(targetDir, opts) {
|
|
|
92
94
|
}
|
|
93
95
|
try {
|
|
94
96
|
const before = readPlanningFile(targetDir, '.yoke/prd.yaml');
|
|
95
|
-
const rollback = () => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
const rollback = () => {
|
|
98
|
+
const destination = join(statePath(targetDir), 'prd.yaml');
|
|
99
|
+
// Unlink a provider-created file link instead of writing through it.
|
|
100
|
+
rmSync(destination, { force: true });
|
|
101
|
+
if (before !== undefined)
|
|
102
|
+
writeFileSync(destination, before, { flag: 'wx' });
|
|
103
|
+
};
|
|
99
104
|
const inv = agentInvocation(agent, buildPrdDraftPrompt(idea, planningBrief), targetDir, 'safe', planner.selection);
|
|
100
105
|
console.log(`Drafting PRD with ${agent}...`);
|
|
101
|
-
const run = opts.run ?? ((i) =>
|
|
102
|
-
|
|
106
|
+
const run = opts.run ?? ((i) => runCapturedAgent(agent, buildWatchdogInvocation(i, idleMs)));
|
|
107
|
+
let result;
|
|
108
|
+
try {
|
|
109
|
+
result = measureInvocation({ root: targetDir, agent, role: 'prd-draft', selection: planner.selection, invocation: inv, execute: run });
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
rollback();
|
|
113
|
+
console.error(`PRD draft failed: ${error.message}`);
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
103
116
|
if (!result.success) {
|
|
104
117
|
rollback();
|
|
105
118
|
console.error(`PRD draft failed: ${result.summary}`);
|
package/dist/prd/decompose.js
CHANGED
|
@@ -11,7 +11,8 @@ import { writeScopesOverlap, validWriteScope } from '../loop/scheduler.js';
|
|
|
11
11
|
import { withSharedWorkerSync } from '../loop/resource-pool.js';
|
|
12
12
|
import { loadConfig } from '../retrofit/config.js';
|
|
13
13
|
import { detectHostAgent, resolveRunnerAgent } from '../agents/host.js';
|
|
14
|
-
import { agentInvocation, buildWatchdogInvocation, isAgentAvailable,
|
|
14
|
+
import { agentInvocation, buildWatchdogInvocation, isAgentAvailable, runCapturedAgent } from '../loop/runner.js';
|
|
15
|
+
import { measureInvocation } from '../observability/invocation.js';
|
|
15
16
|
const ProposalSchema = z.array(z.object({
|
|
16
17
|
id: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/u),
|
|
17
18
|
title: z.string().min(1).max(300),
|
|
@@ -181,7 +182,9 @@ export function runPrdDecompose(root, options) {
|
|
|
181
182
|
if (prompt.length > 60_000)
|
|
182
183
|
throw Error('Planning input exceeds 60000 characters; condense .yoke/plan.md first');
|
|
183
184
|
const invocation = agentInvocation(planner.agent, prompt, root, 'safe', planner.selection);
|
|
184
|
-
const result = withSharedWorkerSync({ targetDir: root, storyId: `prd-decompose:${parent.id}`, provider: planner.agent, role: 'implementation' }, () => (
|
|
185
|
+
const result = withSharedWorkerSync({ targetDir: root, storyId: `prd-decompose:${parent.id}`, provider: planner.agent, role: 'implementation' }, () => measureInvocation({ root, agent: planner.agent, role: 'prd-decompose', storyId: parent.id, selection: planner.selection, invocation,
|
|
186
|
+
execute: options.run ?? (item => runCapturedAgent(planner.agent, buildWatchdogInvocation(item, options.timeoutMinutes === undefined ? 20 * 60_000 : options.timeoutMinutes > 0 ? options.timeoutMinutes * 60_000 : 0))),
|
|
187
|
+
}));
|
|
185
188
|
if (!result.success)
|
|
186
189
|
throw Error(`planning request failed: ${result.summary}`);
|
|
187
190
|
if (!existsSync(proposalPath))
|
package/dist/retrofit/command.js
CHANGED
|
@@ -8,9 +8,16 @@ import { ensureGitignore } from './gitignore.js';
|
|
|
8
8
|
import { loadConfig, saveConfig, defaultConfig } from './config.js';
|
|
9
9
|
import { loadManifest } from '../canon/manifest.js';
|
|
10
10
|
import { detectHostAgent } from '../agents/host.js';
|
|
11
|
+
import { pruneWorktrees } from '../loop/cleanup.js';
|
|
11
12
|
export function runRetrofit(targetDir, opts) {
|
|
12
13
|
const canonDir = resolveCanonDir();
|
|
13
14
|
const canonVersion = loadManifest(join(canonDir, 'manifest.yaml')).version;
|
|
15
|
+
if (opts.cleanWorktrees) {
|
|
16
|
+
const pruneRes = pruneWorktrees(targetDir);
|
|
17
|
+
if (pruneRes.removed.length > 0) {
|
|
18
|
+
console.log(`Pruned ${pruneRes.removed.length} orphaned worktree(s).`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
14
21
|
const detection = detectProject(targetDir);
|
|
15
22
|
const agents = opts.agents && opts.agents.length > 0
|
|
16
23
|
? opts.agents
|
|
@@ -26,6 +33,19 @@ export function runRetrofit(targetDir, opts) {
|
|
|
26
33
|
ensureGitignore(targetDir);
|
|
27
34
|
const priorAgents = existing?.agents ?? [];
|
|
28
35
|
const mergedAgents = [...new Set([...priorAgents, ...agents])];
|
|
36
|
+
const runnerConfig = { ...(existing?.runner ?? {}) };
|
|
37
|
+
if (opts.runnerModel !== undefined)
|
|
38
|
+
runnerConfig.model = opts.runnerModel;
|
|
39
|
+
if (opts.runnerReasoning !== undefined)
|
|
40
|
+
runnerConfig.reasoningEffort = opts.runnerReasoning;
|
|
41
|
+
let updatedWorkers = existing?.routing?.workers;
|
|
42
|
+
if (updatedWorkers && (opts.agentModels || opts.agentReasoning)) {
|
|
43
|
+
updatedWorkers = updatedWorkers.map(w => {
|
|
44
|
+
const model = opts.agentModels?.[w.agent] ?? w.model;
|
|
45
|
+
const reasoningEffort = opts.agentReasoning?.[w.agent] ?? w.reasoningEffort;
|
|
46
|
+
return { ...w, ...(model ? { model } : {}), ...(reasoningEffort ? { reasoningEffort } : {}) };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
29
49
|
const config = {
|
|
30
50
|
...(existing ?? defaultConfig(canonVersion)),
|
|
31
51
|
canonVersion,
|
|
@@ -33,6 +53,8 @@ export function runRetrofit(targetDir, opts) {
|
|
|
33
53
|
loop: { ...existing?.loop, enabled: opts.loop },
|
|
34
54
|
codeGraph,
|
|
35
55
|
codeIntelligence: { ...(existing?.codeIntelligence ?? {}), mode: codeIntelligence },
|
|
56
|
+
...(Object.keys(runnerConfig).length > 0 ? { runner: runnerConfig } : {}),
|
|
57
|
+
...(existing?.routing && updatedWorkers ? { routing: { ...existing.routing, workers: updatedWorkers } } : {}),
|
|
36
58
|
...(existing?.design
|
|
37
59
|
? { design: existing.design }
|
|
38
60
|
: detection.ui.detected ? { design: { mode: 'auto', max: 4 } } : {}),
|
package/dist/retrofit/config.js
CHANGED
|
@@ -16,7 +16,21 @@ const CodeIntelligenceSchema = z.object({
|
|
|
16
16
|
policy: z.object({ allowNetwork: z.boolean().default(false), excludePatterns: z.array(z.string().min(1)).max(100).default([]), maxFileBytes: z.number().int().positive().max(20_000_000).optional() }).optional(),
|
|
17
17
|
limits: z.object({ tokenBudget: z.number().int().min(128).max(16000).default(2400), timeoutMs: z.number().int().min(100).max(600000).default(10000), maxBytes: z.number().int().positive().max(10_000_000).default(2_000_000), maxBackends: z.number().int().min(1).max(3).default(3) }).optional(),
|
|
18
18
|
});
|
|
19
|
-
const
|
|
19
|
+
const SmokeStepTimeout = z.number().int().min(1).max(30000).optional();
|
|
20
|
+
const SmokeSelector = z.string().min(1).max(4096);
|
|
21
|
+
const SmokeStepSchema = z.discriminatedUnion('action', [
|
|
22
|
+
z.object({ action: z.literal('click'), selector: SmokeSelector, timeoutMs: SmokeStepTimeout }).strict(),
|
|
23
|
+
z.object({ action: z.literal('fill'), selector: SmokeSelector, value: z.string().max(8192).optional(), valueEnv: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/u).optional(), timeoutMs: SmokeStepTimeout }).strict(),
|
|
24
|
+
z.object({ action: z.literal('press'), selector: SmokeSelector, key: z.string().min(1).max(128), timeoutMs: SmokeStepTimeout }).strict(),
|
|
25
|
+
z.object({ action: z.literal('expect-visible'), selector: SmokeSelector, timeoutMs: SmokeStepTimeout }).strict(),
|
|
26
|
+
z.object({ action: z.literal('expect-text'), selector: SmokeSelector, text: z.string().max(8192), exact: z.boolean().optional(), timeoutMs: SmokeStepTimeout }).strict(),
|
|
27
|
+
z.object({ action: z.literal('expect-url'), url: z.string().min(1).max(4096), timeoutMs: SmokeStepTimeout }).strict(),
|
|
28
|
+
z.object({ action: z.literal('reload'), timeoutMs: SmokeStepTimeout }).strict(),
|
|
29
|
+
]).superRefine((step, context) => {
|
|
30
|
+
if (step.action === 'fill' && (step.value === undefined) === (step.valueEnv === undefined))
|
|
31
|
+
context.addIssue({ code: 'custom', message: 'A fill step needs exactly one of value or valueEnv' });
|
|
32
|
+
});
|
|
33
|
+
const SmokeFlowSchema = z.object({ name: z.string().min(1), path: z.string().min(1), landmark: z.string().optional(), timeoutMs: z.number().int().min(1).max(120000).optional(), steps: z.array(SmokeStepSchema).min(1).max(50).optional() });
|
|
20
34
|
const SmokeSchema = z.object({ baseUrl: z.string().min(1), flows: z.array(SmokeFlowSchema).min(1) });
|
|
21
35
|
const OutputPolicySchema = z.object({
|
|
22
36
|
previewBytes: z.number().int().positive().optional(),
|
|
@@ -56,6 +70,12 @@ const RoutingWorkerSchema = z.object({
|
|
|
56
70
|
capabilities: z.array(z.string().min(1)).default([]),
|
|
57
71
|
tier: z.enum(['light', 'standard', 'strong', 'frontier']).optional(),
|
|
58
72
|
roles: z.array(z.enum(['implementation', 'reviewer', 'critic', 'repair'])).optional(),
|
|
73
|
+
profileMetadata: z.object({
|
|
74
|
+
version: z.literal(1),
|
|
75
|
+
catalog: z.string().min(1).max(120),
|
|
76
|
+
source: z.enum(['yoke-default', 'operator']),
|
|
77
|
+
basis: z.literal('configured-prior'),
|
|
78
|
+
}).strict().optional(),
|
|
59
79
|
});
|
|
60
80
|
const RoutingRuleSchema = z.object({
|
|
61
81
|
area: z.string().min(1).optional(),
|
|
@@ -104,6 +124,11 @@ export const YokeConfigSchema = z.object({
|
|
|
104
124
|
assessmentPolicy: z.enum(['on-demand', 'prepared']).optional(),
|
|
105
125
|
fallback: z.enum(['parent', 'block']).optional(),
|
|
106
126
|
maxTier: z.enum(['light', 'standard', 'strong', 'frontier']).optional(),
|
|
127
|
+
optimization: z.object({
|
|
128
|
+
version: z.literal(1),
|
|
129
|
+
objective: z.enum(['cost', 'speed', 'balanced']),
|
|
130
|
+
minSamples: z.number().int().min(10).max(10000).optional(),
|
|
131
|
+
}).strict().optional(),
|
|
107
132
|
maxCandidates: z.number().int().min(1).max(5).default(3),
|
|
108
133
|
orchestrator: z.object({
|
|
109
134
|
provider: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/).optional(),
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
export const YOKE_IGNORE_LINES = [
|
|
4
4
|
'.yoke/worktrees/',
|
|
5
5
|
'.yoke/integration-recovery/',
|
|
6
|
+
'.yoke/failure-progress/',
|
|
6
7
|
'.yoke/run-state.json',
|
|
7
8
|
'.yoke/run-state.*.tmp',
|
|
8
9
|
'.yoke/backup/',
|
|
@@ -31,6 +32,7 @@ export const YOKE_IGNORE_LINES = [
|
|
|
31
32
|
'.yoke/checks/',
|
|
32
33
|
'.yoke/events/',
|
|
33
34
|
'.yoke/history/', '.yoke/routing/',
|
|
35
|
+
'.yoke/routing-attempts/',
|
|
34
36
|
'.yoke/goal.json',
|
|
35
37
|
'.yoke/goal.json.*.tmp',
|
|
36
38
|
'.yoke/goal.pause',
|
|
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { loadManifest } from '../../canon/manifest.js';
|
|
4
4
|
import { mcpServers, rtkInstruction } from '../tools.js';
|
|
5
|
-
import { hasWsl } from '../wsl.js';
|
|
5
|
+
import { hasWsl, hasRtk } from '../wsl.js';
|
|
6
6
|
import { detectGstack } from '../gstack.js';
|
|
7
7
|
import { PRESERVE_SCAFFOLD } from '../preserve.js';
|
|
8
8
|
import { skillPackageActions } from '../skill-actions.js';
|
|
@@ -22,7 +22,7 @@ This project uses the Yoke harness. Baseline instructions:
|
|
|
22
22
|
${rtkNote ? `\n${rtkNote}\n` : ''}${composeNote ? `\n${composeNote}\n` : ''}
|
|
23
23
|
${PRESERVE_SCAFFOLD}
|
|
24
24
|
`;
|
|
25
|
-
export function planClaude(canonDir, targetDir, wslAvailable = hasWsl(), codeGraph = 'graphify', gstackDetected = detectGstack(targetDir), codeIntelligence = 'off') {
|
|
25
|
+
export function planClaude(canonDir, targetDir, wslAvailable = (hasWsl() || hasRtk()), codeGraph = 'graphify', gstackDetected = detectGstack(targetDir), codeIntelligence = 'off', rtkAvailable) {
|
|
26
26
|
const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
|
|
27
27
|
const actions = [];
|
|
28
28
|
for (const skill of manifest.skills) {
|
|
@@ -34,8 +34,8 @@ export function planClaude(canonDir, targetDir, wslAvailable = hasWsl(), codeGra
|
|
|
34
34
|
content: readFileSync(join(canonDir, 'AGENTS.md'), 'utf8'),
|
|
35
35
|
reason: 'baseline instructions',
|
|
36
36
|
});
|
|
37
|
-
// rtk: PreToolUse hook
|
|
38
|
-
const rtkHookable = wslAvailable;
|
|
37
|
+
// rtk: PreToolUse hook works natively if rtk is available, or via WSL on Windows.
|
|
38
|
+
const rtkHookable = rtkAvailable !== undefined ? (wslAvailable || rtkAvailable) : wslAvailable;
|
|
39
39
|
actions.push({
|
|
40
40
|
kind: 'write',
|
|
41
41
|
target: 'CLAUDE.md',
|
package/dist/retrofit/wsl.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
// instruction mode. Never throws.
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
// True only on Windows where a WSL distribution responds.
|
|
5
4
|
export function hasWsl() {
|
|
6
5
|
if (process.platform !== 'win32')
|
|
7
6
|
return false;
|
|
@@ -13,3 +12,24 @@ export function hasWsl() {
|
|
|
13
12
|
return false;
|
|
14
13
|
}
|
|
15
14
|
}
|
|
15
|
+
// True if the rtk binary is available natively or on PATH. Never throws.
|
|
16
|
+
export function hasRtk() {
|
|
17
|
+
try {
|
|
18
|
+
const cmd = process.platform === 'win32' ? 'rtk.exe' : 'rtk';
|
|
19
|
+
execFileSync(cmd, ['--version'], { stdio: 'pipe', timeout: 3000 });
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
if (process.platform === 'win32') {
|
|
24
|
+
const home = process.env.USERPROFILE ?? process.env.HOME;
|
|
25
|
+
if (home) {
|
|
26
|
+
try {
|
|
27
|
+
execFileSync(join(home, '.local', 'bin', 'rtk.exe'), ['--version'], { stdio: 'pipe', timeout: 3000 });
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch { /* ignore */ }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|