omp-conductor 0.18.0 → 0.18.2
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/README.md +35 -1
- package/REFERENCE.md +61 -11
- package/agents/to-spec.md +94 -0
- package/package.json +2 -1
- package/schema/config.schema.json +35 -1
- package/src/admission.ts +204 -75
- package/src/arm-challenge.ts +250 -57
- package/src/ask.ts +268 -7
- package/src/board.ts +17 -3
- package/src/briefs/orchestrator.md +62 -21
- package/src/briefs/to-spec.md +88 -0
- package/src/briefs/worker.md +2 -1
- package/src/cli.ts +124 -1
- package/src/command-help.ts +11 -0
- package/src/command-manifest.ts +38 -5
- package/src/commands/arm.ts +1 -1
- package/src/commands/context.ts +1 -0
- package/src/commands/drain.ts +176 -0
- package/src/commands/extend.ts +6 -10
- package/src/commands/intake.ts +4 -19
- package/src/commands/status.ts +5 -1
- package/src/commands/watch.ts +51 -16
- package/src/commands/worker.ts +9 -10
- package/src/config-schema.ts +43 -6
- package/src/config.ts +65 -9
- package/src/daemon.ts +879 -41
- package/src/dashboard/app.js +4 -1
- package/src/dashboard/server.ts +5 -2
- package/src/decisions.ts +243 -17
- package/src/diff-flags.ts +75 -1
- package/src/doctor.ts +60 -82
- package/src/escalate.ts +31 -14
- package/src/failure-class.ts +28 -2
- package/src/fleet.ts +239 -240
- package/src/gitops.ts +188 -81
- package/src/graph-health.ts +35 -1
- package/src/graph.ts +66 -1
- package/src/harness-loader.ts +59 -0
- package/src/host.ts +242 -2
- package/src/lifecycle.ts +122 -1
- package/src/omp-settings.ts +19 -0
- package/src/omp.ts +183 -21
- package/src/orchestrator-tick.ts +1591 -32
- package/src/orchestrator.ts +12 -0
- package/src/privileged.ts +1 -4
- package/src/release-policy.ts +503 -9
- package/src/session-host.ts +65 -6
- package/src/settlement.ts +69 -17
- package/src/setup-host.ts +1225 -9
- package/src/setup-install.ts +28 -0
- package/src/setup-wizard.ts +154 -3
- package/src/setup.ts +83 -17
- package/src/shell.ts +15 -0
- package/src/status-render.ts +216 -12
- package/src/store.ts +443 -42
- package/src/to-spec.ts +408 -0
- package/src/tracker/github.ts +104 -14
- package/src/types.ts +405 -19
- package/src/upgrade-verify.ts +209 -2
- package/src/upgrade.ts +175 -1
- package/src/verbs/protocol.ts +39 -0
- package/src/verbs/server.ts +765 -56
- package/src/verbs/socket.ts +24 -5
- package/src/worker.ts +12 -2
- package/src/worktree.ts +29 -12
package/src/admission.ts
CHANGED
|
@@ -18,9 +18,13 @@ import { join } from "node:path";
|
|
|
18
18
|
import { log, errText, safeEscalate } from "./log.ts";
|
|
19
19
|
import type {
|
|
20
20
|
Caps,
|
|
21
|
+
EffectiveModel,
|
|
21
22
|
Escalation,
|
|
23
|
+
FileLane,
|
|
22
24
|
IssueComment,
|
|
23
25
|
IssueSnapshot,
|
|
26
|
+
LaneDeclaration,
|
|
27
|
+
ModelDeclaration,
|
|
24
28
|
OpenCloser,
|
|
25
29
|
ProjectConfig,
|
|
26
30
|
RunRecord,
|
|
@@ -94,11 +98,16 @@ export function hasFailedAttemptBudget(failures: number, maxAttempts: number): b
|
|
|
94
98
|
* (#608): the exact snapshot the overlap gate enforced. Dispatch renders this
|
|
95
99
|
* value into the worker brief, so a changed or failed second comment read can
|
|
96
100
|
* neither hide nor reword the lane admission held — the gate and the
|
|
97
|
-
* worker-visible brief are one value, not two reads of the same thread.
|
|
101
|
+
* worker-visible brief are one value, not two reads of the same thread.
|
|
102
|
+
* `model` is the effective `Model:` declaration resolved from the same read
|
|
103
|
+
* (#535): not a gate, but the same one-value contract — dispatch launches on
|
|
104
|
+
* this selector and the brief echoes it, so a second read can neither hide
|
|
105
|
+
* the model admission held nor drift from it. */
|
|
98
106
|
export interface Admission {
|
|
99
107
|
r: Routed;
|
|
100
108
|
attempt: number;
|
|
101
109
|
lane?: FileLane;
|
|
110
|
+
model?: EffectiveModel;
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
export interface AdmissionHold {
|
|
@@ -192,20 +201,42 @@ function planUsageEscalation(project: string, plan: PlanUsageStatus): Escalation
|
|
|
192
201
|
* files an issue declares it will touch (#555).
|
|
193
202
|
*
|
|
194
203
|
* The orchestrator already writes exactly this list into every promotion brief
|
|
195
|
-
* in prose; this parses that same
|
|
196
|
-
* pre-dispatch comment, which is the same
|
|
204
|
+
* in prose; this parses that same declaration out of the issue body — or out of
|
|
205
|
+
* a pre-dispatch comment, which is the same declaration on a supported brief
|
|
197
206
|
* surface since #517 — so the interlock is load-bearing rather than advisory.
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* (
|
|
207
|
+
* Two spellings are accepted, and both work on every surface:
|
|
208
|
+
*
|
|
209
|
+
* - The inline line: a line beginning with "file lane" (case-insensitive,
|
|
210
|
+
* optional bold/heading markers) whose rest carries the paths as
|
|
211
|
+
* backtick-delimited spans (the brief form), with a bare
|
|
212
|
+
* comma/space-separated fallback that keeps tokens that look like relative
|
|
213
|
+
* paths.
|
|
214
|
+
* - The write-lane section (#825): a markdown heading "Exact write lane" (or
|
|
215
|
+
* "Write lane" / "write-lane") whose immediately following bullet items
|
|
216
|
+
* carry the paths — the package-floor decomposition format, where a groomed
|
|
217
|
+
* child names the files it writes as one backticked path per bullet. Only
|
|
218
|
+
* the contiguous bullet run under that heading is read: a following
|
|
219
|
+
* paragraph (like a "Read only:" caveat) or a later section ends it, so
|
|
220
|
+
* read-only entry points, proof commands and acceptance bullets elsewhere in
|
|
221
|
+
* the issue are never captured.
|
|
222
|
+
*
|
|
223
|
+
* Inline wins when both are present — the machine-shaped sentence has held
|
|
224
|
+
* since #555, and the mediated label echo shows exactly which declaration
|
|
225
|
+
* admission enforces. A line or section whose tokens are not pathlike (like
|
|
226
|
+
* `File lane: none`) is not a declaration. Anything else — including an
|
|
203
227
|
* absent declaration, which is the default — is an empty lane: the issue is
|
|
204
228
|
* admitted exactly as today (`fail open`), and the gate never refuses work for
|
|
205
|
-
* wanting a lane.
|
|
206
|
-
*
|
|
229
|
+
* wanting a lane. A clearly delimited write-lane section that parsed nothing
|
|
230
|
+
* is NOT an absent declaration: the promotion verb refuses it with an
|
|
231
|
+
* actionable syntax error ({@link writeLaneSectionHeading}) rather than
|
|
232
|
+
* echoing a fail-open the section's own heading contradicts.
|
|
207
233
|
*/
|
|
208
234
|
export function laneDeclaration(text: string): LaneDeclaration | undefined {
|
|
235
|
+
return inlineLaneDeclaration(text) ?? sectionLaneDeclaration(text);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The inline `File lane:`/`File-lane=` sentence grammar (see {@link laneDeclaration}). */
|
|
239
|
+
function inlineLaneDeclaration(text: string): LaneDeclaration | undefined {
|
|
209
240
|
const match = text.match(
|
|
210
241
|
/^\s*(?:[#>*-]\s*)*file[- ]lane\s*[:=]\s*([^\n]*)$/im,
|
|
211
242
|
);
|
|
@@ -222,34 +253,58 @@ export function laneDeclaration(text: string): LaneDeclaration | undefined {
|
|
|
222
253
|
return { files, source: match[0].trim() };
|
|
223
254
|
}
|
|
224
255
|
|
|
225
|
-
/** One parsed `File lane:` declaration: the paths and the verbatim source
|
|
226
|
-
* line, so a renderer can reproduce the declaration itself rather than a
|
|
227
|
-
* summary of it. Exported for the brief's guarantee that the gate's effective
|
|
228
|
-
* lane is always visible to the worker (#608). */
|
|
229
|
-
export interface LaneDeclaration {
|
|
230
|
-
files: string[];
|
|
231
|
-
/** The declaration line verbatim, as written on the surface it came from. */
|
|
232
|
-
source: string;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
256
|
/**
|
|
236
|
-
*
|
|
237
|
-
* (#
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
* the
|
|
241
|
-
*
|
|
242
|
-
* worker. `at` records which surface won (`"body"`, or the 0-based comment
|
|
243
|
-
* index), letting the brief reproduce the declaration verbatim when the
|
|
244
|
-
* winning comment sits beyond its rendered discussion budget.
|
|
257
|
+
* A markdown heading that opens a clearly delimited write-lane section
|
|
258
|
+
* (#825): `## Exact write lane`, `## Write lane`, or a hyphenated spelling,
|
|
259
|
+
* optional trailing colon. Returns the heading line verbatim, or undefined.
|
|
260
|
+
* The promotion verb uses this to refuse a section that parsed to nothing —
|
|
261
|
+
* the echo must never claim fail-open for a heading that plainly tried to
|
|
262
|
+
* declare one.
|
|
245
263
|
*/
|
|
246
|
-
export
|
|
247
|
-
|
|
264
|
+
export function writeLaneSectionHeading(text: string): string | undefined {
|
|
265
|
+
const match = text.match(
|
|
266
|
+
/^[ \t]*#{1,6}[ \t]+(?:exact[ \t]+)?write[-\s]lane[ \t]*[:.]?[ \t]*$/im,
|
|
267
|
+
);
|
|
268
|
+
return match === null ? undefined : match[0].trim();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** The write-lane section grammar (see {@link laneDeclaration}): the heading
|
|
272
|
+
* plus the contiguous bullet run beneath it. A non-bullet line ends the run,
|
|
273
|
+
* so a caveat paragraph or the next section bounds the declaration. */
|
|
274
|
+
function sectionLaneDeclaration(text: string): LaneDeclaration | undefined {
|
|
275
|
+
const lines = text.split("\n");
|
|
276
|
+
let start = -1;
|
|
277
|
+
for (let i = 0; i < lines.length; i++) {
|
|
278
|
+
if (writeLaneSectionHeading(lines[i]!) !== undefined) {
|
|
279
|
+
start = i;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (start < 0) return undefined;
|
|
284
|
+
const files: string[] = [];
|
|
285
|
+
const bullets: string[] = [];
|
|
286
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
287
|
+
const line = lines[i]!;
|
|
288
|
+
if (line.trim() === "") continue;
|
|
289
|
+
// A markdown bullet marker (`- `, `* `, `+ `, `1. `), with an optional
|
|
290
|
+
// task checkbox. Anything else ends the section's bullet run.
|
|
291
|
+
const marker = line.match(/^[ \t]*(?:[-*+]|\d+[.)])[ \t]+(?:\[[ xX]\][ \t]+)?/);
|
|
292
|
+
if (marker === null) break;
|
|
293
|
+
const rest = line.slice(marker[0].length);
|
|
294
|
+
const backticked = [...rest.matchAll(/`([^`]+)`/g)]
|
|
295
|
+
.map((m) => m[1]!.trim())
|
|
296
|
+
.filter(isPathLike);
|
|
297
|
+
files.push(...(backticked.length > 0 ? backticked : rest.split(/[,\s]+/).map((s) => s.trim()).filter(isPathLike)));
|
|
298
|
+
bullets.push(line.trim());
|
|
299
|
+
}
|
|
300
|
+
if (files.length === 0) return undefined;
|
|
301
|
+
return { files: [...new Set(files)], source: [lines[start]!.trim(), ...bullets].join("\n") };
|
|
248
302
|
}
|
|
249
303
|
|
|
250
304
|
/** Who holds one file's lane this admission pass, and which read proved it.
|
|
251
|
-
* `"declared"`
|
|
252
|
-
*
|
|
305
|
+
* `"declared"` covers both occupancy halves: an admitted candidate's own
|
|
306
|
+
* lane for the rest of its pass, and — since #744 — a live run's persisted
|
|
307
|
+
* declaration on every later pass, without any probe read. */
|
|
253
308
|
export type LaneHolder = { issue: number; source: LaneSource | "declared" };
|
|
254
309
|
|
|
255
310
|
/** Resolves the effective lane across the body and the whole comment thread. */
|
|
@@ -270,6 +325,41 @@ export function declaredLane(body: string): string[] {
|
|
|
270
325
|
return laneDeclaration(body)?.files ?? [];
|
|
271
326
|
}
|
|
272
327
|
|
|
328
|
+
/**
|
|
329
|
+
* The model half of a promotion brief (#535): the selector an issue declares
|
|
330
|
+
* it will dispatch on, read from the body or a pre-dispatch comment — the
|
|
331
|
+
* same two surfaces `File lane:` reads. Conductor names a selector and omp
|
|
332
|
+
* resolves what it means, so the declaration is one token in omp's own model
|
|
333
|
+
* syntax — a role alias (`@slow`) or a concrete `provider/model`, either
|
|
334
|
+
* optionally `:thinking`-suffixed — passed through verbatim as the session's
|
|
335
|
+
* `modelPattern`. A line whose rest is not exactly one selector (blank,
|
|
336
|
+
* `none`, multi-word prose) is not a declaration: dispatch fails open to the
|
|
337
|
+
* project's `workerModel`, exactly as a lane-less issue is admitted today.
|
|
338
|
+
* This is the one grammar for all surfaces; there is no second comment-only
|
|
339
|
+
* spelling.
|
|
340
|
+
*/
|
|
341
|
+
export function modelDeclaration(text: string): ModelDeclaration | undefined {
|
|
342
|
+
const match = text.match(
|
|
343
|
+
/^\s*(?:[#>*-]\s*)*model\s*[:=]\s*([^\n]*)$/im,
|
|
344
|
+
);
|
|
345
|
+
if (match === null) return undefined;
|
|
346
|
+
const rest = (match[1] ?? "").trim().replace(/^[*`\s]+|[*`\s]+$/g, "").trim();
|
|
347
|
+
if (rest === "" || /^none$/i.test(rest) || /\s/.test(rest)) return undefined;
|
|
348
|
+
return { model: rest, source: match[0].trim() };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Resolves the effective model across the body and the whole comment thread. */
|
|
352
|
+
export function effectiveModel(body: string, comments: IssueComment[]): EffectiveModel | undefined {
|
|
353
|
+
let current: EffectiveModel | undefined;
|
|
354
|
+
const bodyDecl = modelDeclaration(body);
|
|
355
|
+
if (bodyDecl !== undefined) current = { ...bodyDecl, at: "body" };
|
|
356
|
+
for (let i = 0; i < comments.length; i++) {
|
|
357
|
+
const decl = modelDeclaration(comments[i]!.body);
|
|
358
|
+
if (decl !== undefined) current = { ...decl, at: i };
|
|
359
|
+
}
|
|
360
|
+
return current;
|
|
361
|
+
}
|
|
362
|
+
|
|
273
363
|
/**
|
|
274
364
|
* The file lane as one line, for surfaces where a human asserts the claim
|
|
275
365
|
* (#724): the parsed file list exactly as admission will enforce it, or the
|
|
@@ -348,43 +438,61 @@ export async function admitCandidates(
|
|
|
348
438
|
};
|
|
349
439
|
|
|
350
440
|
// The file-lane interlock (#555): repo -> file -> the issue holding that file.
|
|
351
|
-
// Seeded from
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
441
|
+
// Seeded from what a live run actually *occupies*, never from its issue body:
|
|
442
|
+
// the probed worktrees/branches of active runs (#288) unioned — since #744 —
|
|
443
|
+
// with the lane declaration admission persisted on the row at dispatch, which
|
|
444
|
+
// is the exact enforced snapshot, not a re-parse. The declared half is what
|
|
445
|
+
// survives across passes: a run admitted with a lane it has not yet written
|
|
446
|
+
// still occupies until it reaches a terminal state and leaves the active set,
|
|
447
|
+
// and the declared lanes are extended as this pass admits candidates, so two
|
|
448
|
+
// overlapping candidates cannot both clear the gate in one tick. Keyed by
|
|
449
|
+
// repo because a path only collides within its own checkout: a `daemon.ts` on
|
|
450
|
+
// the api repo and one on the web repo are different files. Built lazily and
|
|
451
|
+
// once, only when the first candidate that declares a lane reaches the gate,
|
|
452
|
+
// so a queue of laneless issues pays nothing for it.
|
|
359
453
|
let laneOccupancy: Map<string, Map<string, LaneHolder>> | undefined;
|
|
360
454
|
const ensureLaneOccupancy = async (): Promise<Map<string, Map<string, LaneHolder>>> => {
|
|
361
455
|
if (laneOccupancy !== undefined) return laneOccupancy;
|
|
362
456
|
const occupied = new Map<string, Map<string, LaneHolder>>();
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
mirror: join(project.mirrorRoot, `${run.repo}.git`),
|
|
373
|
-
});
|
|
374
|
-
} catch {
|
|
375
|
-
// Fail open, like an unreadable lane: the gate never refuses a
|
|
376
|
-
// well-formed issue because one probe could not be answered.
|
|
377
|
-
files = [];
|
|
457
|
+
// Shared per-file first-read-wins: a file a run both declares and has
|
|
458
|
+
// actually touched is tagged by the concrete probe read, and across runs
|
|
459
|
+
// whichever live run comes first in the active set is the holder named —
|
|
460
|
+
// both are legitimate for the gate, which holds on any overlap.
|
|
461
|
+
const occupy = (run: RunRecord, file: string, source: LaneSource | "declared"): void => {
|
|
462
|
+
let perRepo = occupied.get(run.repo);
|
|
463
|
+
if (perRepo === undefined) {
|
|
464
|
+
perRepo = new Map();
|
|
465
|
+
occupied.set(run.repo, perRepo);
|
|
378
466
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
467
|
+
if (!perRepo.has(file)) perRepo.set(file, { issue: run.issue, source });
|
|
468
|
+
};
|
|
469
|
+
for (const run of activeRuns) {
|
|
470
|
+
// The probed actual half first, so the concrete read outnames the
|
|
471
|
+
// declaration for a file the run has already written.
|
|
472
|
+
if (d.probeWorktreeLane !== undefined) {
|
|
473
|
+
const base = project.routing.repos[run.repo]?.defaultBranch ?? "main";
|
|
474
|
+
let files: LaneFile[];
|
|
475
|
+
try {
|
|
476
|
+
files = await d.probeWorktreeLane({
|
|
477
|
+
worktree: run.worktree,
|
|
478
|
+
baseRef: `refs/remotes/origin/${base}`,
|
|
479
|
+
...(run.branch === "" ? {} : { branchRef: `refs/heads/${run.branch}` }),
|
|
480
|
+
mirror: join(project.mirrorRoot, `${run.repo}.git`),
|
|
481
|
+
});
|
|
482
|
+
} catch {
|
|
483
|
+
// Fail open, like an unreadable lane: the gate never refuses a
|
|
484
|
+
// well-formed issue because one probe could not be answered.
|
|
485
|
+
files = [];
|
|
384
486
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
487
|
+
for (const { file, source } of files) occupy(run, file, source);
|
|
488
|
+
}
|
|
489
|
+
// The declared durable half (#744): the lane admission persisted at
|
|
490
|
+
// dispatch. Occupies even before the run has written anything, on every
|
|
491
|
+
// pass — the declaration lives on the row, not in a per-pass memo.
|
|
492
|
+
// Released by the active-set filter exactly like probed occupancy: a
|
|
493
|
+
// terminal run leaves `activeRuns` and its files stop occupying here.
|
|
494
|
+
if (run.lane !== undefined && run.lane.files.length > 0) {
|
|
495
|
+
for (const file of run.lane.files) occupy(run, file, "declared");
|
|
388
496
|
}
|
|
389
497
|
}
|
|
390
498
|
laneOccupancy = occupied;
|
|
@@ -408,31 +516,45 @@ export async function admitCandidates(
|
|
|
408
516
|
// the worker therefore agree on one lane, which is the #608 defect's shape.
|
|
409
517
|
//
|
|
410
518
|
// Comments are read at most once per issue per pass — the lane feeds both
|
|
411
|
-
// the gate and the same-pass sibling occupancy below,
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
519
|
+
// the gate and the same-pass sibling occupancy below, and the model rides
|
|
520
|
+
// the same read for its one-value contract with dispatch (#535), so the
|
|
521
|
+
// cache is what stops one candidate costing two comment reads. Unreadable
|
|
522
|
+
// comments fail open to the body declarations (a body lane stays
|
|
523
|
+
// load-bearing, and a body model stays the one dispatch follows), and with
|
|
524
|
+
// no readable declaration at all the #555 fail-open admission is unchanged.
|
|
415
525
|
//
|
|
416
526
|
// The resolved `FileLane` — not just its paths — is what an admitted
|
|
417
527
|
// candidate carries into dispatch (#608): the gate and the brief must agree
|
|
418
528
|
// on the *same declaration* (paths, verbatim source line, and which surface
|
|
419
529
|
// it came from), so dispatch renders this cached value rather than reading
|
|
420
|
-
// the thread a second time and hoping it did not change.
|
|
530
|
+
// the thread a second time and hoping it did not change. The resolved
|
|
531
|
+
// `EffectiveModel` is carried the same way and for the same reason: dispatch
|
|
532
|
+
// launches on exactly this selector and the brief echoes it.
|
|
421
533
|
const laneCache = new Map<number, FileLane | undefined>();
|
|
422
|
-
const
|
|
534
|
+
const modelCache = new Map<number, EffectiveModel | undefined>();
|
|
535
|
+
const declarationsFor = async (r: Routed): Promise<void> => {
|
|
423
536
|
const issue = r.issue.number;
|
|
424
|
-
if (laneCache.has(issue)) return
|
|
537
|
+
if (laneCache.has(issue)) return;
|
|
425
538
|
let lane: FileLane | undefined;
|
|
539
|
+
let model: EffectiveModel | undefined;
|
|
426
540
|
try {
|
|
427
|
-
|
|
541
|
+
const comments = await tracker.listComments(issue);
|
|
542
|
+
lane = effectiveLane(r.issue.body, comments);
|
|
543
|
+
model = effectiveModel(r.issue.body, comments);
|
|
428
544
|
} catch (err) {
|
|
429
545
|
const bodyDecl = laneDeclaration(r.issue.body);
|
|
430
546
|
lane = bodyDecl === undefined ? undefined : { ...bodyDecl, at: "body" };
|
|
431
|
-
|
|
547
|
+
const bodyModel = modelDeclaration(r.issue.body);
|
|
548
|
+
model = bodyModel === undefined ? undefined : { ...bodyModel, at: "body" };
|
|
549
|
+
log(`#${issue} comments unreadable at admission; the body's declarations stand: ${errText(err)}`);
|
|
432
550
|
}
|
|
433
551
|
laneCache.set(issue, lane);
|
|
434
|
-
|
|
552
|
+
modelCache.set(issue, model);
|
|
435
553
|
};
|
|
554
|
+
const laneFor = (r: Routed): Promise<FileLane | undefined> =>
|
|
555
|
+
declarationsFor(r).then(() => laneCache.get(r.issue.number));
|
|
556
|
+
const modelFor = (r: Routed): Promise<EffectiveModel | undefined> =>
|
|
557
|
+
declarationsFor(r).then(() => modelCache.get(r.issue.number));
|
|
436
558
|
|
|
437
559
|
// The plan allowance is a fleet-wide question, so it is asked once per pass
|
|
438
560
|
// and answers for every candidate — unlike every gate below it, which is
|
|
@@ -450,6 +572,9 @@ export async function admitCandidates(
|
|
|
450
572
|
log(`plan usage gate holding ${String(routed.length)} candidate(s): ${plan.detail}`);
|
|
451
573
|
const escalation = planUsageEscalation(project.name, plan);
|
|
452
574
|
if (escalation !== undefined) await safeEscalate(d, escalation);
|
|
575
|
+
// Every pass exit reconciles the durable blocked inventory against this
|
|
576
|
+
// pass's holds, so a hold that stopped applying clears itself (#735).
|
|
577
|
+
store.reconcileGrooming(project.name, holds);
|
|
453
578
|
return { admitted: [], holds };
|
|
454
579
|
}
|
|
455
580
|
|
|
@@ -1035,7 +1160,7 @@ export async function admitCandidates(
|
|
|
1035
1160
|
if (blocked) continue;
|
|
1036
1161
|
}
|
|
1037
1162
|
|
|
1038
|
-
admitted.push({ r, attempt: priorRuns + 1, lane });
|
|
1163
|
+
admitted.push({ r, attempt: priorRuns + 1, lane, model: await modelFor(r) });
|
|
1039
1164
|
liveByRepo.set(r.repo.name, (liveByRepo.get(r.repo.name) ?? 0) + 1);
|
|
1040
1165
|
// Same-pass sibling occupancy for the file-lane gate: once admitted, a
|
|
1041
1166
|
// candidate's declared lane occupies for the rest of the pass, so a later
|
|
@@ -1064,5 +1189,9 @@ export async function admitCandidates(
|
|
|
1064
1189
|
}
|
|
1065
1190
|
}
|
|
1066
1191
|
|
|
1192
|
+
// Every pass exit reconciles the durable blocked inventory against this
|
|
1193
|
+
// pass's holds, so a lane/dependency hold that stopped applying clears
|
|
1194
|
+
// itself on the very next pass (#735).
|
|
1195
|
+
store.reconcileGrooming(project.name, holds);
|
|
1067
1196
|
return { admitted, holds };
|
|
1068
1197
|
}
|