@ai-dossier/sched 0.49.0 → 0.50.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/README.md +42 -5
- package/dist/batch-dispatch.d.ts +58 -0
- package/dist/batch-dispatch.d.ts.map +1 -1
- package/dist/batch-dispatch.js +174 -41
- package/dist/batch-dispatch.js.map +1 -1
- package/dist/groundtruth.d.ts +63 -3
- package/dist/groundtruth.d.ts.map +1 -1
- package/dist/groundtruth.js +115 -14
- package/dist/groundtruth.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/state.d.ts +12 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +47 -21
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +20 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1040,7 +1040,9 @@ import {
|
|
|
1040
1040
|
// runBatchCapability — #523)
|
|
1041
1041
|
createSpawnDeps, // real detached-spawn process I/O
|
|
1042
1042
|
createExecGroundTruth, // runstate/gh/git ground truth via subprocesses (injectable exec);
|
|
1043
|
-
// since #468 also gh pr view PR state + setup info from comments
|
|
1043
|
+
// since #468 also gh pr view PR state + setup info from comments;
|
|
1044
|
+
// since #789 also mergedPrForBranch when constructed with a
|
|
1045
|
+
// verified `repo` (resolveProjectRepo)
|
|
1044
1046
|
resolveDispatch, // config → resolved command/prompt/report-prompt/tier-models/timers/
|
|
1045
1047
|
// per-tier spawn specs (tiers — #527)
|
|
1046
1048
|
buildTierCommand, // resolved dispatch + tier + issue → argv, using that tier's OWN
|
|
@@ -1068,6 +1070,12 @@ import {
|
|
|
1068
1070
|
prOfMilestone, // a milestone's pr= key as a positive integer
|
|
1069
1071
|
parsePrViewJson, // gh pr view --json → PR truth (mergedAt/mergeable/blocked label)
|
|
1070
1072
|
parseOpenPrListJson, // gh pr list --head <b> --state open → the open PR we opened (#596)
|
|
1073
|
+
parseMergedPrListJson, // gh pr list --head <b> --base <base> --state merged → MergedPrLookup
|
|
1074
|
+
// found/none/ambiguous — a hand-opened batch PR the ledger never
|
|
1075
|
+
// recorded (#789)
|
|
1076
|
+
type MergedPrLookup, // { kind: 'found', pr, mergedAt } | { kind: 'none' } |
|
|
1077
|
+
// { kind: 'ambiguous', matches } — GroundTruth.mergedPrForBranch's
|
|
1078
|
+
// own return type (#789)
|
|
1071
1079
|
parseSetupInfo, // gh issue view --json comments → teardown inputs
|
|
1072
1080
|
runTeardown, // #468 script teardown for a merged unit (pool return / worktree remove)
|
|
1073
1081
|
isSafeWorktree, // worktree-path containment check (CWE-22)
|
|
@@ -1138,7 +1146,7 @@ import {
|
|
|
1138
1146
|
issueCloseReader, parseIssueCloseTruthJson, parseRepoName,
|
|
1139
1147
|
resolveProjectRepo, // #768 owner/name of the cwd repo only when it IS the project's
|
|
1140
1148
|
hasLabel, // case-insensitive label match (#768)
|
|
1141
|
-
validateState, // strict persisted-state validation (1.0.0-1.
|
|
1149
|
+
validateState, // strict persisted-state validation (1.0.0-1.23.0 files migrate)
|
|
1142
1150
|
DEFAULT_ISSUE_PRIORITY, DEFAULT_BATCH_PRIORITY, // priority defaults (0 / 10, #565)
|
|
1143
1151
|
IllegalTransitionError, EnqueueError, CorruptStateError, LockTimeoutError,
|
|
1144
1152
|
SchedNotFoundError,
|
|
@@ -1306,8 +1314,9 @@ after-the-fact recovery, not a missing-data bug.
|
|
|
1306
1314
|
queue data.
|
|
1307
1315
|
- **Schema**: state/config files from #460 (schema 1.0.0), #464 (1.1.0), #468 (1.2.0),
|
|
1308
1316
|
#472 (1.3.0), #500 (1.4.0), #505 (1.5.0), #504 (1.6.0), #523 (1.7.0) and #524 (1.8.0)
|
|
1309
|
-
load and migrate to the current schema (1.
|
|
1310
|
-
optional, absent = an `evicted` record with no branch) automatically
|
|
1317
|
+
load and migrate to the current schema (1.25.0 — 1.24.0 was #810: no backfill,
|
|
1318
|
+
`kind`/`branch` optional, absent = an `evicted` record with no branch) automatically
|
|
1319
|
+
(slot `branch`/`last_head`/`pid_start`, slot `role` (inferred from the
|
|
1311
1320
|
unit's queue entry, with the persisted `phase` as a fallback — #500), entry
|
|
1312
1321
|
`pr`/`cleanup`/`failure_evidence`, batch `anchor`/`branch`/`run_id`/`eviction_groups`/
|
|
1313
1322
|
`evictions`/`fix_attempts`/`rebase_attempts`, state-level `last_pr_poll_at` backfill to
|
|
@@ -1320,7 +1329,10 @@ after-the-fact recovery, not a missing-data bug.
|
|
|
1320
1329
|
`consecutive_dispatch_api_errors`/`dispatch_pause_reset_at` backfill to `0`/`null` —
|
|
1321
1330
|
#629; batch `anchor_closed_at` and `anchor_close_failed_reason`/`_since`/`_ticks`
|
|
1322
1331
|
backfill to `null`/`null`/`null`/`0` — #768, schema 1.22.0; batch `member_dispatch`/`member_runs`
|
|
1323
|
-
backfill to `null`/`[]` — #809, schema 1.23.0 — a null mode past `ready` runs serially
|
|
1332
|
+
backfill to `null`/`[]` — #809, schema 1.23.0 — a null mode past `ready` runs serially;
|
|
1333
|
+
`IssueStatus` gains `handed-back`, `EvictionRecord`/`FailureEvidence` gain optional
|
|
1334
|
+
`kind`/`branch` — #810, schema 1.24.0; batch `pr_detect_ambiguous_reason`/`_since`/`_ticks`
|
|
1335
|
+
backfill to `null`/`null`/`0` — #789, schema 1.25.0).
|
|
1324
1336
|
- **`max_slots`** bounds live units (`assigned | running | recovering`); dependency
|
|
1325
1337
|
edges gate readiness — an issue with an unmerged dependency, and a batch behind an
|
|
1326
1338
|
unmerged batch, are never runnable.
|
|
@@ -1413,6 +1425,31 @@ predicate in `anchor-close.ts`) over `blocked`/`done` batches touched within the
|
|
|
1413
1425
|
the journal line says the worktree was kept, and `sched status` then raises a
|
|
1414
1426
|
`kept-worktree` warning for it on every run until the ledger field clears — see
|
|
1415
1427
|
above and `reconcileKeptWorktrees` (#791).
|
|
1428
|
+
- **PR-detection evidence (#789).** When `batch.pr` was never recorded at all — a PR
|
|
1429
|
+
opened by hand, e.g. imboard#4255 — the stale-blocked reconcile also looks for
|
|
1430
|
+
exactly one MERGED pull request whose head is the batch branch, based against
|
|
1431
|
+
`batch.base_branch`, created at or after the batch's own `created_at`, and not from a
|
|
1432
|
+
fork (`GroundTruth.mergedPrForBranch`: `gh pr list -R <repo> --head <branch> --base
|
|
1433
|
+
<base> --state merged`, gated on the same verified `resolveProjectRepo` #768 uses —
|
|
1434
|
+
absent entirely without one). Positive evidence only: zero candidates records
|
|
1435
|
+
nothing and falls through to the branch/members evidence below; two or more is
|
|
1436
|
+
`ambiguous` — no PR is recorded, and only when no OTHER evidence reconciles the
|
|
1437
|
+
batch that same tick is the ambiguity journaled (`pr-detect-ambiguous`, deduped like
|
|
1438
|
+
`pr-watch-failed`: once per `blocked` stretch, re-announced every
|
|
1439
|
+
`JOURNAL_DEDUP_REANNOUNCE_TICKS`; a batch that leaves `blocked` any way — this
|
|
1440
|
+
reconcile's own success, `sched resume --batch`, `sched abandon` — clears the marker,
|
|
1441
|
+
so a later re-block never inherits a stale streak). A batch that still reconciles via
|
|
1442
|
+
`commits-in-base`/`members-closed` does so without a PR and finishes inline, same as
|
|
1443
|
+
before #789. On a match, `batch.pr` is recorded as part of the SAME `blocked →
|
|
1444
|
+
merged` transition (the `stale-failure-reconciled` line then also carries
|
|
1445
|
+
`pr_detected: true`), so the ordinary `deployed` machinery (item 6, "Report dispatch",
|
|
1446
|
+
under "The PR watcher + tail work (#468)" below — shared infrastructure, not
|
|
1447
|
+
per-issue-only despite the section's origin) dispatches the report agent exactly as if the
|
|
1448
|
+
fleet had opened the PR itself, and `sched status`'s existing `pr` column shows it.
|
|
1449
|
+
Recording `batch.pr` never closes the anchor by itself — that stays #768's own
|
|
1450
|
+
evidence-gated `reconcileAnchorClosure`, unchanged. An ambiguous match with no
|
|
1451
|
+
explicit way to resolve it today (#824 tracks a `sched attach-pr` verb) still lets the
|
|
1452
|
+
batch reconcile via its other evidence, or falls to `sched abandon --batch`.
|
|
1416
1453
|
|
|
1417
1454
|
Everything else is surfaced, never closed: `sched status --anchors` (opt-in; `status`
|
|
1418
1455
|
makes no GitHub call without it) lists each still-open anchor of a batch no longer in
|
package/dist/batch-dispatch.d.ts
CHANGED
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
* before returning, so a member's first command is never the one that
|
|
62
62
|
* discovers `node_modules` is missing (`env-cold`, `docs/agent-traps.md`).
|
|
63
63
|
*/
|
|
64
|
+
import { type CommitInBase, type IssueCloseReader } from './anchor-close';
|
|
64
65
|
import { type ResolvedDispatch, type SpawnDeps } from './dispatch';
|
|
65
66
|
import { type GroundTruth } from './groundtruth';
|
|
66
67
|
import { type Journal } from './journal';
|
|
@@ -237,6 +238,63 @@ export declare function resumeBlockedGate(deps: BatchDispatchDeps, config: Sched
|
|
|
237
238
|
blockedBy?: 'inconclusive' | 'unevidenced-failure';
|
|
238
239
|
detail?: string;
|
|
239
240
|
};
|
|
241
|
+
/**
|
|
242
|
+
* #686: reconcile a `blocked` batch whose work demonstrably shipped anyway.
|
|
243
|
+
* `reconcilePrWatch` above only watches `awaiting-merge` batches, and
|
|
244
|
+
* `resumeBlockedGate` only re-runs the gate — so a batch blocked on a stale
|
|
245
|
+
* verdict (the concrete case: `gate-inconclusive:test.focused`, itself often
|
|
246
|
+
* a #690-declined gate TIMEOUT) whose PR merged out of band — or whose every
|
|
247
|
+
* member commit is already in the base — stayed blocked FOREVER: no report,
|
|
248
|
+
* no teardown, an operator-visible `blocked` row, and members stuck
|
|
249
|
+
* `in-work`. Sibling of the per-issue `reconcileStaleFailedParks`
|
|
250
|
+
* (engine.ts) under the same principle: ground truth beats the ledger, for
|
|
251
|
+
* ANY stale verdict — the blocked_reason is deliberately not examined.
|
|
252
|
+
*
|
|
253
|
+
* Evidence (cheapest first): the batch PR is MERGED with a real timestamp (a
|
|
254
|
+
* merged batch PR contains the whole batch branch) — including one
|
|
255
|
+
* `batch.pr` never recorded at all (#789): when `pr === null`, before falling
|
|
256
|
+
* through to the weaker branch/members evidence below, look for exactly one
|
|
257
|
+
* MERGED PR whose head is the batch branch (`imboard#4255`, a hand-opened PR
|
|
258
|
+
* the ledger never saw). Positive evidence only — zero candidates changes
|
|
259
|
+
* nothing, more than one is `ambiguous` and journaled rather than guessed —
|
|
260
|
+
* or the batch branch has fully landed in the base branch. A survivability
|
|
261
|
+
* guard applies to every path (see `everySurvivingMemberDispatched`) — a
|
|
262
|
+
* batch with a surviving member that was never dispatched is not
|
|
263
|
+
* reconcilable.
|
|
264
|
+
*
|
|
265
|
+
* On reconcile the batch joins the SAME rail a normally-merged batch takes:
|
|
266
|
+
* `blocked → merged → deployed` (the `blocked → merged` edge exists for
|
|
267
|
+
* exactly this caller), surviving members walked to `done` along legal
|
|
268
|
+
* edges, and — when the batch now carries a PR (recorded before or detected
|
|
269
|
+
* here) — the ordinary `deployed` machinery dispatches the report agent next
|
|
270
|
+
* tick, with teardown after `batch-report done`. With no PR at all no report
|
|
271
|
+
* agent can be prompted (it names the PR), so the same lock continues
|
|
272
|
+
* `deployed → reported → done` and the worktree is torn down here instead;
|
|
273
|
+
* the journal line says why no report agent was dispatched rather than
|
|
274
|
+
* leaving the gap silent.
|
|
275
|
+
*
|
|
276
|
+
* Bounded by construction: only `blocked` batches within the window are
|
|
277
|
+
* examined, the PR check is one `gh` call, git runs only when PR evidence
|
|
278
|
+
* didn't already reconcile, and a reconciled batch leaves `blocked` — the
|
|
279
|
+
* one journal line fires on the one-way transition, never per-tick (the
|
|
280
|
+
* #632 lesson: no journal inside a loop that doesn't terminate the unit).
|
|
281
|
+
*/
|
|
282
|
+
export declare function reconcileStaleBlockedBatches(deps: BatchDispatchDeps, now: Date, result: BatchTickResult, anchorCtx: AnchorTickContext | undefined): void;
|
|
283
|
+
/**
|
|
284
|
+
* #768: what one tick's anchor work reads GitHub and git through — built once
|
|
285
|
+
* per tick so the stale-blocked reconcile and the anchor close never read the
|
|
286
|
+
* same issue twice in a tick. `undefined` when the tick cannot verify anything
|
|
287
|
+
* (no issue-closure ground truth, or no verified `anchorRepo`): then neither
|
|
288
|
+
* pass acts at all.
|
|
289
|
+
*/
|
|
290
|
+
interface AnchorTickContext {
|
|
291
|
+
repo: string;
|
|
292
|
+
/** Cached for the tick — the verdict passes. */
|
|
293
|
+
read: IssueCloseReader;
|
|
294
|
+
/** Uncached — the re-read immediately before an irreversible close. */
|
|
295
|
+
readFresh: IssueCloseReader;
|
|
296
|
+
commitInBase: CommitInBase;
|
|
297
|
+
}
|
|
240
298
|
/**
|
|
241
299
|
* How a batch should dispatch its members (#809) — decided once, at the
|
|
242
300
|
* `ready → executing` claim, and persisted on `BatchEntry.member_dispatch`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;
|
|
1
|
+
{"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"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"}
|
package/dist/batch-dispatch.js
CHANGED
|
@@ -99,6 +99,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
99
99
|
exports.memberBranchFor = memberBranchFor;
|
|
100
100
|
exports.evictMemberAndContinue = evictMemberAndContinue;
|
|
101
101
|
exports.resumeBlockedGate = resumeBlockedGate;
|
|
102
|
+
exports.reconcileStaleBlockedBatches = reconcileStaleBlockedBatches;
|
|
102
103
|
exports.memberDispatchModeFor = memberDispatchModeFor;
|
|
103
104
|
exports.runBatchTick = runBatchTick;
|
|
104
105
|
const fs = __importStar(require("node:fs"));
|
|
@@ -2655,18 +2656,44 @@ function reconcileReportSlot(deps, batchId, slot, now, result) {
|
|
|
2655
2656
|
}
|
|
2656
2657
|
// --- PR watch for `awaiting-merge` batches (mirrors `pollParkedPrs`/`reconcileParked`) ---
|
|
2657
2658
|
/**
|
|
2658
|
-
*
|
|
2659
|
-
* streak is recorded
|
|
2660
|
-
*
|
|
2659
|
+
* Clear one `<prefix>_reason`/`_since`/`_ticks` batch dedup marker — a no-op
|
|
2660
|
+
* when no streak is recorded (`currentReason === null`), so every caller is
|
|
2661
|
+
* safe to invoke unconditionally, every tick, on a batch that may or may not
|
|
2662
|
+
* have an open streak. Shared by `pr-watch-failed` (#630/#633),
|
|
2663
|
+
* `anchor-close-failed` (#768) and `pr-detect-ambiguous` (#789) — each
|
|
2664
|
+
* family previously carried its own copy of this exact body (#789 review).
|
|
2661
2665
|
*/
|
|
2662
|
-
function
|
|
2663
|
-
if (
|
|
2666
|
+
function clearBatchDedupMarker(deps, batch, currentReason, clearedFields, now) {
|
|
2667
|
+
if (currentReason === null)
|
|
2664
2668
|
return;
|
|
2665
2669
|
deps.store.withLock((s) => ({
|
|
2666
|
-
state: (0, state_1.patchBatch)(s, batch.id,
|
|
2670
|
+
state: (0, state_1.patchBatch)(s, batch.id, clearedFields, now, false),
|
|
2667
2671
|
result: undefined,
|
|
2668
2672
|
}));
|
|
2669
2673
|
}
|
|
2674
|
+
/**
|
|
2675
|
+
* Advance one `<prefix>_reason`/`_since`/`_ticks` batch dedup marker by one
|
|
2676
|
+
* tick and decide whether THIS tick should journal — the streak's first tick,
|
|
2677
|
+
* or every `JOURNAL_DEDUP_REANNOUNCE_TICKS` after, never every tick
|
|
2678
|
+
* (#610/#630/#632/#768/#789's shared shape). Pure — the caller still owns
|
|
2679
|
+
* building its own journal payload (each family logs different extra fields)
|
|
2680
|
+
* and the `deps.store.withLock` persistence write; this only computes
|
|
2681
|
+
* `since`/`ticks`/whether to announce.
|
|
2682
|
+
*/
|
|
2683
|
+
function advanceBatchDedupStreak(priorReason, priorSince, priorTicks, reason, now) {
|
|
2684
|
+
const isNewStreak = priorReason !== reason || priorSince === null;
|
|
2685
|
+
const ticks = isNewStreak ? 1 : priorTicks + 1;
|
|
2686
|
+
const since = isNewStreak || priorSince === null ? now.toISOString() : priorSince;
|
|
2687
|
+
return { since, ticks, announce: isNewStreak || ticks % types_1.JOURNAL_DEDUP_REANNOUNCE_TICKS === 0 };
|
|
2688
|
+
}
|
|
2689
|
+
/**
|
|
2690
|
+
* Reset a batch's `pr-watch-failed` dedup marker (#630/#633). A no-op when no
|
|
2691
|
+
* streak is recorded, so it is safe to call on every non-`awaiting-merge`
|
|
2692
|
+
* batch every tick.
|
|
2693
|
+
*/
|
|
2694
|
+
function clearPrWatchFailed(deps, batch, now) {
|
|
2695
|
+
clearBatchDedupMarker(deps, batch, batch.pr_watch_failed_reason, state_1.CLEARED_PR_WATCH_FIELDS, now);
|
|
2696
|
+
}
|
|
2670
2697
|
function reconcilePrWatch(deps, now, result) {
|
|
2671
2698
|
const state = deps.store.load();
|
|
2672
2699
|
for (const batch of state.batches) {
|
|
@@ -2715,9 +2742,6 @@ function reconcilePrWatch(deps, now, result) {
|
|
|
2715
2742
|
}
|
|
2716
2743
|
if (truth.blocked || truth.mergeable === 'CONFLICTING') {
|
|
2717
2744
|
const reason = truth.blocked ? 'auto-merge-blocked' : 'pr-conflicting';
|
|
2718
|
-
const isNewStreak = batch.pr_watch_failed_reason !== reason;
|
|
2719
|
-
const ticks = isNewStreak ? 1 : batch.pr_watch_failed_ticks + 1;
|
|
2720
|
-
const since = isNewStreak ? now.toISOString() : batch.pr_watch_failed_since;
|
|
2721
2745
|
// #630: journal on the streak's first tick, then again only every
|
|
2722
2746
|
// `JOURNAL_DEDUP_REANNOUNCE_TICKS` — never every tick — mirrors #610's
|
|
2723
2747
|
// `stale_milestone_ignored_for` dedup for the "distinct condition"
|
|
@@ -2729,7 +2753,8 @@ function reconcilePrWatch(deps, now, result) {
|
|
|
2729
2753
|
// of its own); `since` is the streak's onset, and is what makes the
|
|
2730
2754
|
// duration legible — `ticks_persisted` is a tick count against an
|
|
2731
2755
|
// operator-tunable `reconcile_interval_ms`, not a fixed wall-clock.
|
|
2732
|
-
|
|
2756
|
+
const { since, ticks, announce } = advanceBatchDedupStreak(batch.pr_watch_failed_reason, batch.pr_watch_failed_since, batch.pr_watch_failed_ticks, reason, now);
|
|
2757
|
+
if (announce) {
|
|
2733
2758
|
journalEvent(deps, 'pr-watch-failed', unit(batch.id), {
|
|
2734
2759
|
reason,
|
|
2735
2760
|
pr,
|
|
@@ -2872,6 +2897,51 @@ function branchMergedIntoBase(deps, batch) {
|
|
|
2872
2897
|
const baseTip = deps.exec('git', ['rev-parse', base], deps.repoDir);
|
|
2873
2898
|
return branchTip !== null && baseTip !== null && branchTip.trim() !== '' && branchTip !== baseTip;
|
|
2874
2899
|
}
|
|
2900
|
+
/**
|
|
2901
|
+
* The ambiguity condition stopped qualifying (the branch resolved to zero or
|
|
2902
|
+
* exactly one candidate), or the batch left `blocked` some other way (this
|
|
2903
|
+
* reconcile's own success, `sched resume --batch`, `sched abandon`, or the
|
|
2904
|
+
* batch aging out of the reconcile window): end any `pr-detect-ambiguous`
|
|
2905
|
+
* streak, so a future ambiguity episode — even a re-block of the SAME batch
|
|
2906
|
+
* with the SAME candidates — journals its own first line instead of silently
|
|
2907
|
+
* continuing a streak from a previous `blocked` stretch (#789 review). A
|
|
2908
|
+
* no-op (no write) when no streak is recorded.
|
|
2909
|
+
*/
|
|
2910
|
+
function clearPrDetectAmbiguous(deps, batch, now) {
|
|
2911
|
+
clearBatchDedupMarker(deps, batch, batch.pr_detect_ambiguous_reason, state_1.CLEARED_PR_DETECT_AMBIGUOUS_FIELDS, now);
|
|
2912
|
+
}
|
|
2913
|
+
/**
|
|
2914
|
+
* #789: more than one MERGED PR matches `batch.branch` — positive evidence
|
|
2915
|
+
* only, so the caller records nothing. Journal it on the streak's first tick
|
|
2916
|
+
* and every `JOURNAL_DEDUP_REANNOUNCE_TICKS` after — never every tick — the
|
|
2917
|
+
* same dedup shape `recordAnchorCloseFailed`/`reconcilePrWatch` use.
|
|
2918
|
+
*/
|
|
2919
|
+
function recordPrDetectAmbiguous(deps, batch, matches, now) {
|
|
2920
|
+
const { since, ticks, announce } = advanceBatchDedupStreak(batch.pr_detect_ambiguous_reason, batch.pr_detect_ambiguous_since, batch.pr_detect_ambiguous_ticks, state_1.PR_DETECT_AMBIGUOUS_REASON, now);
|
|
2921
|
+
if (announce) {
|
|
2922
|
+
journalEvent(deps, 'pr-detect-ambiguous', unit(batch.id), {
|
|
2923
|
+
reason: state_1.PR_DETECT_AMBIGUOUS_REASON,
|
|
2924
|
+
branch: batch.branch ?? undefined,
|
|
2925
|
+
matches: matches.join(','),
|
|
2926
|
+
at: now.toISOString(),
|
|
2927
|
+
since,
|
|
2928
|
+
ticks_persisted: ticks,
|
|
2929
|
+
// #789 review: neither "record it explicitly" (no command exists) nor
|
|
2930
|
+
// "close the extras" (a MERGED PR cannot be closed, so it never leaves
|
|
2931
|
+
// `gh pr list --state merged`) was ever an actionable remedy — state
|
|
2932
|
+
// what actually resolves it instead. #824 tracks the missing verb.
|
|
2933
|
+
detail: `${matches.length} MERGED PRs match head=${batch.branch ?? '?'} (#${matches.join(', #')}) — refusing to guess which is ours; nothing recorded. The batch still reconciles via commits-in-base/members-closed evidence if either holds; otherwise inspect and \`sched abandon --batch ${batch.id}\`, or use \`sched attach-pr\` once #824 ships it`,
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
deps.store.withLock((s) => ({
|
|
2937
|
+
state: (0, state_1.patchBatch)(s, batch.id, {
|
|
2938
|
+
pr_detect_ambiguous_reason: state_1.PR_DETECT_AMBIGUOUS_REASON,
|
|
2939
|
+
pr_detect_ambiguous_since: since,
|
|
2940
|
+
pr_detect_ambiguous_ticks: ticks,
|
|
2941
|
+
}, now, false),
|
|
2942
|
+
result: undefined,
|
|
2943
|
+
}));
|
|
2944
|
+
}
|
|
2875
2945
|
/**
|
|
2876
2946
|
* #686: reconcile a `blocked` batch whose work demonstrably shipped anyway.
|
|
2877
2947
|
* `reconcilePrWatch` above only watches `awaiting-merge` batches, and
|
|
@@ -2884,21 +2954,28 @@ function branchMergedIntoBase(deps, batch) {
|
|
|
2884
2954
|
* (engine.ts) under the same principle: ground truth beats the ledger, for
|
|
2885
2955
|
* ANY stale verdict — the blocked_reason is deliberately not examined.
|
|
2886
2956
|
*
|
|
2887
|
-
* Evidence (
|
|
2888
|
-
*
|
|
2889
|
-
* batch
|
|
2890
|
-
*
|
|
2891
|
-
*
|
|
2957
|
+
* Evidence (cheapest first): the batch PR is MERGED with a real timestamp (a
|
|
2958
|
+
* merged batch PR contains the whole batch branch) — including one
|
|
2959
|
+
* `batch.pr` never recorded at all (#789): when `pr === null`, before falling
|
|
2960
|
+
* through to the weaker branch/members evidence below, look for exactly one
|
|
2961
|
+
* MERGED PR whose head is the batch branch (`imboard#4255`, a hand-opened PR
|
|
2962
|
+
* the ledger never saw). Positive evidence only — zero candidates changes
|
|
2963
|
+
* nothing, more than one is `ambiguous` and journaled rather than guessed —
|
|
2964
|
+
* or the batch branch has fully landed in the base branch. A survivability
|
|
2965
|
+
* guard applies to every path (see `everySurvivingMemberDispatched`) — a
|
|
2966
|
+
* batch with a surviving member that was never dispatched is not
|
|
2967
|
+
* reconcilable.
|
|
2892
2968
|
*
|
|
2893
2969
|
* On reconcile the batch joins the SAME rail a normally-merged batch takes:
|
|
2894
2970
|
* `blocked → merged → deployed` (the `blocked → merged` edge exists for
|
|
2895
2971
|
* exactly this caller), surviving members walked to `done` along legal
|
|
2896
|
-
* edges, and — when
|
|
2897
|
-
*
|
|
2898
|
-
*
|
|
2899
|
-
* PR), so the same lock continues
|
|
2900
|
-
*
|
|
2901
|
-
* agent was dispatched rather than
|
|
2972
|
+
* edges, and — when the batch now carries a PR (recorded before or detected
|
|
2973
|
+
* here) — the ordinary `deployed` machinery dispatches the report agent next
|
|
2974
|
+
* tick, with teardown after `batch-report done`. With no PR at all no report
|
|
2975
|
+
* agent can be prompted (it names the PR), so the same lock continues
|
|
2976
|
+
* `deployed → reported → done` and the worktree is torn down here instead;
|
|
2977
|
+
* the journal line says why no report agent was dispatched rather than
|
|
2978
|
+
* leaving the gap silent.
|
|
2902
2979
|
*
|
|
2903
2980
|
* Bounded by construction: only `blocked` batches within the window are
|
|
2904
2981
|
* examined, the PR check is one `gh` call, git runs only when PR evidence
|
|
@@ -2910,19 +2987,48 @@ function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
|
|
|
2910
2987
|
const state = deps.store.load();
|
|
2911
2988
|
const nowMs = now.getTime();
|
|
2912
2989
|
for (const batch of state.batches) {
|
|
2913
|
-
if (batch.status !== 'blocked')
|
|
2990
|
+
if (batch.status !== 'blocked') {
|
|
2991
|
+
// #789 review: the marker is scoped to ONE `blocked` stretch, exactly
|
|
2992
|
+
// like `pr_watch_failed_*`'s own `awaiting-merge` scoping (#633) — the
|
|
2993
|
+
// batch left `blocked` (this reconcile's own success below, `sched
|
|
2994
|
+
// resume --batch`, or `sched abandon`), so a LATER re-block — even of
|
|
2995
|
+
// the SAME batch with the SAME candidate PRs — must journal a fresh
|
|
2996
|
+
// first line, never silently continue a stale streak from before.
|
|
2997
|
+
clearPrDetectAmbiguous(deps, batch, now);
|
|
2914
2998
|
continue;
|
|
2999
|
+
}
|
|
2915
3000
|
if (nowMs - Date.parse(batch.updated_at) >= STALE_BLOCKED_RECONCILE_WINDOW_MS)
|
|
2916
3001
|
continue;
|
|
2917
3002
|
if (!everySurvivingMemberDispatched(state, batch))
|
|
2918
3003
|
continue;
|
|
2919
3004
|
let evidence = null;
|
|
3005
|
+
// #789 review: one outer variable for the whole lookup outcome (rather
|
|
3006
|
+
// than three separate booleans/values re-deriving pieces of it) — the
|
|
3007
|
+
// resolution block below reads `lookup.kind` directly instead of a
|
|
3008
|
+
// parallel flag that has to be kept in sync with it by hand.
|
|
3009
|
+
let lookup;
|
|
2920
3010
|
if (batch.pr !== null) {
|
|
2921
3011
|
const truth = deps.groundTruth.prState(batch.pr);
|
|
2922
3012
|
if (truth !== undefined && truth.state === 'MERGED' && truth.mergedAt !== null) {
|
|
2923
3013
|
evidence = { kind: 'pr-merged', pr: batch.pr, mergedAt: truth.mergedAt };
|
|
2924
3014
|
}
|
|
2925
3015
|
}
|
|
3016
|
+
else if (batch.branch !== null) {
|
|
3017
|
+
// #789: batch.pr was never recorded — a hand-opened or manually
|
|
3018
|
+
// recovered PR the ledger never saw (the imboard#4255 shape). Look for
|
|
3019
|
+
// exactly one MERGED PR whose head is the batch branch before falling
|
|
3020
|
+
// through to the weaker branch/members evidence below. Positive
|
|
3021
|
+
// evidence only: `undefined` (unreachable) touches nothing, and
|
|
3022
|
+
// `ambiguous` (more than one candidate) is journaled, never guessed.
|
|
3023
|
+
lookup = deps.groundTruth.mergedPrForBranch?.(batch.branch, batch.base_branch, batch.created_at);
|
|
3024
|
+
if (lookup?.kind === 'found') {
|
|
3025
|
+
evidence = { kind: 'pr-merged', pr: lookup.pr, mergedAt: lookup.mergedAt };
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
// Set only when THIS tick's automatic lookup found the PR — never when
|
|
3029
|
+
// `batch.pr` was already recorded (that PR is reported via `evidence.pr`
|
|
3030
|
+
// as before, and there is nothing new to persist).
|
|
3031
|
+
const detectedPr = lookup?.kind === 'found' ? lookup.pr : null;
|
|
2926
3032
|
if (evidence === null && branchMergedIntoBase(deps, batch)) {
|
|
2927
3033
|
evidence = { kind: 'commits-in-base' };
|
|
2928
3034
|
}
|
|
@@ -2940,12 +3046,20 @@ function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
|
|
|
2940
3046
|
}).kind === 'closable') {
|
|
2941
3047
|
evidence = { kind: 'members-closed' };
|
|
2942
3048
|
}
|
|
3049
|
+
// #789: resolve the ambiguity streak exactly once per tick. An ambiguous
|
|
3050
|
+
// lookup records only when the batch ISN'T reconciling by other evidence
|
|
3051
|
+
// this same tick; any evidence at all (including a `found`/`none` lookup
|
|
3052
|
+
// result) clears a stale or same-tick streak — reconciling via ANY
|
|
3053
|
+
// evidence means the batch is leaving `blocked`, so a same-tick ambiguity
|
|
3054
|
+
// is moot too.
|
|
3055
|
+
if (lookup?.kind === 'ambiguous' && evidence === null) {
|
|
3056
|
+
recordPrDetectAmbiguous(deps, batch, lookup.matches, now);
|
|
3057
|
+
}
|
|
3058
|
+
else if (evidence !== null || lookup?.kind === 'found' || lookup?.kind === 'none') {
|
|
3059
|
+
clearPrDetectAmbiguous(deps, batch, now);
|
|
3060
|
+
}
|
|
2943
3061
|
if (evidence === null)
|
|
2944
3062
|
continue;
|
|
2945
|
-
// A members-closed reconcile has no merged batch PR for a report agent to
|
|
2946
|
-
// report on, whatever `batch.pr` recorded — finish the rail inline, as
|
|
2947
|
-
// for `pr === null`.
|
|
2948
|
-
const finishInline = batch.pr === null || evidence.kind === 'members-closed';
|
|
2949
3063
|
// ...but never tear its worktree down: the members shipped OUTSIDE the
|
|
2950
3064
|
// batch branch, so the blocked integration worktree may hold an
|
|
2951
3065
|
// operator's unpushed repair work. It is left in place, inert, and the
|
|
@@ -2955,6 +3069,12 @@ function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
|
|
|
2955
3069
|
const failedAt = batch.updated_at;
|
|
2956
3070
|
let reconciled = false;
|
|
2957
3071
|
let unwalkable = [];
|
|
3072
|
+
// #789 review: what the lock ACTUALLY applied, not what the outer
|
|
3073
|
+
// snapshot predicted — re-derived from `b` (read under the lock) below,
|
|
3074
|
+
// since `batch.pr` could in principle have changed between the snapshot
|
|
3075
|
+
// above and the lock acquiring (e.g. a future `sched attach-pr`, #824).
|
|
3076
|
+
let effectivePr = null;
|
|
3077
|
+
let prWasRecorded = false;
|
|
2958
3078
|
deps.store.withLock((s) => {
|
|
2959
3079
|
const b = (0, state_1.findBatch)(s, batch.id);
|
|
2960
3080
|
// Re-check under the lock: `sched resume --batch` (a passing recheck)
|
|
@@ -2962,7 +3082,19 @@ function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
|
|
|
2962
3082
|
// snapshot this pass started from.
|
|
2963
3083
|
if (!b || b.status !== 'blocked')
|
|
2964
3084
|
return { state: s, result: undefined };
|
|
2965
|
-
|
|
3085
|
+
// #789: record the auto-detected PR as part of the SAME transition that
|
|
3086
|
+
// leaves `blocked` — a batch whose ledger never had one now does. Only
|
|
3087
|
+
// when `b.pr` is STILL null under the lock — never overwrite a PR
|
|
3088
|
+
// something else recorded between the snapshot and here.
|
|
3089
|
+
prWasRecorded = detectedPr !== null && b.pr === null;
|
|
3090
|
+
effectivePr = prWasRecorded ? detectedPr : b.pr;
|
|
3091
|
+
let n = (0, state_1.transitionBatch)(s, batch.id, 'merged', {
|
|
3092
|
+
// The ambiguity streak (this tick's own, or a stale one) is moot
|
|
3093
|
+
// the instant the batch reconciles by any evidence — fold the
|
|
3094
|
+
// clear into this same transition instead of a separate lock write.
|
|
3095
|
+
...state_1.CLEARED_PR_DETECT_AMBIGUOUS_FIELDS,
|
|
3096
|
+
...(prWasRecorded ? { pr: detectedPr } : {}),
|
|
3097
|
+
}, now);
|
|
2966
3098
|
n = (0, state_1.transitionBatch)(n, batch.id, 'deployed', {}, now);
|
|
2967
3099
|
unwalkable = [];
|
|
2968
3100
|
for (const issue of b.members) {
|
|
@@ -2986,7 +3118,11 @@ function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
|
|
|
2986
3118
|
entry = (0, state_1.findEntry)(n, issue);
|
|
2987
3119
|
}
|
|
2988
3120
|
}
|
|
2989
|
-
|
|
3121
|
+
// The batch now carries a PR — recorded before, recorded just above,
|
|
3122
|
+
// or still null — only when one exists does the ordinary `deployed`
|
|
3123
|
+
// machinery have anything to hand the report agent. A members-closed
|
|
3124
|
+
// reconcile has no merged batch PR to report on regardless.
|
|
3125
|
+
if (effectivePr === null || evidence.kind === 'members-closed') {
|
|
2990
3126
|
// No (merged) PR → no report agent is possible (its prompt names the
|
|
2991
3127
|
// PR). Finish the terminal rail and let teardown run below.
|
|
2992
3128
|
n = (0, state_1.transitionBatch)(n, batch.id, 'reported', {}, now);
|
|
@@ -2997,14 +3133,19 @@ function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
|
|
|
2997
3133
|
});
|
|
2998
3134
|
if (!reconciled)
|
|
2999
3135
|
continue;
|
|
3136
|
+
const finishInline = effectivePr === null || evidence.kind === 'members-closed';
|
|
3000
3137
|
journalEvent(deps, 'stale-failure-reconciled', unit(batch.id), {
|
|
3001
|
-
pr:
|
|
3138
|
+
pr: effectivePr ?? undefined,
|
|
3002
3139
|
reason: blockedReason,
|
|
3003
3140
|
failedAt,
|
|
3004
3141
|
evidence: evidence.kind,
|
|
3005
3142
|
...(evidence.kind === 'pr-merged' ? { mergedAt: evidence.mergedAt } : {}),
|
|
3143
|
+
...(prWasRecorded ? { pr_detected: true } : {}),
|
|
3006
3144
|
detail: (evidence.kind === 'pr-merged'
|
|
3007
|
-
?
|
|
3145
|
+
? (prWasRecorded
|
|
3146
|
+
? `batch.pr was never recorded — detected PR #${evidence.pr} MERGED with head=${batch.branch} (gh pr list --state merged), recorded batch.pr=${evidence.pr}`
|
|
3147
|
+
: `PR #${evidence.pr} is MERGED`) +
|
|
3148
|
+
` — ledger reconciled blocked (${blockedReason}) to the merged rail (blocked at ${failedAt}); report and teardown will now dispatch`
|
|
3008
3149
|
: evidence.kind === 'members-closed'
|
|
3009
3150
|
? `every member issue is CLOSED as completed on GitHub with no failure trail — ledger reconciled blocked (${blockedReason}) to shipped (blocked at ${failedAt})`
|
|
3010
3151
|
: `every member commit is an ancestor of ${batch.base_branch} — ledger reconciled blocked (${blockedReason}) to shipped (blocked at ${failedAt})`) +
|
|
@@ -3221,12 +3362,7 @@ function recordAnchorClosed(deps, batchId, now) {
|
|
|
3221
3362
|
* stale streak's count. A no-op (no write) when no streak is recorded.
|
|
3222
3363
|
*/
|
|
3223
3364
|
function clearAnchorCloseFailed(deps, batch, now) {
|
|
3224
|
-
|
|
3225
|
-
return;
|
|
3226
|
-
deps.store.withLock((s) => ({
|
|
3227
|
-
state: (0, state_1.patchBatch)(s, batch.id, state_1.CLEARED_ANCHOR_CLOSE_FAILED_FIELDS, now, false),
|
|
3228
|
-
result: undefined,
|
|
3229
|
-
}));
|
|
3365
|
+
clearBatchDedupMarker(deps, batch, batch.anchor_close_failed_reason, state_1.CLEARED_ANCHOR_CLOSE_FAILED_FIELDS, now);
|
|
3230
3366
|
}
|
|
3231
3367
|
/**
|
|
3232
3368
|
* A close that qualified but failed: journal it on the streak's first tick and
|
|
@@ -3234,11 +3370,8 @@ function clearAnchorCloseFailed(deps, batch, now) {
|
|
|
3234
3370
|
* exactly `reconcilePrWatch`'s `pr-watch-failed` dedup.
|
|
3235
3371
|
*/
|
|
3236
3372
|
function recordAnchorCloseFailed(deps, batch, outcome, shipped, now) {
|
|
3237
|
-
const
|
|
3238
|
-
|
|
3239
|
-
const ticks = isNewStreak ? 1 : batch.anchor_close_failed_ticks + 1;
|
|
3240
|
-
const since = isNewStreak || priorSince === null ? now.toISOString() : priorSince;
|
|
3241
|
-
if (isNewStreak || ticks % types_1.JOURNAL_DEDUP_REANNOUNCE_TICKS === 0) {
|
|
3373
|
+
const { since, ticks, announce } = advanceBatchDedupStreak(batch.anchor_close_failed_reason, batch.anchor_close_failed_since, batch.anchor_close_failed_ticks, outcome, now);
|
|
3374
|
+
if (announce) {
|
|
3242
3375
|
journalEvent(deps, 'anchor-close-failed', unit(batch.id), {
|
|
3243
3376
|
issue: batch.anchor ?? undefined,
|
|
3244
3377
|
reason: outcome,
|