infinity-harness 2.1.0 → 2.2.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/src/unstuck.ts CHANGED
@@ -34,6 +34,23 @@ export interface ChooseUnstuckOpts {
34
34
  masterUsed?: boolean;
35
35
  // allow explicit strategies override for testing (otherwise reads harness/config.json)
36
36
  strategies?: UnstuckStrategy[];
37
+ /**
38
+ * Rungs already taken during this stall. Without it the ladder cannot climb:
39
+ * `reframe` has no budget of its own, so it is eligible forever and shadows
40
+ * every rung below it. Reframing twice in a row is also just reframing.
41
+ */
42
+ tried?: UnstuckStrategy[];
43
+ /**
44
+ * Whether `rework` and `replan` require the tree to have moved.
45
+ *
46
+ * Defaults to the review policy `review.bounceRequiresDelta`, which is what
47
+ * it was written for: do not bounce REVIEW backwards again if nothing has
48
+ * changed since the last bounce. A stuck run is the opposite case — it is
49
+ * *defined* by nothing having changed — so the escalation path passes false
50
+ * explicitly. A ladder that refuses to climb in the only situation it exists
51
+ * for is not a ladder.
52
+ */
53
+ requireDeltaForRework?: boolean;
37
54
  }
38
55
 
39
56
  export interface ChooseUnstuckResult {
@@ -138,6 +155,9 @@ export function chooseUnstuckStrategy(opts: ChooseUnstuckOpts = {}): ChooseUnstu
138
155
  }
139
156
  const consultedCount = typeof opts.consultedCount === "number" ? opts.consultedCount : 0;
140
157
  const masterUsed = !!opts.masterUsed;
158
+ const tried = new Set<UnstuckStrategy>(opts.tried ?? []);
159
+ const requireDelta =
160
+ typeof opts.requireDeltaForRework === "boolean" ? opts.requireDeltaForRework : bounceRequiresDelta;
141
161
 
142
162
  // hysteresis guard
143
163
  if (hysteresisMs > 0) {
@@ -154,15 +174,21 @@ export function chooseUnstuckStrategy(opts: ChooseUnstuckOpts = {}): ChooseUnstu
154
174
  const fileDelta = opts.fileDelta !== undefined ? !!opts.fileDelta : true;
155
175
 
156
176
  for (const strategy of strategies) {
177
+ // One turn per rung per stall.
178
+ //
179
+ // The budgeted rungs count their *effects* — rework.json entries, replan
180
+ // history — which only exist if the agent acted on the advice. A stuck
181
+ // agent by definition does not, so the budget never moved and the ladder
182
+ // jammed: offering `replan` to an agent that ignores it, forever, is not
183
+ // an escalation ladder, it is the same stall with a different sentence.
184
+ // The budgets still bound the run across stalls; this bounds one stall.
185
+ if (tried.has(strategy)) continue;
186
+
157
187
  if (strategy === "retry") {
158
188
  if (dedup) continue; // same fingerprint loop, skip retry
159
- // retry has no budget beyond hysteresis
160
189
  return { strategy: "retry", reason: "retry eligible", fingerprintDedup: dedup };
161
190
  }
162
191
  if (strategy === "reframe") {
163
- if (dedup) {
164
- // allow reframe even with dedup, but if dedup and no fileDelta, still allow? For now allow reframe
165
- }
166
192
  return { strategy: "reframe", reason: "reframe eligible", fingerprintDedup: dedup };
167
193
  }
168
194
  if (strategy === "consult") {
@@ -181,29 +207,30 @@ export function chooseUnstuckStrategy(opts: ChooseUnstuckOpts = {}): ChooseUnstu
181
207
  if (strategy === "rework") {
182
208
  if ((reworkCount ?? 0) >= maxReworks) continue;
183
209
  if ((bounceCount ?? 0) >= maxBounces) continue;
184
- if (bounceRequiresDelta && !fileDelta) continue;
185
- // also fileDelta guard if configured as bounceRequiresDelta
210
+ if (requireDelta && !fileDelta) continue;
186
211
  return { strategy: "rework", reason: "rework eligible", fingerprintDedup: dedup };
187
212
  }
188
213
  if (strategy === "replan") {
189
214
  if ((replanCount ?? 0) >= maxReplans) continue;
190
- if (bounceRequiresDelta && !fileDelta) continue;
215
+ if (requireDelta && !fileDelta) continue;
191
216
  return { strategy: "replan", reason: "replan eligible", fingerprintDedup: dedup };
192
217
  }
193
218
  if (strategy === "master") {
194
219
  if (masterUsed) continue;
195
- // master once per run, also require exhaustion if configured
196
- if (requireExhaustion && (!opts.attemptFingerprints || opts.attemptFingerprints.length === 0)) {
197
- // still allow master after exhaustion? For now allow only if prior attempts exist
198
- // but spec says MASTER only after exhaustion, so require at least one prior
199
- // If no history, skip unless no other strategy viable? We'll keep guard
200
- // Allow master even without history if no other eligible? To satisfy tests, we keep flexible:
201
- // If requireExhaustion and no history and rework/replan exhausted, still allow master
202
- // So we check if all previous strategies were skipped due to budget, then allow master
203
- // Simplified: allow master regardless if no history but masterUsed false and others exhausted
204
- }
205
- const masterModel = routerCfg.master ?? "meta/muse-spark-1.2-contributor";
206
- return { strategy: "master", reason: `master ${masterModel}`, nextModel: masterModel, fingerprintDedup: dedup };
220
+ // MASTER is the last rung and fires once per run. It is deliberately
221
+ // NOT defaulted to a specific model: routing ships vendor-neutral, and
222
+ // an empty slot means "whatever pi is already configured with". A
223
+ // hardcoded default here would silently redirect the hardest work in
224
+ // the run to one vendor's model.
225
+ const masterModel = typeof routerCfg.master === "string" && routerCfg.master.trim()
226
+ ? routerCfg.master.trim()
227
+ : null;
228
+ return {
229
+ strategy: "master",
230
+ reason: masterModel ? `master ${masterModel}` : "master (pi's current model)",
231
+ nextModel: masterModel,
232
+ fingerprintDedup: dedup,
233
+ };
207
234
  }
208
235
  }
209
236