@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/routing/router.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { buildWatchdogInvocation, makeRunner, runCapturedAgent, runnerInvocation, contextBlockFor, } from '../loop/runner.js';
|
|
2
3
|
import { isAcceptanceCriterion, criterionCommandProblem } from '../loop/prd.js';
|
|
3
4
|
import { historyForWorkers, projectHash, readRoutingObservations, recordRoutingObservation, storyHash } from './registry.js';
|
|
4
5
|
import { assessmentInstructions, parseAssessment, tiers } from './assessment.js';
|
|
5
6
|
import { chooseCapability, readAssessment, saveAssessment, routingAssessmentKey, knownInfrastructureFailure } from './capability.js';
|
|
6
7
|
import { readPlanningFile } from './contracts.js';
|
|
8
|
+
import { finishRoutingAttempt, markRoutingAttemptUsageIncomplete, readRoutingAttempts, recordRoutingAttemptUsage, reserveRoutingAttempt, routingEpisodeSummary } from './attempts.js';
|
|
9
|
+
import { assessmentSignature } from './optimization.js';
|
|
10
|
+
export function buildAssessmentPrompt(root, ctx) {
|
|
11
|
+
return [assessmentInstructions, 'Use the supplied task contract and project context to produce a bounded plan. Do not implement or change files.',
|
|
12
|
+
'Approved planning brief:', readPlanningFile(root, '.yoke/plan.md', 80_000) ?? '',
|
|
13
|
+
contextBlockFor(ctx.targetDir, ctx.story), JSON.stringify(ctx.story), 'Return exactly one line: YOKE_ASSESS {"taskClass":"implementation","difficulty":"medium","uncertainty":"low","risk":"low","scope":"low","testability":"high","reason":"evidence","approach":"steps and tests"}'].join('\n');
|
|
14
|
+
}
|
|
7
15
|
const costRank = { low: 0, medium: 1, high: 2 };
|
|
8
16
|
export function rankWorkers(workers, strategy, maxCandidates) {
|
|
9
17
|
const history = historyForWorkers(workers);
|
|
@@ -106,6 +114,8 @@ function callUsage(role, provider, selection, tokens, durationMs, profile) {
|
|
|
106
114
|
...(selection.reasoningEffort ? { requestedReasoningEffort: selection.reasoningEffort } : {}),
|
|
107
115
|
...(selection.variant ? { requestedVariant: selection.variant } : {}),
|
|
108
116
|
...(tokens?.model ? { actualModel: tokens.model } : {}),
|
|
117
|
+
usageMissingFields: tokens?.usageMissingFields ?? (!tokens ? ['inputTokens', 'outputTokens', 'cachedInputTokens', 'reasoningOutputTokens', 'totalCostUsd'] : []),
|
|
118
|
+
...(tokens?.usagePartialFields ? { usagePartialFields: tokens.usagePartialFields } : {}),
|
|
109
119
|
inputTokens: tokens?.inputTokens ?? 0,
|
|
110
120
|
...(tokens?.cachedInputTokens !== undefined ? { cachedInputTokens: tokens.cachedInputTokens } : {}),
|
|
111
121
|
...(tokens?.cacheWriteInputTokens !== undefined ? { cacheWriteInputTokens: tokens.cacheWriteInputTokens } : {}),
|
|
@@ -113,6 +123,7 @@ function callUsage(role, provider, selection, tokens, durationMs, profile) {
|
|
|
113
123
|
usageAvailable: tokens !== undefined && tokens.measurementComplete !== false,
|
|
114
124
|
...(tokens?.reasoningOutputTokens !== undefined ? { reasoningOutputTokens: tokens.reasoningOutputTokens } : {}),
|
|
115
125
|
...(tokens?.totalCostUsd !== undefined ? { totalCostUsd: tokens.totalCostUsd } : {}),
|
|
126
|
+
costMeasurementComplete: tokens?.totalCostUsd !== undefined && tokens.costMeasurementComplete !== false,
|
|
116
127
|
durationMs,
|
|
117
128
|
};
|
|
118
129
|
}
|
|
@@ -121,8 +132,17 @@ export function makeAdaptiveRunner(options) {
|
|
|
121
132
|
return ctx => {
|
|
122
133
|
const steps = run(ctx);
|
|
123
134
|
let next = steps.next();
|
|
124
|
-
while (!next.done)
|
|
125
|
-
|
|
135
|
+
while (!next.done) {
|
|
136
|
+
let result;
|
|
137
|
+
try {
|
|
138
|
+
result = next.value();
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
next = steps.throw(error);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
next = steps.next(result);
|
|
145
|
+
}
|
|
126
146
|
return next.value;
|
|
127
147
|
};
|
|
128
148
|
}
|
|
@@ -131,8 +151,17 @@ export function makeAsyncAdaptiveRunner(options) {
|
|
|
131
151
|
return async (ctx) => {
|
|
132
152
|
const steps = run(ctx);
|
|
133
153
|
let next = steps.next();
|
|
134
|
-
while (!next.done)
|
|
135
|
-
|
|
154
|
+
while (!next.done) {
|
|
155
|
+
let result;
|
|
156
|
+
try {
|
|
157
|
+
result = await next.value();
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
next = steps.throw(error);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
next = steps.next(result);
|
|
164
|
+
}
|
|
136
165
|
return next.value;
|
|
137
166
|
};
|
|
138
167
|
}
|
|
@@ -142,26 +171,100 @@ function routingSteps(options) {
|
|
|
142
171
|
const eligibleWorkers = options.workers.filter(worker => available(worker.agent));
|
|
143
172
|
const failedStories = new Set();
|
|
144
173
|
const makeWorker = options.makeWorker ?? ((agent, selection) => makeRunner(agent, options.idleTimeoutMs ?? 0, {
|
|
145
|
-
...options.runnerOpts,
|
|
146
|
-
permissions: options.permissions ?? 'safe',
|
|
147
|
-
selection,
|
|
174
|
+
...options.runnerOpts, permissions: options.permissions ?? 'safe', selection,
|
|
148
175
|
}));
|
|
149
176
|
return function* (ctx) {
|
|
150
|
-
const
|
|
177
|
+
const root = options.projectRoot ?? ctx.targetDir;
|
|
178
|
+
const calls = [];
|
|
179
|
+
let reservation;
|
|
180
|
+
const routeStartedAt = new Date().toISOString();
|
|
181
|
+
const blocked = (summary) => ({ success: false, summary,
|
|
182
|
+
...(calls.length ? { tokens: { ...aggregateCalls(calls), storyId: ctx.story.id, ...(reservation ? { routingAttemptId: reservation.id } : {}) } } : {}),
|
|
183
|
+
routing: { blocked: true, recordOutcome: () => undefined },
|
|
184
|
+
});
|
|
185
|
+
function* perform(role, provider, selection, work, profile, beforeInvoke) {
|
|
186
|
+
const request = { callId: randomUUID(), storyId: ctx.story.id, role, provider, selection: { ...selection } };
|
|
187
|
+
try {
|
|
188
|
+
if (!available(provider))
|
|
189
|
+
return { blocked: `Configured ${role} provider is unavailable` };
|
|
190
|
+
const refusal = options.admitCall?.(request);
|
|
191
|
+
if (refusal)
|
|
192
|
+
return { blocked: refusal };
|
|
193
|
+
beforeInvoke?.();
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return { blocked: `Call admission failed: ${error.message}` };
|
|
197
|
+
}
|
|
198
|
+
const started = now();
|
|
199
|
+
let result;
|
|
200
|
+
try {
|
|
201
|
+
result = yield work;
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
const reported = error && typeof error === 'object' ? error.tokens : undefined;
|
|
205
|
+
result = { success: false, infrastructureFailure: true, summary: error?.message ?? String(error), output: '', ...(reported ? { tokens: reported } : {}) };
|
|
206
|
+
}
|
|
207
|
+
const durationMs = Math.max(0, now() - started);
|
|
208
|
+
const callId = result.tokens?.callId ?? (result.tokens?.calls?.length === 1 ? result.tokens.calls[0].callId : undefined) ?? request.callId;
|
|
209
|
+
const call = { ...callUsage(role, provider, selection, result.tokens, durationMs, profile), callId,
|
|
210
|
+
...(reservation ? { routingAttemptId: reservation.id } : {}) };
|
|
211
|
+
calls.push(call);
|
|
212
|
+
const usage = { ...aggregateCalls([call]), callId, storyId: ctx.story.id,
|
|
213
|
+
...(reservation ? { routingAttemptId: reservation.id } : {}) };
|
|
214
|
+
let accountingError;
|
|
215
|
+
try {
|
|
216
|
+
// Earlier planning calls acquire the same execution identity once a worker
|
|
217
|
+
// has actually been admitted. Stable call IDs make reporter joins idempotent.
|
|
218
|
+
if (reservation)
|
|
219
|
+
recordRoutingAttemptUsage(root, reservation.id, { ...aggregateCalls(calls), routingAttemptId: reservation.id });
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
accountingError = error;
|
|
223
|
+
}
|
|
224
|
+
// A failed optional join must never suppress delivery of already paid
|
|
225
|
+
// usage to the caller's own durable budget account.
|
|
226
|
+
try {
|
|
227
|
+
options.onCallUsage?.({ ...request, callId, durationMs, usage });
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
accountingError ??= error;
|
|
231
|
+
}
|
|
232
|
+
if (accountingError) {
|
|
233
|
+
if (reservation) {
|
|
234
|
+
try {
|
|
235
|
+
markRoutingAttemptUsageIncomplete(root, reservation.id);
|
|
236
|
+
}
|
|
237
|
+
catch { /* unreadable state cannot provide a complete sample */ }
|
|
238
|
+
try {
|
|
239
|
+
finishRoutingAttempt(root, reservation, { verificationSuccess: false, failureKind: 'infrastructure', actualModel: result.tokens?.model });
|
|
240
|
+
}
|
|
241
|
+
catch { /* reservation remains charged and unresolved */ }
|
|
242
|
+
}
|
|
243
|
+
return { blocked: `Call accounting failed: ${accountingError.message}` };
|
|
244
|
+
}
|
|
245
|
+
return { result };
|
|
246
|
+
}
|
|
247
|
+
const capture = (provider, prompt, selection) => () => options.captureRoute
|
|
248
|
+
? options.captureRoute(provider, ctx, prompt, selection)
|
|
249
|
+
: runCapturedAgent(provider, buildWatchdogInvocation(runnerInvocation(provider, prompt, ctx.targetDir, true, 'read-only', selection), options.idleTimeoutMs ?? 0));
|
|
250
|
+
const reserveWorker = (provider, selection, profile, limit) => {
|
|
251
|
+
reservation = reserveRoutingAttempt({ root, storyId: ctx.story.id, contractKey: routingAssessmentKey(root, ctx.story), limit,
|
|
252
|
+
provider, profile, selection, startedAt: routeStartedAt, accountingScope: options.accountingScope,
|
|
253
|
+
executionPolicyKey: options.executionPolicyKey });
|
|
254
|
+
};
|
|
151
255
|
if (options.strategy === 'capability' && options.assessmentPolicy === 'prepared') {
|
|
152
256
|
try {
|
|
153
|
-
const criteria = ctx.story.acceptance;
|
|
154
|
-
if (criteria.length < 2 || criteria.length > 5 || criteria.some(c => !isAcceptanceCriterion(c) || criterionCommandProblem(c)))
|
|
155
|
-
return blocked('Prepared routing requires 2-5 executable acceptance criteria');
|
|
156
|
-
if (!readAssessment(
|
|
157
|
-
return blocked(
|
|
257
|
+
const criteria = ctx.story.acceptance, goal = options.contractKind === 'goal';
|
|
258
|
+
if (criteria.length < (goal ? 1 : 2) || (!goal && criteria.length > 5) || criteria.some(c => !isAcceptanceCriterion(c) || (!goal && criterionCommandProblem(c)) || (goal && (!c.verify.length || c.verify.some(command => !command.trim())))))
|
|
259
|
+
return blocked(goal ? 'Prepared goal routing requires bound executable acceptance criteria' : 'Prepared routing requires 2-5 executable acceptance criteria');
|
|
260
|
+
if (!readAssessment(root, ctx.story, true))
|
|
261
|
+
return blocked(`Task assessment is missing or stale. Run yoke ${goal ? 'goal' : 'prd'} assess before execution.`);
|
|
158
262
|
}
|
|
159
263
|
catch (error) {
|
|
160
264
|
return blocked(`Cannot read prepared assessment: ${error.message}`);
|
|
161
265
|
}
|
|
162
266
|
}
|
|
163
267
|
if (options.strategy === 'capability' && !options.rules?.some(rule => (!rule.area || rule.area === ctx.story.area) && (!rule.storyId || rule.storyId === ctx.story.id))) {
|
|
164
|
-
const root = options.projectRoot ?? ctx.targetDir;
|
|
165
268
|
let assessment;
|
|
166
269
|
try {
|
|
167
270
|
assessment = readAssessment(root, ctx.story, options.assessmentPolicy === 'prepared');
|
|
@@ -169,79 +272,151 @@ function routingSteps(options) {
|
|
|
169
272
|
catch (error) {
|
|
170
273
|
return blocked(`Cannot read assessment: ${error.message}`);
|
|
171
274
|
}
|
|
172
|
-
let planning;
|
|
173
|
-
const calls = [];
|
|
174
275
|
if (!assessment) {
|
|
175
|
-
const inputKey = routingAssessmentKey(root, ctx.story);
|
|
176
|
-
const prompt = [assessmentInstructions, 'Use the supplied task contract and project context to produce a bounded plan. Do not implement or change files.',
|
|
177
|
-
'Approved planning brief:', readPlanningFile(root, '.yoke/plan.md', 80_000) ?? '',
|
|
178
|
-
contextBlockFor(ctx.targetDir, ctx.story), JSON.stringify(ctx.story), 'Return exactly one line: YOKE_ASSESS {"taskClass":"implementation","difficulty":"medium","uncertainty":"low","risk":"low","scope":"low","testability":"high","reason":"evidence","approach":"steps and tests"}'].join('\n');
|
|
179
276
|
const planner = options.planner?.agent ?? options.parent;
|
|
180
|
-
if (!available(planner))
|
|
181
|
-
return blocked('Configured planning provider is unavailable');
|
|
182
277
|
const selection = { ...(options.planner?.selection ?? options.parentSelection), nativeMultiAgent: false };
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
278
|
+
let inputKey, prompt;
|
|
279
|
+
try {
|
|
280
|
+
inputKey = routingAssessmentKey(root, ctx.story);
|
|
281
|
+
prompt = buildAssessmentPrompt(root, ctx);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
return blocked(`Cannot prepare assessment: ${error.message}`);
|
|
285
|
+
}
|
|
286
|
+
const planningCall = yield* perform('orchestrator', planner, selection, capture(planner, prompt, selection));
|
|
287
|
+
if (planningCall.blocked)
|
|
288
|
+
return blocked(planningCall.blocked);
|
|
289
|
+
const planning = planningCall.result;
|
|
290
|
+
try {
|
|
291
|
+
if (routingAssessmentKey(root, ctx.story) !== inputKey)
|
|
292
|
+
return blocked('Planning inputs changed during assessment; retry planning with the current contract');
|
|
293
|
+
assessment = planning.success ? parseAssessment(planning.output) : undefined;
|
|
294
|
+
if (assessment)
|
|
295
|
+
saveAssessment(root, ctx.story, assessment, { provider: planner, model: planning.tokens?.model ?? selection.model });
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
return blocked(`Cannot persist current assessment: ${error.message}`);
|
|
299
|
+
}
|
|
192
300
|
}
|
|
193
301
|
if (!assessment)
|
|
194
|
-
return
|
|
195
|
-
|
|
302
|
+
return blocked('Routing assessment unavailable or invalid; implementation was not started');
|
|
303
|
+
let choice;
|
|
304
|
+
try {
|
|
305
|
+
choice = chooseCapability({ root, story: ctx.story, assessment, workers: eligibleWorkers, parent: options.parent, parentSelection: options.parentSelection,
|
|
306
|
+
maxAttempts: options.maxAttempts, fallback: options.fallback, maxTier: options.maxTier,
|
|
307
|
+
optimization: options.optimization, executionPolicyKey: options.executionPolicyKey });
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
return blocked(`Cannot admit routing attempt: ${error.message}`);
|
|
311
|
+
}
|
|
196
312
|
options.onDecision?.(ctx.story.id, { profile: choice.worker?.id ?? 'SELF', provider: choice.provider, model: choice.selection.model, reasoningEffort: choice.selection.reasoningEffort, variant: choice.selection.variant, providerModel: choice.selection.provider, reason: choice.reason, next: choice.next, assessment });
|
|
197
313
|
if (choice.blocked)
|
|
198
|
-
return
|
|
314
|
+
return blocked(choice.reason);
|
|
199
315
|
if (choice.exhausted)
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
316
|
+
return blocked('Routing attempt budget exhausted; replan this task before retrying');
|
|
317
|
+
let runner;
|
|
318
|
+
const implementation = yield* perform(choice.worker ? 'worker' : 'parent', choice.provider, choice.selection, () => runner({ ...ctx, attempt: reservation.ordinal, story: { ...ctx.story, assessment } }), choice.worker?.id ?? 'SELF', () => { runner = makeWorker(choice.provider, choice.selection); reserveWorker(choice.provider, choice.selection, choice.worker?.id ?? 'SELF', choice.attemptLimit); });
|
|
319
|
+
if (implementation.blocked)
|
|
320
|
+
return blocked(implementation.blocked);
|
|
321
|
+
const result = implementation.result;
|
|
204
322
|
let recorded = false;
|
|
205
323
|
const infrastructureFailure = result.infrastructureFailure || (!result.success && knownInfrastructureFailure(result.summary));
|
|
324
|
+
const recordOutcome = (verified, failureKind) => {
|
|
325
|
+
if (recorded)
|
|
326
|
+
return;
|
|
327
|
+
const kind = infrastructureFailure ? 'infrastructure' : failureKind ?? 'implementation';
|
|
328
|
+
const measured = finishRoutingAttempt(root, reservation, { verificationSuccess: infrastructureFailure ? false : verified, failureKind: kind, actualModel: result.tokens?.model });
|
|
329
|
+
const episode = routingEpisodeSummary(root, reservation);
|
|
330
|
+
recorded = true;
|
|
331
|
+
recordRoutingObservation({ projectHash: projectHash(root), storyHash: storyHash(projectHash(root), ctx.story.id), assessmentKey: reservation.contractKey, taskClass: assessment.taskClass, requiredTier: choice.requiredTier,
|
|
332
|
+
role: 'implementation', strategy: 'capability', selected: choice.worker?.id ?? 'SELF', provider: choice.provider,
|
|
333
|
+
requestedProvider: choice.selection.provider, requestedModel: choice.selection.model, requestedReasoningEffort: choice.selection.reasoningEffort, requestedVariant: choice.selection.variant,
|
|
334
|
+
actualModel: result.tokens?.model, orchestratorProvider: options.planner?.agent ?? options.parent, orchestratorModel: (options.planner?.selection ?? options.parentSelection)?.model,
|
|
335
|
+
orchestratorDurationMs: calls.filter(c => c.role === 'orchestrator').reduce((sum, call) => sum + call.durationMs, 0), workerDurationMs: calls.at(-1).durationMs,
|
|
336
|
+
processSuccess: result.success, verificationSuccess: infrastructureFailure ? false : verified, failureKind: kind,
|
|
337
|
+
usageAvailable: measured.usageComplete, inputTokens: measured.inputTokens, outputTokens: measured.outputTokens,
|
|
338
|
+
totalCostUsd: measured.totalCostUsd, costMeasurementComplete: measured.costComplete, accountingScope: measured.accountingScope,
|
|
339
|
+
totalDurationMs: measured.durationMs, executionPolicyKey: options.executionPolicyKey,
|
|
340
|
+
economicEpisode: { ...episode, assessmentSignature: assessmentSignature(assessment) },
|
|
341
|
+
});
|
|
342
|
+
};
|
|
343
|
+
if (infrastructureFailure)
|
|
344
|
+
recordOutcome(false, 'infrastructure');
|
|
206
345
|
return { ...result, summary: `route=${choice.worker?.id ?? 'SELF'} (${choice.reason}); ${result.summary}`,
|
|
207
346
|
...(infrastructureFailure ? { success: false, infrastructureFailure: true } : {}),
|
|
208
|
-
tokens: { ...aggregateCalls(calls), storyId: ctx.story.id, escalated: choice.failures > 1 },
|
|
209
|
-
routing: { blocked: infrastructureFailure || undefined, canRetry: !infrastructureFailure &&
|
|
210
|
-
if (recorded)
|
|
211
|
-
return;
|
|
212
|
-
recorded = true;
|
|
213
|
-
recordRoutingObservation({ projectHash: projectHash(root), storyHash: storyHash(projectHash(root), ctx.story.id), assessmentKey: routingAssessmentKey(root, ctx.story), taskClass: assessment.taskClass, requiredTier: choice.requiredTier,
|
|
214
|
-
role: 'implementation', strategy: 'capability', selected: choice.worker?.id ?? 'SELF', provider: choice.provider, requestedProvider: choice.selection.provider, requestedModel: choice.selection.model, requestedReasoningEffort: choice.selection.reasoningEffort, requestedVariant: choice.selection.variant,
|
|
215
|
-
actualModel: result.tokens?.model, orchestratorProvider: options.planner?.agent ?? options.parent, orchestratorModel: (options.planner?.selection ?? options.parentSelection)?.model, orchestratorDurationMs: calls.filter(c => c.role === 'orchestrator').reduce((s, c) => s + c.durationMs, 0), workerDurationMs: calls[calls.length - 1].durationMs,
|
|
216
|
-
processSuccess: result.success, verificationSuccess: infrastructureFailure ? false : verified, failureKind: infrastructureFailure ? 'infrastructure' : failureKind ?? 'implementation', usageAvailable: result.tokens !== undefined && result.tokens.measurementComplete !== false,
|
|
217
|
-
inputTokens: result.tokens?.inputTokens ?? 0, outputTokens: result.tokens?.outputTokens ?? 0, totalCostUsd: result.tokens?.totalCostUsd });
|
|
218
|
-
} } };
|
|
347
|
+
tokens: { ...aggregateCalls(calls), storyId: ctx.story.id, routingAttemptId: reservation.id, escalated: choice.failures > 1 },
|
|
348
|
+
routing: { blocked: infrastructureFailure || undefined, canRetry: !infrastructureFailure && reservation.ordinal < choice.attemptLimit, recordOutcome } };
|
|
219
349
|
}
|
|
220
|
-
// Re-rank per story so a long-running loop can use gate outcomes learned by
|
|
221
|
-
// earlier stories without rebuilding the runner.
|
|
222
350
|
const rule = options.rules?.find(rule => (!rule.area || rule.area === ctx.story.area) && (!rule.storyId || rule.storyId === ctx.story.id) && (rule.area || rule.storyId));
|
|
351
|
+
const bounded = options.strategy === 'capability' || options.maxAttempts !== undefined;
|
|
352
|
+
let priorAttempts = [];
|
|
353
|
+
let failureKey = ctx.story.id;
|
|
354
|
+
if (bounded) {
|
|
355
|
+
try {
|
|
356
|
+
failureKey = routingAssessmentKey(root, ctx.story);
|
|
357
|
+
priorAttempts = readRoutingAttempts(root, ctx.story.id, failureKey);
|
|
358
|
+
if (priorAttempts.length >= (options.maxAttempts ?? 5))
|
|
359
|
+
return blocked('Routing attempt budget exhausted; replan this task before retrying');
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
return blocked(`Cannot read routing attempt state: ${error.message}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
223
365
|
if (rule) {
|
|
224
|
-
const project = projectHash(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
366
|
+
const project = projectHash(root);
|
|
367
|
+
try {
|
|
368
|
+
const prior = bounded ? priorAttempts.filter(entry => entry.outcome?.failureKind !== 'infrastructure').at(-1)?.outcome
|
|
369
|
+
: readRoutingObservations().reverse().find(event => event.projectHash === project && event.storyHash === storyHash(project, ctx.story.id) && typeof event.verificationSuccess === 'boolean');
|
|
370
|
+
if (prior?.verificationSuccess === false)
|
|
371
|
+
failedStories.add(failureKey);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
return blocked(`Cannot read routing attempt state: ${error.message}`);
|
|
375
|
+
}
|
|
228
376
|
}
|
|
229
|
-
const ruleWorker = rule && failedStories.has(
|
|
377
|
+
const ruleWorker = rule && failedStories.has(failureKey) ? rule.escalateTo ?? 'SELF' : rule?.worker;
|
|
230
378
|
const candidates = rule ? eligibleWorkers : rankWorkers(eligibleWorkers, options.strategy, options.maxCandidates);
|
|
231
379
|
if (candidates.length === 0) {
|
|
232
380
|
if (options.fallback === 'block' || options.maxTier)
|
|
233
381
|
return blocked('No eligible routing profiles; parent fallback is disabled');
|
|
234
|
-
|
|
382
|
+
let runner;
|
|
383
|
+
const selection = options.parentSelection ?? {};
|
|
384
|
+
options.onDecision?.(ctx.story.id, { profile: 'SELF', provider: options.parent, model: selection.model, providerModel: selection.provider,
|
|
385
|
+
reasoningEffort: selection.reasoningEffort, variant: selection.variant, reason: 'No eligible profile; configured parent fallback', next: 'independent gate verification' });
|
|
386
|
+
const execution = yield* perform('parent', options.parent, selection, () => runner(ctx), 'SELF', () => {
|
|
387
|
+
runner = makeWorker(options.parent, selection);
|
|
388
|
+
if (bounded)
|
|
389
|
+
reserveWorker(options.parent, selection, 'SELF', options.maxAttempts ?? 5);
|
|
390
|
+
});
|
|
391
|
+
if (execution.blocked)
|
|
392
|
+
return blocked(execution.blocked);
|
|
393
|
+
const result = execution.result;
|
|
394
|
+
const infrastructureFailure = result.infrastructureFailure || (!result.success && knownInfrastructureFailure(result.summary));
|
|
395
|
+
let recorded = false;
|
|
396
|
+
const recordOutcome = (verified, failureKind) => {
|
|
397
|
+
if (recorded)
|
|
398
|
+
return;
|
|
399
|
+
const kind = infrastructureFailure ? 'infrastructure' : failureKind ?? 'implementation';
|
|
400
|
+
if (reservation)
|
|
401
|
+
finishRoutingAttempt(root, reservation, { verificationSuccess: infrastructureFailure ? false : verified, failureKind: kind, actualModel: result.tokens?.model });
|
|
402
|
+
recorded = true;
|
|
403
|
+
};
|
|
404
|
+
if (infrastructureFailure)
|
|
405
|
+
recordOutcome(false, 'infrastructure');
|
|
406
|
+
return { ...result, ...(infrastructureFailure ? { success: false, infrastructureFailure: true } : {}),
|
|
407
|
+
tokens: { ...aggregateCalls(calls), storyId: ctx.story.id, ...(reservation ? { routingAttemptId: reservation.id } : {}) },
|
|
408
|
+
routing: { recordOutcome, ...(infrastructureFailure ? { blocked: true } : {}), ...(bounded ? { canRetry: !infrastructureFailure && reservation.ordinal < (options.maxAttempts ?? 5) } : {}) } };
|
|
235
409
|
}
|
|
236
|
-
const prompt = buildRoutingPrompt(ctx, candidates, options.strategy);
|
|
237
410
|
const orchestratorSelection = { ...(options.parentSelection ?? {}), ...(options.orchestratorSelection ?? {}), nativeMultiAgent: false };
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
411
|
+
let routeRun = { success: true, summary: 'Explicit rule', output: '' };
|
|
412
|
+
if (!rule) {
|
|
413
|
+
const routed = yield* perform('orchestrator', options.parent, orchestratorSelection, capture(options.parent, buildRoutingPrompt(ctx, candidates, options.strategy), orchestratorSelection));
|
|
414
|
+
if (routed.blocked)
|
|
415
|
+
return blocked(routed.blocked);
|
|
416
|
+
routeRun = routed.result;
|
|
417
|
+
}
|
|
243
418
|
const decision = rule
|
|
244
|
-
? { worker: ruleWorker === 'SELF' || candidates.some(w => w.id === ruleWorker) ? ruleWorker : 'SELF', reason: failedStories.has(
|
|
419
|
+
? { worker: ruleWorker === 'SELF' || candidates.some(w => w.id === ruleWorker) ? ruleWorker : 'SELF', reason: failedStories.has(failureKey) ? 'gate failure escalated by project rule' : 'explicit project routing rule' }
|
|
245
420
|
: routeRun.success ? parseRouteDecision(routeRun.output, candidates.map(worker => worker.id)) : null;
|
|
246
421
|
const selected = decision?.worker ?? 'SELF';
|
|
247
422
|
const worker = selected === 'SELF' ? undefined : candidates.find(candidate => candidate.id === selected);
|
|
@@ -251,75 +426,55 @@ function routingSteps(options) {
|
|
|
251
426
|
const selection = worker
|
|
252
427
|
? { provider: worker.provider, model: worker.model, reasoningEffort: worker.reasoningEffort, variant: worker.variant, nativeMultiAgent: false, ...(provider !== 'gemini' && provider !== 'qwen' && provider !== 'pi' && provider !== 'hermes' ? { bare: options.parentSelection?.bare } : {}) }
|
|
253
428
|
: { ...(options.parentSelection ?? {}), nativeMultiAgent: false };
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
: {}),
|
|
268
|
-
...((routeRun.tokens?.cacheWriteInputTokens !== undefined || result.tokens?.cacheWriteInputTokens !== undefined)
|
|
269
|
-
? { cacheWriteInputTokens: (routeRun.tokens?.cacheWriteInputTokens ?? 0) + (result.tokens?.cacheWriteInputTokens ?? 0) }
|
|
270
|
-
: {}),
|
|
271
|
-
outputTokens: (routeRun.tokens?.outputTokens ?? 0) + (result.tokens?.outputTokens ?? 0),
|
|
272
|
-
...((routeRun.tokens?.reasoningOutputTokens !== undefined || result.tokens?.reasoningOutputTokens !== undefined)
|
|
273
|
-
? { reasoningOutputTokens: (routeRun.tokens?.reasoningOutputTokens ?? 0) + (result.tokens?.reasoningOutputTokens ?? 0) }
|
|
274
|
-
: {}),
|
|
275
|
-
...((routeRun.tokens?.totalCostUsd !== undefined || result.tokens?.totalCostUsd !== undefined)
|
|
276
|
-
? { totalCostUsd: (routeRun.tokens?.totalCostUsd ?? 0) + (result.tokens?.totalCostUsd ?? 0) }
|
|
277
|
-
: {}),
|
|
278
|
-
...(result.tokens?.model ? { model: result.tokens.model } : routeRun.tokens?.model ? { model: routeRun.tokens.model } : {}),
|
|
279
|
-
calls,
|
|
280
|
-
measurementComplete: calls.every(call => call.usageAvailable),
|
|
281
|
-
costMeasurementComplete: calls.every(call => call.totalCostUsd !== undefined),
|
|
282
|
-
};
|
|
429
|
+
options.onDecision?.(ctx.story.id, { profile: selected, provider, model: selection.model, providerModel: selection.provider,
|
|
430
|
+
reasoningEffort: selection.reasoningEffort, variant: selection.variant, reason: decision?.reason ?? 'Invalid controller response; configured parent fallback',
|
|
431
|
+
next: rule?.escalateTo ?? 'independent gate verification', ...(ctx.story.assessment ? { assessment: ctx.story.assessment } : {}) });
|
|
432
|
+
let runner;
|
|
433
|
+
const execution = yield* perform(worker ? 'worker' : 'parent', provider, selection, () => runner(ctx), selected, () => {
|
|
434
|
+
runner = makeWorker(provider, selection);
|
|
435
|
+
if (bounded)
|
|
436
|
+
reserveWorker(provider, selection, selected, options.maxAttempts ?? 5);
|
|
437
|
+
});
|
|
438
|
+
if (execution.blocked)
|
|
439
|
+
return blocked(execution.blocked);
|
|
440
|
+
const result = execution.result;
|
|
441
|
+
const tokens = { ...aggregateCalls(calls), storyId: ctx.story.id, escalated: Boolean(rule && failedStories.has(failureKey)), ...(reservation ? { routingAttemptId: reservation.id } : {}) };
|
|
283
442
|
let recorded = false;
|
|
284
|
-
const recordOutcome = (verificationSuccess) => {
|
|
443
|
+
const recordOutcome = (verificationSuccess, failureKind) => {
|
|
285
444
|
if (recorded)
|
|
286
445
|
return;
|
|
446
|
+
if (reservation)
|
|
447
|
+
finishRoutingAttempt(root, reservation, { verificationSuccess, failureKind, actualModel: result.tokens?.model });
|
|
287
448
|
recorded = true;
|
|
288
|
-
if (!verificationSuccess)
|
|
289
|
-
failedStories.add(
|
|
290
|
-
else
|
|
291
|
-
failedStories.delete(
|
|
292
|
-
const project = projectHash(
|
|
293
|
-
recordRoutingObservation({
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
...(selection.reasoningEffort ? { requestedReasoningEffort: selection.reasoningEffort } : {}),
|
|
301
|
-
...(selection.provider ? { requestedProvider: selection.provider } : {}),
|
|
302
|
-
...(selection.variant ? { requestedVariant: selection.variant } : {}),
|
|
303
|
-
...(result.tokens?.model ? { actualModel: result.tokens.model } : {}),
|
|
304
|
-
orchestratorProvider: options.parent,
|
|
305
|
-
...(orchestratorSelection.model ? { orchestratorModel: orchestratorSelection.model } : {}),
|
|
306
|
-
orchestratorDurationMs,
|
|
307
|
-
workerDurationMs,
|
|
308
|
-
processSuccess: result.success,
|
|
309
|
-
verificationSuccess,
|
|
310
|
-
inputTokens: tokens.inputTokens,
|
|
311
|
-
outputTokens: tokens.outputTokens,
|
|
312
|
-
});
|
|
449
|
+
if (!verificationSuccess && failureKind !== 'infrastructure')
|
|
450
|
+
failedStories.add(failureKey);
|
|
451
|
+
else if (verificationSuccess)
|
|
452
|
+
failedStories.delete(failureKey);
|
|
453
|
+
const project = projectHash(root);
|
|
454
|
+
recordRoutingObservation({ projectHash: project, storyHash: storyHash(project, ctx.story.id), strategy: options.strategy, selected, provider,
|
|
455
|
+
requestedModel: selection.model, requestedReasoningEffort: selection.reasoningEffort, requestedProvider: selection.provider, requestedVariant: selection.variant,
|
|
456
|
+
actualModel: result.tokens?.model, orchestratorProvider: options.parent, orchestratorModel: orchestratorSelection.model,
|
|
457
|
+
orchestratorDurationMs: calls.filter(call => call.role === 'orchestrator').reduce((sum, call) => sum + call.durationMs, 0), workerDurationMs: calls.at(-1).durationMs,
|
|
458
|
+
processSuccess: result.success, verificationSuccess, failureKind,
|
|
459
|
+
inputTokens: tokens.inputTokens, outputTokens: tokens.outputTokens, usageAvailable: tokens.measurementComplete,
|
|
460
|
+
totalCostUsd: tokens.totalCostUsd, costMeasurementComplete: tokens.costMeasurementComplete, });
|
|
313
461
|
};
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return { ...result, summary: `${routeSummary}; ${result.summary}`, tokens, routing: { recordOutcome } }
|
|
462
|
+
if (result.infrastructureFailure)
|
|
463
|
+
recordOutcome(false, 'infrastructure');
|
|
464
|
+
const routeSummary = decision ? `route=${selected} (${decision.reason})` : `route=SELF (${routeRun.success ? 'invalid routing response' : 'orchestrator failed'})`;
|
|
465
|
+
return { ...result, summary: `${routeSummary}; ${result.summary}`, tokens, routing: { recordOutcome, ...(result.infrastructureFailure ? { blocked: true } : {}),
|
|
466
|
+
...(bounded ? { canRetry: !result.infrastructureFailure && reservation.ordinal < (options.maxAttempts ?? 5) } : {}) } };
|
|
318
467
|
};
|
|
319
468
|
}
|
|
320
469
|
function aggregateCalls(calls) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
470
|
+
const optional = {};
|
|
471
|
+
for (const field of ['cachedInputTokens', 'cacheWriteInputTokens', 'reasoningOutputTokens', 'totalCostUsd']) {
|
|
472
|
+
if (calls.some(call => call[field] !== undefined))
|
|
473
|
+
optional[field] = calls.reduce((sum, call) => sum + (call[field] ?? 0), 0);
|
|
474
|
+
}
|
|
475
|
+
const model = calls.at(-1)?.actualModel;
|
|
476
|
+
return { inputTokens: calls.reduce((sum, call) => sum + call.inputTokens, 0), outputTokens: calls.reduce((sum, call) => sum + call.outputTokens, 0),
|
|
477
|
+
...optional, ...(model ? { model } : {}), calls,
|
|
478
|
+
measurementComplete: calls.every(call => call.usageAvailable !== false),
|
|
479
|
+
costMeasurementComplete: calls.every(call => call.totalCostUsd !== undefined && call.costMeasurementComplete !== false) };
|
|
325
480
|
}
|
package/dist/setup/command.js
CHANGED
|
@@ -46,7 +46,10 @@ export function defaultRoutingWorkers(agents) {
|
|
|
46
46
|
{ id: 'hermes-standard', agent: 'hermes', tier: 'standard', costTier: 'medium', capabilities: ['implementation'] },
|
|
47
47
|
],
|
|
48
48
|
};
|
|
49
|
-
return agents.flatMap(agent => workers[agent])
|
|
49
|
+
return agents.flatMap(agent => workers[agent]).map(worker => ({
|
|
50
|
+
...worker,
|
|
51
|
+
profileMetadata: { version: 1, catalog: 'yoke-1.22.0', source: 'yoke-default', basis: 'configured-prior' },
|
|
52
|
+
}));
|
|
50
53
|
}
|
|
51
54
|
function parseAgents(value, fallback) {
|
|
52
55
|
if (value.trim().toLowerCase() === 'all')
|
|
@@ -173,7 +176,9 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
173
176
|
const workers = rawWorkers.map(w => {
|
|
174
177
|
const model = agentModels[w.agent] ?? w.model;
|
|
175
178
|
const reasoningEffort = agentReasoning[w.agent] ?? w.reasoningEffort;
|
|
176
|
-
return { ...w, ...(model ? { model } : {}), ...(reasoningEffort ? { reasoningEffort } : {})
|
|
179
|
+
return { ...w, ...(model ? { model } : {}), ...(reasoningEffort ? { reasoningEffort } : {}),
|
|
180
|
+
...((agentModels[w.agent] || agentReasoning[w.agent]) && w.profileMetadata ? { profileMetadata: { ...w.profileMetadata, source: 'operator' } } : {}),
|
|
181
|
+
};
|
|
177
182
|
});
|
|
178
183
|
config.routing = {
|
|
179
184
|
...config.routing,
|
|
@@ -182,6 +187,7 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
182
187
|
maxCandidates: config.routing?.maxCandidates ?? 3,
|
|
183
188
|
assessmentPolicy: existing?.routing?.assessmentPolicy ?? (existing ? 'on-demand' : 'prepared'),
|
|
184
189
|
fallback: existing?.routing?.fallback ?? (existing ? 'parent' : 'block'),
|
|
190
|
+
...(!existing && !config.routing?.optimization ? { optimization: { version: 1, objective: 'balanced', minSamples: 20 } } : {}),
|
|
185
191
|
...(config.routing?.orchestrator ? { orchestrator: config.routing.orchestrator } : {}),
|
|
186
192
|
workers,
|
|
187
193
|
};
|