@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
package/dist/loop/worker.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { cacheIsolationProblem } from './cache-isolation.js';
|
|
1
2
|
import { knownInfrastructureFailure } from "../routing/capability.js";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { acceptanceProtectionProblem } from "../check/command.js";
|
|
5
6
|
import { isAcceptanceCriterion } from './prd.js';
|
|
7
|
+
import { observeFailure } from './failure.js';
|
|
8
|
+
import { consumeAmbiguity } from './loop.js';
|
|
9
|
+
import { gateIdentity, snapshotGates, reuseGates } from './gate-snapshot.js';
|
|
6
10
|
import { runQualityRepairLoop } from '../quality/loop.js';
|
|
7
11
|
function emptyEvidence() { return { criteria: [] }; }
|
|
8
12
|
function errorMessage(error) {
|
|
@@ -40,6 +44,10 @@ function reviewOutcome(result) {
|
|
|
40
44
|
return { kind: 'malformed', summary: result.summary };
|
|
41
45
|
}
|
|
42
46
|
function runMechanicalGates(input, context, evidence) {
|
|
47
|
+
evidence.criteria = [];
|
|
48
|
+
const cacheProblem = input.failureRoot && input.failureRoot !== context.targetDir ? cacheIsolationProblem(context.targetDir) : undefined;
|
|
49
|
+
if (cacheProblem)
|
|
50
|
+
return { kind: 'failed', stage: 'verify', summary: cacheProblem };
|
|
43
51
|
const criteria = context.story.acceptance.filter(isAcceptanceCriterion);
|
|
44
52
|
if (criteria.length === 0) {
|
|
45
53
|
if (input.requireCriterionEvidence)
|
|
@@ -170,8 +178,13 @@ export async function runStoryWorker(input) {
|
|
|
170
178
|
return finalResult(input, { ...baseResult(input, evidence, preflight.summary), kind: 'mechanical-failure', stage: 'quality' });
|
|
171
179
|
}
|
|
172
180
|
let implementation;
|
|
181
|
+
let verified;
|
|
182
|
+
let failure;
|
|
173
183
|
try {
|
|
174
|
-
|
|
184
|
+
const outcome = await runWorkerImplementation(input, context, evidence);
|
|
185
|
+
implementation = outcome.result;
|
|
186
|
+
verified = outcome.gates;
|
|
187
|
+
failure = outcome.failure;
|
|
175
188
|
}
|
|
176
189
|
catch (error) {
|
|
177
190
|
return finalResult(input, {
|
|
@@ -185,110 +198,158 @@ export async function runStoryWorker(input) {
|
|
|
185
198
|
if (implementation.infrastructureFailure)
|
|
186
199
|
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
187
200
|
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
|
-
|
|
201
|
+
return finalResult(input, { ...baseResult(input, evidence, implementation.summary), kind: "mechanical-failure", stage: "implementation", ...(failure ? { failure } : {}) });
|
|
202
|
+
try {
|
|
203
|
+
const afterImplementationCancellation = cancellationReason(input.cancellation);
|
|
204
|
+
if (afterImplementationCancellation) {
|
|
205
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
206
|
+
return finalResult(input, { ...baseResult(input, evidence, afterImplementationCancellation), kind: 'cancelled' });
|
|
207
|
+
}
|
|
208
|
+
const ambiguity = consumeAmbiguity(context.targetDir);
|
|
209
|
+
if (ambiguity) {
|
|
210
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
211
|
+
return finalResult(input, {
|
|
212
|
+
...baseResult(input, evidence, `story ${context.story.id} stopped: ambiguous acceptance criteria — ${ambiguity}`),
|
|
213
|
+
kind: 'paused', reason: 'ambiguity',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const decision = input.beforeGates?.(context);
|
|
217
|
+
if (decision) {
|
|
218
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
219
|
+
return finalResult(input, { ...baseResult(input, evidence, decision), kind: 'paused', reason: 'decision' });
|
|
220
|
+
}
|
|
221
|
+
if (input.pause?.()) {
|
|
222
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
223
|
+
return finalResult(input, { ...baseResult(input, evidence, 'worker paused before acceptance checks'), kind: 'paused' });
|
|
224
|
+
}
|
|
225
|
+
const gates = reuseGates(context.targetDir, context.story, verified) ?? runMechanicalGates(input, context, evidence);
|
|
226
|
+
if (gates.kind === 'cancelled') {
|
|
227
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
228
|
+
return finalResult(input, { ...baseResult(input, evidence, gates.summary), kind: 'cancelled' });
|
|
229
|
+
}
|
|
230
|
+
if (gates.kind === 'failed') {
|
|
231
|
+
implementation.routing?.recordOutcome(false);
|
|
232
|
+
const observed = input.failureRoot ? observeFailure({ root: input.failureRoot, scope: input.failureScope, directory: context.targetDir, story: context.story, stage: gates.stage, summary: gates.summary }) : undefined;
|
|
233
|
+
return finalResult(input, { ...baseResult(input, evidence, observed?.feedback ?? gates.summary), kind: 'mechanical-failure', stage: gates.stage, ...(observed ? { failure: observed.failure } : {}) });
|
|
234
|
+
}
|
|
235
|
+
const summary = implementation.success
|
|
236
|
+
? implementation.summary
|
|
237
|
+
: `${implementation.summary} (runner exited non-zero but verify is green)`;
|
|
238
|
+
if (!qualityEnabled && !input.review) {
|
|
239
|
+
return finalResult(input, {
|
|
240
|
+
...baseResult(input, evidence, summary),
|
|
241
|
+
kind: 'candidate',
|
|
242
|
+
routing: { outcome: 'pending-integration', ...(implementation.routing ? { recordOutcome: implementation.routing.recordOutcome } : {}) },
|
|
243
|
+
...(implementation.tokens ? { tokens: implementation.tokens } : {}),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const beforeQualityCancellation = cancellationReason(input.cancellation);
|
|
247
|
+
if (beforeQualityCancellation) {
|
|
248
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
249
|
+
return finalResult(input, { ...baseResult(input, evidence, beforeQualityCancellation), kind: 'cancelled' });
|
|
250
|
+
}
|
|
251
|
+
const qualityStage = qualityEnabled ? input.qualityStage : undefined;
|
|
252
|
+
const review = input.review;
|
|
253
|
+
let qualityFailure;
|
|
254
|
+
const quality = runQualityRepairLoop({
|
|
255
|
+
...(qualityStage
|
|
256
|
+
? { quality: (round) => {
|
|
257
|
+
input.reporter?.phase('comparing');
|
|
258
|
+
return qualityStage(context, round);
|
|
259
|
+
} }
|
|
260
|
+
: {}),
|
|
261
|
+
...(review
|
|
262
|
+
? { review: () => {
|
|
263
|
+
input.reporter?.phase('reviewing');
|
|
264
|
+
return reviewOutcome(review(context));
|
|
265
|
+
} }
|
|
266
|
+
: {}),
|
|
267
|
+
repair: request => {
|
|
268
|
+
if (!input.repair)
|
|
269
|
+
return { kind: 'blocked', summary: 'repair callback is not configured' };
|
|
270
|
+
input.reporter?.phase('repairing');
|
|
271
|
+
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;
|
|
272
|
+
if (observed?.action === 'blocked') {
|
|
273
|
+
qualityFailure = observed.failure;
|
|
274
|
+
return { kind: 'blocked', summary: observed.feedback };
|
|
275
|
+
}
|
|
276
|
+
const targetedRequest = observed?.action === 'diagnose' ? { ...request, finding: { ...request.finding, message: observed.feedback } } : request;
|
|
277
|
+
const repair = input.repair(context, targetedRequest);
|
|
278
|
+
return repair.success ? { kind: 'repaired' } : { kind: 'blocked', summary: repair.summary };
|
|
279
|
+
},
|
|
280
|
+
rerunGates: () => {
|
|
281
|
+
const rerun = runMechanicalGates(input, context, evidence);
|
|
282
|
+
if (rerun.kind === 'passed')
|
|
283
|
+
return { kind: 'passed' };
|
|
284
|
+
if (rerun.kind === 'cancelled')
|
|
285
|
+
return rerun;
|
|
286
|
+
return { kind: 'failed', stage: rerun.stage, summary: rerun.summary };
|
|
287
|
+
},
|
|
288
|
+
...(input.repairLimits ? { limits: input.repairLimits } : {}),
|
|
289
|
+
pause: input.pause,
|
|
290
|
+
onStatus: status => {
|
|
291
|
+
const metadata = input.qualityMetadata?.(context);
|
|
292
|
+
input.reporter?.quality({ ...status, ...(metadata ?? { policy: 'blocking' }) });
|
|
293
|
+
},
|
|
214
294
|
});
|
|
295
|
+
evidence.quality = quality;
|
|
296
|
+
const afterQualityCancellation = cancellationReason(input.cancellation);
|
|
297
|
+
if (afterQualityCancellation) {
|
|
298
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
299
|
+
return finalResult(input, { ...baseResult(input, evidence, afterQualityCancellation), kind: 'cancelled' });
|
|
300
|
+
}
|
|
301
|
+
const result = { ...resultFromQuality(input, evidence, summary, quality), ...(qualityFailure ? { failure: qualityFailure } : {}) };
|
|
302
|
+
if (result.kind === 'paused' || result.kind === 'cancelled' || result.kind === 'review-failure' || result.kind === 'quality-failure')
|
|
303
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
304
|
+
else if (result.kind !== 'candidate')
|
|
305
|
+
implementation.routing?.recordOutcome(false);
|
|
306
|
+
if (result.kind === 'candidate') {
|
|
307
|
+
return finalResult(input, {
|
|
308
|
+
...result,
|
|
309
|
+
routing: { outcome: 'pending-integration', ...(implementation.routing ? { recordOutcome: implementation.routing.recordOutcome } : {}) },
|
|
310
|
+
...(implementation.tokens ? { tokens: implementation.tokens } : {}),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return finalResult(input, result);
|
|
215
314
|
}
|
|
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
|
-
});
|
|
315
|
+
catch (error) {
|
|
316
|
+
implementation.routing?.recordOutcome(false, 'infrastructure');
|
|
317
|
+
throw error;
|
|
271
318
|
}
|
|
272
|
-
return finalResult(input, result);
|
|
273
319
|
}
|
|
274
320
|
async function runWorkerImplementation(input, context, evidence) {
|
|
275
|
-
let feedback;
|
|
321
|
+
let feedback = input.feedback;
|
|
276
322
|
for (let attempt = 0;; attempt++) {
|
|
277
323
|
const result = await input.runner({ ...context, feedback });
|
|
324
|
+
const cacheProblem = input.failureRoot && input.failureRoot !== context.targetDir ? cacheIsolationProblem(context.targetDir) : undefined;
|
|
325
|
+
if (cacheProblem)
|
|
326
|
+
return { result: { ...result, success: false, infrastructureFailure: true, summary: cacheProblem } };
|
|
278
327
|
if (!result.routing?.canRetry || result.routing.blocked || attempt >= 7 || cancellationReason(input.cancellation) || input.pause?.())
|
|
279
|
-
return result;
|
|
328
|
+
return { result };
|
|
280
329
|
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
|
-
|
|
330
|
+
return { result };
|
|
331
|
+
const before = gateIdentity(context.targetDir, context.story);
|
|
332
|
+
let gates;
|
|
333
|
+
try {
|
|
334
|
+
gates = runMechanicalGates(input, context, evidence);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
result.routing.recordOutcome(false, 'infrastructure');
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
if (gates.kind !== 'failed') {
|
|
341
|
+
return { result, ...(gates.kind === 'passed' ? { gates: snapshotGates(context.targetDir, context.story, before, gates) } : {}) };
|
|
342
|
+
}
|
|
285
343
|
if (knownInfrastructureFailure(gates.summary)) {
|
|
286
344
|
result.routing.recordOutcome(false, 'infrastructure');
|
|
287
|
-
return { ...result, success: false, infrastructureFailure: true, summary: gates.summary, routing: { ...result.routing, blocked: true, canRetry: false } };
|
|
345
|
+
return { result: { ...result, success: false, infrastructureFailure: true, summary: gates.summary, routing: { ...result.routing, blocked: true, canRetry: false } } };
|
|
288
346
|
}
|
|
289
347
|
result.routing.recordOutcome(false);
|
|
348
|
+
const observed = input.failureRoot ? observeFailure({ root: input.failureRoot, scope: input.failureScope, directory: context.targetDir, story: context.story, stage: gates.stage, summary: gates.summary }) : undefined;
|
|
349
|
+
if (observed?.action === 'blocked')
|
|
350
|
+
return { result: { ...result, success: false, summary: observed.feedback, routing: { ...result.routing, blocked: true, canRetry: false } }, failure: observed.failure };
|
|
290
351
|
if (result.tokens)
|
|
291
352
|
input.reporter?.addTokens(result.tokens);
|
|
292
|
-
feedback = gates.summary;
|
|
353
|
+
feedback = observed?.feedback ?? gates.summary;
|
|
293
354
|
}
|
|
294
355
|
}
|
|
@@ -30,7 +30,8 @@ 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
|
+
allowed.push('usageSource', 'usageMissingFields', 'usagePartialFields', 'failureCategory');
|
|
34
35
|
const compact = { ...event, data: Object.fromEntries(allowed.filter(key => data[key] !== undefined).map(key => [key, data[key]])) };
|
|
35
36
|
appendFileSync(file, JSON.stringify(compact) + '\n');
|
|
36
37
|
}
|
|
@@ -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,120 @@
|
|
|
1
|
+
import { EVENT_CAP, readEvents } from './events.js';
|
|
2
|
+
import { readMeasurements } from './history.js';
|
|
3
|
+
const fields = ['inputTokens', 'cachedInputTokens', 'outputTokens', 'reasoningOutputTokens', 'totalCostUsd'];
|
|
4
|
+
const finite = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
5
|
+
const text = (value) => typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
6
|
+
function intervalTotals(events) {
|
|
7
|
+
const intervals = events.flatMap(event => {
|
|
8
|
+
const end = Date.parse(event.timestamp);
|
|
9
|
+
return finite(event.durationMs) && Number.isFinite(end) ? [[end - event.durationMs, end]] : [];
|
|
10
|
+
}).sort((a, b) => a[0] - b[0]);
|
|
11
|
+
if (!intervals.length)
|
|
12
|
+
return { sumMs: null, unionMs: null };
|
|
13
|
+
let sumMs = 0, unionMs = 0, end = -Infinity;
|
|
14
|
+
for (const [start, stop] of intervals) {
|
|
15
|
+
sumMs += stop - start;
|
|
16
|
+
unionMs += Math.max(0, stop - Math.max(start, end));
|
|
17
|
+
end = Math.max(end, stop);
|
|
18
|
+
}
|
|
19
|
+
return { sumMs, unionMs };
|
|
20
|
+
}
|
|
21
|
+
/** Recorded evidence only. Parent views conservatively cover their identified children. */
|
|
22
|
+
export function summarizeUsageEvents(input) {
|
|
23
|
+
const events = [...new Map(input.map(event => [event.id, event])).values()];
|
|
24
|
+
const byId = new Map();
|
|
25
|
+
for (const event of events) {
|
|
26
|
+
if (event.type !== 'tokens')
|
|
27
|
+
continue;
|
|
28
|
+
const data = event.data ?? {};
|
|
29
|
+
const breakdown = Array.isArray(data.calls) && data.calls.length > 0;
|
|
30
|
+
const records = breakdown ? data.calls : [data];
|
|
31
|
+
records.forEach((record, index) => {
|
|
32
|
+
if (!record || typeof record !== 'object' || Array.isArray(record))
|
|
33
|
+
return;
|
|
34
|
+
const raw = record;
|
|
35
|
+
const callId = text(raw.callId) ?? `${event.id}:${index}`;
|
|
36
|
+
// IDs identify views of the same invocation even across reporter run IDs.
|
|
37
|
+
const durationMs = finite(raw.durationMs) ? raw.durationMs : !breakdown && finite(event.durationMs) ? event.durationMs : null;
|
|
38
|
+
const previous = byId.get(callId);
|
|
39
|
+
if (previous) {
|
|
40
|
+
previous.durationMs ??= durationMs;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const missing = Array.isArray(raw.usageMissingFields) ? raw.usageMissingFields : [];
|
|
44
|
+
const partial = Array.isArray(raw.usagePartialFields) ? raw.usagePartialFields : [];
|
|
45
|
+
const metrics = Object.fromEntries(fields.map(field => [field, !missing.includes(field) && finite(raw[field]) ? raw[field] : null]));
|
|
46
|
+
const complete = raw.usageAvailable !== false && raw.measurementComplete !== false
|
|
47
|
+
&& (breakdown || (data.usageAvailable !== false && data.measurementComplete !== false))
|
|
48
|
+
&& metrics.inputTokens !== null && metrics.outputTokens !== null;
|
|
49
|
+
const coverage = complete ? 'measured' : fields.some(field => metrics[field] !== null) ? 'partial' : 'unknown';
|
|
50
|
+
byId.set(callId, {
|
|
51
|
+
...metrics, callId, parentCallId: text(raw.parentCallId ?? data.parentCallId), runId: event.runId,
|
|
52
|
+
attemptId: event.attemptId, storyId: event.storyId,
|
|
53
|
+
role: text(raw.role ?? event.role ?? data.role) ?? 'unknown',
|
|
54
|
+
provider: text(raw.provider ?? event.provider ?? data.provider ?? event.agent),
|
|
55
|
+
source: text(raw.usageSource ?? data.usageSource) ?? 'local-event', coverage,
|
|
56
|
+
costCoverage: metrics.totalCostUsd === null ? 'unknown' : raw.costMeasurementComplete === false || (!breakdown && data.costMeasurementComplete === false) ? 'partial' : 'measured',
|
|
57
|
+
missingFields: fields.filter(field => metrics[field] === null),
|
|
58
|
+
fieldCoverage: Object.fromEntries(fields.map(field => [field, metrics[field] === null ? 'unknown' : partial.includes(field) ? 'partial' : 'measured'])),
|
|
59
|
+
durationMs,
|
|
60
|
+
counted: true,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const calls = [...byId.values()];
|
|
65
|
+
for (const call of calls) {
|
|
66
|
+
if (call.parentCallId && byId.has(call.parentCallId)) {
|
|
67
|
+
call.counted = false;
|
|
68
|
+
call.exclusion = 'parent-view-overlap';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const counted = calls.filter(call => call.counted);
|
|
72
|
+
const sum = (key, values = counted) => {
|
|
73
|
+
const measured = values.map(call => call[key]).filter((value) => value !== null);
|
|
74
|
+
return measured.length ? measured.reduce((total, value) => total + value, 0) : null;
|
|
75
|
+
};
|
|
76
|
+
const coverage = (values) => !values.length || values.every(call => call.coverage === 'unknown') ? 'unknown' : values.every(call => call.coverage === 'measured') ? 'measured' : 'partial';
|
|
77
|
+
const unmeasuredAttempts = events.filter(event => event.type === 'attempt-ended' && !calls.some(call => call.runId === event.runId && call.attemptId === event.attemptId)).length;
|
|
78
|
+
const inputTokens = sum('inputTokens'), outputTokens = sum('outputTokens');
|
|
79
|
+
const phases = events.filter(event => event.type === 'phase-ended');
|
|
80
|
+
const phaseTime = intervalTotals(phases), attemptTime = intervalTotals(events.filter(event => event.type === 'attempt-ended'));
|
|
81
|
+
const failures = { observer: 0, infrastructure: 0, product: 0, unknown: 0 };
|
|
82
|
+
for (const event of events) {
|
|
83
|
+
if (!['failed', 'blocked', 'rejected', 'error'].includes(event.outcome ?? ''))
|
|
84
|
+
continue;
|
|
85
|
+
const category = event.data?.failureCategory;
|
|
86
|
+
failures[category === 'observer' || category === 'infrastructure' || category === 'product' ? category : 'unknown']++;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
calls,
|
|
90
|
+
total: { inputTokens, cachedInputTokens: sum('cachedInputTokens'), outputTokens, reasoningOutputTokens: sum('reasoningOutputTokens'),
|
|
91
|
+
totalTokens: inputTokens !== null && outputTokens !== null ? inputTokens + outputTokens : null,
|
|
92
|
+
totalCostUsd: sum('totalCostUsd'), coverage: unmeasuredAttempts && coverage(counted) === 'measured' ? 'partial' : coverage(counted),
|
|
93
|
+
costCoverage: !counted.length || counted.every(call => call.costCoverage === 'unknown') ? 'unknown' : counted.every(call => call.costCoverage === 'measured') && !unmeasuredAttempts ? 'measured' : 'partial', },
|
|
94
|
+
hostCoverage: { guardian: coverage(calls.filter(call => call.role === 'guardian')), approval: coverage(calls.filter(call => call.role === 'approval')) },
|
|
95
|
+
// Serial makeRunner labels story implementation as parent; routing controllers use orchestrator.
|
|
96
|
+
time: { workerProcessDurationMs: sum('durationMs', counted.filter(call => call.role === 'worker' || (call.role === 'parent' && call.storyId !== undefined))),
|
|
97
|
+
phaseDurationSumMs: phaseTime.sumMs, phaseDurationUnionMs: phaseTime.unionMs,
|
|
98
|
+
attemptDurationSumMs: attemptTime.sumMs, attemptDurationUnionMs: attemptTime.unionMs,
|
|
99
|
+
phases: [...new Set(phases.map(event => event.phase ?? 'unknown'))].sort().map(phase => ({ phase, ...intervalTotals(phases.filter(event => (event.phase ?? 'unknown') === phase)) })),
|
|
100
|
+
},
|
|
101
|
+
failures, unmeasuredAttempts,
|
|
102
|
+
limitations: ['Recorded local events only; unrecorded host sessions and approvals remain unknown.',
|
|
103
|
+
'Totals are known lower bounds when coverage is partial; missing prices remain unknown.',
|
|
104
|
+
'Cache read is contained in input; reasoning is contained in output.',
|
|
105
|
+
'Identified child views are excluded when a parent view exists; disjoint usage is not assumed.',
|
|
106
|
+
'Intervals derive from recorded end times and durations; unrecorded waiting time is unknown.'],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** No model/provider invocation: reads bounded local history and recent events. */
|
|
110
|
+
export function localUsageReport(root, options) {
|
|
111
|
+
const { from, to, runId } = options;
|
|
112
|
+
if (!Number.isFinite(from) || !Number.isFinite(to) || from >= to || to - from > 366 * 86400000)
|
|
113
|
+
throw Error('Choose a valid period of at most 366 days');
|
|
114
|
+
const history = readMeasurements(root, from, to);
|
|
115
|
+
const events = [...history.events, ...readEvents(root, EVENT_CAP)].filter(event => {
|
|
116
|
+
const time = Date.parse(event.timestamp);
|
|
117
|
+
return time >= from && time < to && (!runId || event.runId === runId);
|
|
118
|
+
});
|
|
119
|
+
return { from: new Date(from).toISOString(), to: new Date(to).toISOString(), ...summarizeUsageEvents(events), errors: history.errors };
|
|
120
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
usageMissingFields: ['inputTokens', 'cachedInputTokens', 'outputTokens', 'reasoningOutputTokens', 'totalCostUsd'].filter(field => typeof known[field] !== 'number'),
|
|
17
|
+
usagePartialFields: Object.keys(telemetry.partialUsage ?? {}).filter(field => typeof telemetry.tokens?.[field] !== 'number' && typeof known[field] === 'number'),
|
|
18
|
+
};
|
|
19
|
+
}
|
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/config.js
CHANGED
|
@@ -16,8 +16,24 @@ 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
|
|
20
|
-
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() });
|
|
34
|
+
const SmokeSchema = z.object({ baseUrl: z.string().min(1), flows: z.array(SmokeFlowSchema).min(1),
|
|
35
|
+
sourceIdentity: z.object({ path: z.string().regex(/^\/(?!\/)/).max(4096), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).optional(),
|
|
36
|
+
});
|
|
21
37
|
const OutputPolicySchema = z.object({
|
|
22
38
|
previewBytes: z.number().int().positive().optional(),
|
|
23
39
|
artifactThresholdBytes: z.number().int().positive().optional(),
|
|
@@ -56,6 +72,12 @@ const RoutingWorkerSchema = z.object({
|
|
|
56
72
|
capabilities: z.array(z.string().min(1)).default([]),
|
|
57
73
|
tier: z.enum(['light', 'standard', 'strong', 'frontier']).optional(),
|
|
58
74
|
roles: z.array(z.enum(['implementation', 'reviewer', 'critic', 'repair'])).optional(),
|
|
75
|
+
profileMetadata: z.object({
|
|
76
|
+
version: z.literal(1),
|
|
77
|
+
catalog: z.string().min(1).max(120),
|
|
78
|
+
source: z.enum(['yoke-default', 'operator']),
|
|
79
|
+
basis: z.literal('configured-prior'),
|
|
80
|
+
}).strict().optional(),
|
|
59
81
|
});
|
|
60
82
|
const RoutingRuleSchema = z.object({
|
|
61
83
|
area: z.string().min(1).optional(),
|
|
@@ -104,6 +126,11 @@ export const YokeConfigSchema = z.object({
|
|
|
104
126
|
assessmentPolicy: z.enum(['on-demand', 'prepared']).optional(),
|
|
105
127
|
fallback: z.enum(['parent', 'block']).optional(),
|
|
106
128
|
maxTier: z.enum(['light', 'standard', 'strong', 'frontier']).optional(),
|
|
129
|
+
optimization: z.object({
|
|
130
|
+
version: z.literal(1),
|
|
131
|
+
objective: z.enum(['cost', 'speed', 'balanced']),
|
|
132
|
+
minSamples: z.number().int().min(10).max(10000).optional(),
|
|
133
|
+
}).strict().optional(),
|
|
107
134
|
maxCandidates: z.number().int().min(1).max(5).default(3),
|
|
108
135
|
orchestrator: z.object({
|
|
109
136
|
provider: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/).optional(),
|