@ai-dossier/sched 0.53.0 → 0.54.1

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 CHANGED
@@ -37,6 +37,7 @@ ai-dossier sched stop --batch b1 # terminate every batch agent (incl. parallel
37
37
  ai-dossier sched abandon --issue 42 --reason "operator abort"
38
38
  ai-dossier sched abandon --batch b1 # dissolve; every unshipped member — parked and validated ones included — requeues as full-cycle
39
39
  ai-dossier sched requeue --issue 42 # a PARKED batch member (evicted / handed-back) → full-cycle from its member branch, on the batch profile (#810)
40
+ ai-dossier sched attach-pr --batch b1 4270 # record a blocked batch's own MERGED PR by hand (resolves pr-detect-ambiguous, #824)
40
41
  ai-dossier sched stats --issues 4..9 # per-issue tokens/cost from ~/.dossier/runs.jsonl (#524)
41
42
  ai-dossier sched stats --batch b1 --project owner-repo # batch member/tail/report/fix costs from raw dispatch logs (#564)
42
43
  ```
@@ -1133,8 +1134,9 @@ import {
1133
1134
  createSpawnDeps, // real detached-spawn process I/O
1134
1135
  createExecGroundTruth, // runstate/gh/git ground truth via subprocesses (injectable exec);
1135
1136
  // since #468 also gh pr view PR state + setup info from comments;
1136
- // since #789 also mergedPrForBranch when constructed with a
1137
- // verified `repo` (resolveProjectRepo)
1137
+ // since #789 also mergedPrForBranch, and since #824
1138
+ // batchPrCandidate, when constructed with a verified `repo`
1139
+ // (resolveProjectRepo) — which also pins prState with -R
1138
1140
  resolveDispatch, // config → resolved command/prompt/report-prompt/tier-models/timers/
1139
1141
  // per-tier spawn specs (tiers — #527)
1140
1142
  buildTierCommand, // resolved dispatch + tier + issue → argv, using that tier's OWN
@@ -1168,6 +1170,16 @@ import {
1168
1170
  type MergedPrLookup, // { kind: 'found', pr, mergedAt } | { kind: 'none' } |
1169
1171
  // { kind: 'ambiguous', matches } — GroundTruth.mergedPrForBranch's
1170
1172
  // own return type (#789)
1173
+ checkBatchPrCandidate, // the #789 candidate checks (fork/head/base/merged/created-after)
1174
+ // → { ok: true, pr, mergedAt } | { ok: false, check } — shared by
1175
+ // parseMergedPrListJson and attachBatchPr (#824)
1176
+ type BatchPrCheck, // which check failed: fork|head|base|unmerged|created-before-batch|number
1177
+ parsePrCandidateJson, // gh pr view --json → the PR object (GroundTruth.batchPrCandidate, #824)
1178
+ attachBatchPr, // sched attach-pr: verify an operator-named PR, record batch.pr,
1179
+ // clear the pr-detect-ambiguous streak, journal pr-attached (#824)
1180
+ type AttachPrDeps, // { store, journal, groundTruth (repo-verified), repo? }
1181
+ type AttachPrResult, // { outcome: 'attached', pr, mergedAt, clearedAmbiguousTicks } |
1182
+ // { outcome: 'already-attached', pr }
1171
1183
  parseSetupInfo, // gh issue view --json comments → teardown inputs
1172
1184
  runTeardown, // #468 script teardown for a merged unit (pool return / worktree remove)
1173
1185
  isSafeWorktree, // worktree-path containment check (CWE-22)
@@ -1547,7 +1559,8 @@ predicate in `anchor-close.ts`) over `blocked`/`done` batches touched within the
1547
1559
  batch that same tick is the ambiguity journaled (`pr-detect-ambiguous`, deduped like
1548
1560
  `pr-watch-failed`: once per `blocked` stretch, re-announced every
1549
1561
  `JOURNAL_DEDUP_REANNOUNCE_TICKS`; a batch that leaves `blocked` any way — this
1550
- reconcile's own success, `sched resume --batch`, `sched abandon` — clears the marker,
1562
+ reconcile's own success, `sched resume --batch`, `sched abandon` — or `sched
1563
+ attach-pr` recording the batch's PR clears the marker,
1551
1564
  so a later re-block never inherits a stale streak). A batch that still reconciles via
1552
1565
  `commits-in-base`/`members-closed` does so without a PR and finishes inline, same as
1553
1566
  before #789. On a match, `batch.pr` is recorded as part of the SAME `blocked →
@@ -1557,9 +1570,34 @@ predicate in `anchor-close.ts`) over `blocked`/`done` batches touched within the
1557
1570
  per-issue-only despite the section's origin) dispatches the report agent exactly as if the
1558
1571
  fleet had opened the PR itself, and `sched status`'s existing `pr` column shows it.
1559
1572
  Recording `batch.pr` never closes the anchor by itself — that stays #768's own
1560
- evidence-gated `reconcileAnchorClosure`, unchanged. An ambiguous match with no
1561
- explicit way to resolve it today (#824 tracks a `sched attach-pr` verb) still lets the
1562
- batch reconcile via its other evidence, or falls to `sched abandon --batch`.
1573
+ evidence-gated `reconcileAnchorClosure`, unchanged. An ambiguous match still lets the
1574
+ batch reconcile via its other evidence; otherwise the operator resolves it with
1575
+ `sched attach-pr` (below), or falls to `sched abandon --batch`.
1576
+ - **`sched attach-pr --batch <id> <pr>` (#824).** The operator's explicit, auditable
1577
+ answer to `pr-detect-ambiguous` — and the scripted form of the `batch-integrate`
1578
+ dossier's manual-recovery step that hand-records a batch PR (Step 6b of that registry
1579
+ dossier; `<pr>` also accepts `#4270`). Positive evidence only: the project repository is verified first
1580
+ (`resolveProjectRepo`; unverifiable → refused before any PR is read), the PR is read
1581
+ with `gh pr view <pr> -R <owner/name>` (`GroundTruth.batchPrCandidate`, repo-gated like
1582
+ `mergedPrForBranch`), and it must pass the SAME candidate checks #789's automatic path
1583
+ applies (`checkBatchPrCandidate`: not a fork, head = `batch.branch`, base =
1584
+ `batch.base_branch`, a real merge timestamp, created at or after the batch) plus an
1585
+ explicit `state == MERGED` and a positively-read `isCrossRepository == false`. Any
1586
+ mismatch, a failed or unusable gh read, a batch that is not `blocked` (or has no
1587
+ branch), or a batch whose `batch.pr` is already a DIFFERENT PR refuses with a message
1588
+ naming why — nothing written, nothing journaled; the same PR already recorded is a
1589
+ no-op. There is no `--force`: every check reads a field `gh pr view` always returns.
1590
+ On success `batch.pr` is recorded and the `pr_detect_ambiguous_*` streak cleared in
1591
+ one locked write (`updated_at` is touched, which re-arms the 7-day reconcile window),
1592
+ journaled as its own `pr-attached` event — never `stale-failure-reconciled` /
1593
+ `pr_detected`, so `events.jsonl` always tells operator from automatic apart. Nothing
1594
+ else happens in the command: the next tick's stale-blocked reconcile settles the batch
1595
+ on its ordinary `pr-merged` evidence (report + teardown follow; that read, `prState`,
1596
+ is `-R`-pinned too whenever the engine's repo is verified), and the anchor closes only
1597
+ through `reconcileAnchorClosure`, which never reads `batch.pr`. A batch with a surviving
1598
+ member that was never dispatched is refused too — the reconcile would never settle it
1599
+ on PR evidence. `--json` emits `{batch, repo, outcome: "attached", pr, mergedAt,
1600
+ clearedAmbiguousTicks}` or `{batch, repo, outcome: "already-attached", pr}`.
1563
1601
 
1564
1602
  Everything else is surfaced, never closed: `sched status --anchors` (opt-in; `status`
1565
1603
  makes no GitHub call without it) lists each still-open anchor of a batch no longer in
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `sched attach-pr --batch <id> <pr>` (#824): the operator's explicit, auditable
3
+ * way to record `batch.pr` when #789's automatic detection refused to guess
4
+ * (`pr-detect-ambiguous` — two or more merged PRs share the batch branch), and
5
+ * the scripted form of the `batch-integrate` dossier's manual-recovery step
6
+ * that hand-records a batch PR (the registry dossier's Step 6b).
7
+ *
8
+ * Trust boundary: the operator's word is not evidence. The named PR must pass
9
+ * the SAME candidate checks #789's automatic path applies
10
+ * (`checkBatchPrCandidate`) plus an explicit `state === 'MERGED'` and a
11
+ * positively-read same-repo flag, read through a ground truth pinned with `-R`
12
+ * to the verified project repository — absent that pin, nothing is read and
13
+ * nothing is recorded. There is no `--force`: every check reads a field
14
+ * `gh pr view` always returns, so an unreadable one refuses rather than
15
+ * inviting an override.
16
+ *
17
+ * Recording is ALL this does: `batch.pr` is set (and any
18
+ * `pr_detect_ambiguous_*` streak cleared) under the store lock, with no status
19
+ * transition and no GitHub write. The next engine tick's
20
+ * `reconcileStaleBlockedBatches` then settles the batch on its ordinary
21
+ * `pr-merged` evidence, and the anchor closes (or not) only through #768's own
22
+ * evidence-gated `reconcileAnchorClosure` — which never reads `batch.pr`.
23
+ */
24
+ import type { GroundTruth } from './groundtruth';
25
+ import type { Journal } from './journal';
26
+ import type { SchedStore } from './persist';
27
+ export interface AttachPrDeps {
28
+ store: SchedStore;
29
+ journal: Journal;
30
+ /** Must come from `createExecGroundTruth` with a verified `repo` — else `batchPrCandidate` is absent and the attach refuses. */
31
+ groundTruth: GroundTruth;
32
+ /** The verified `owner/name` the ground truth is pinned to — named in refusals and the journal line. */
33
+ repo?: string;
34
+ }
35
+ export type AttachPrResult = {
36
+ outcome: 'attached';
37
+ pr: number;
38
+ mergedAt: string;
39
+ /** Ticks of the `pr-detect-ambiguous` streak this attach cleared (0 when none was open). */
40
+ clearedAmbiguousTicks: number;
41
+ } | {
42
+ outcome: 'already-attached';
43
+ pr: number;
44
+ };
45
+ /**
46
+ * Record an operator-named PR as `batch.pr` after verifying it (see the module
47
+ * comment). Throws `SchedNotFoundError` — nothing written, nothing journaled —
48
+ * on every refusal; returns `already-attached` (also nothing written) when the
49
+ * same PR is already recorded.
50
+ */
51
+ export declare function attachBatchPr(deps: AttachPrDeps, batchId: string, pr: number, now?: Date): AttachPrResult;
52
+ //# sourceMappingURL=attach-pr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach-pr.d.ts","sourceRoot":"","sources":["../src/attach-pr.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,eAAe,CAAC;AAE/D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAI5C,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,UAAU,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,gIAAgI;IAChI,WAAW,EAAE,WAAW,CAAC;IACzB,wGAAwG;IACxG,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,cAAc,GACtB;IACE,OAAO,EAAE,UAAU,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,4FAA4F;IAC5F,qBAAqB,EAAE,MAAM,CAAC;CAC/B,GACD;IAAE,OAAO,EAAE,kBAAkB,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AA6HhD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,MAAM,EACV,GAAG,GAAE,IAAiB,GACrB,cAAc,CAqEhB"}
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ /**
3
+ * `sched attach-pr --batch <id> <pr>` (#824): the operator's explicit, auditable
4
+ * way to record `batch.pr` when #789's automatic detection refused to guess
5
+ * (`pr-detect-ambiguous` — two or more merged PRs share the batch branch), and
6
+ * the scripted form of the `batch-integrate` dossier's manual-recovery step
7
+ * that hand-records a batch PR (the registry dossier's Step 6b).
8
+ *
9
+ * Trust boundary: the operator's word is not evidence. The named PR must pass
10
+ * the SAME candidate checks #789's automatic path applies
11
+ * (`checkBatchPrCandidate`) plus an explicit `state === 'MERGED'` and a
12
+ * positively-read same-repo flag, read through a ground truth pinned with `-R`
13
+ * to the verified project repository — absent that pin, nothing is read and
14
+ * nothing is recorded. There is no `--force`: every check reads a field
15
+ * `gh pr view` always returns, so an unreadable one refuses rather than
16
+ * inviting an override.
17
+ *
18
+ * Recording is ALL this does: `batch.pr` is set (and any
19
+ * `pr_detect_ambiguous_*` streak cleared) under the store lock, with no status
20
+ * transition and no GitHub write. The next engine tick's
21
+ * `reconcileStaleBlockedBatches` then settles the batch on its ordinary
22
+ * `pr-merged` evidence, and the anchor closes (or not) only through #768's own
23
+ * evidence-gated `reconcileAnchorClosure` — which never reads `batch.pr`.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.attachBatchPr = attachBatchPr;
27
+ const batch_dispatch_1 = require("./batch-dispatch");
28
+ const groundtruth_1 = require("./groundtruth");
29
+ const journal_1 = require("./journal");
30
+ const state_1 = require("./state");
31
+ const types_1 = require("./types");
32
+ /**
33
+ * Why each shared candidate check refuses, in operator words. `number` and
34
+ * `fork` are pre-empted by `verifyCandidate`'s own stricter checks and exist
35
+ * for the record's exhaustiveness (`fork` is also used directly there).
36
+ */
37
+ const CHECK_REFUSALS = {
38
+ fork: () => 'it is from a fork (isCrossRepository=true), not the project repository',
39
+ head: (pr, batch) => `its head is ${JSON.stringify(pr.headRefName)}, not the batch branch ${JSON.stringify(batch.branch)}`,
40
+ base: (pr, batch) => `its base is ${JSON.stringify(pr.baseRefName)}, not the batch base ${JSON.stringify(batch.base_branch)}`,
41
+ unmerged: () => 'it has no merge timestamp — not merged',
42
+ 'created-before-batch': (pr, batch) => `it was created ${JSON.stringify(pr.createdAt)}, before the batch itself (${batch.created_at}) — a PR from an earlier use of the branch`,
43
+ number: () => 'gh returned no usable PR number',
44
+ };
45
+ /** The one refusal shape every PR-level refusal uses. */
46
+ function refusal(batchId, pr, why) {
47
+ return new types_1.SchedNotFoundError(`Refusing to attach PR #${pr} to batch ${batchId}: ${why}`);
48
+ }
49
+ /**
50
+ * The ledger-only preconditions, re-run under the lock on fresh state.
51
+ * Throws `SchedNotFoundError` naming why on every refusal.
52
+ */
53
+ function precheck(state, batchId, pr) {
54
+ const batch = (0, state_1.findBatch)(state, batchId);
55
+ if (!batch)
56
+ throw new types_1.SchedNotFoundError(`Batch not found: ${batchId}`);
57
+ if (batch.pr === pr)
58
+ return { noop: true };
59
+ if (batch.pr !== null) {
60
+ throw new types_1.SchedNotFoundError(`Batch ${batchId} already has PR #${batch.pr} recorded — refusing to replace it with #${pr}`);
61
+ }
62
+ if (batch.status !== 'blocked') {
63
+ throw new types_1.SchedNotFoundError(`Batch ${batchId} is ${batch.status} — attach-pr only applies to a blocked batch (the state whose merge evidence reconcileStaleBlockedBatches settles from a recorded PR)`);
64
+ }
65
+ const { branch } = batch;
66
+ if (branch === null) {
67
+ throw new types_1.SchedNotFoundError(`Batch ${batchId} has no branch recorded — nothing to check PR #${pr}'s head against`);
68
+ }
69
+ // The reconcile never settles a batch with a surviving member that was never
70
+ // dispatched (it would call an issue `done` that shipped nothing), so a PR
71
+ // recorded here would sit unused forever — refuse instead of reporting a
72
+ // reconcile that will not happen.
73
+ const undispatched = (0, batch_dispatch_1.undispatchedSurvivingMembers)(state, batch);
74
+ if (undispatched.length > 0) {
75
+ throw new types_1.SchedNotFoundError(`Batch ${batchId} has member(s) ${undispatched.map((m) => `#${m}`).join(', ')} that were never dispatched — the stale-blocked reconcile will not settle it on PR evidence (it would mark unshipped work done); \`sched abandon --batch ${batchId}\` requeues them instead`);
76
+ }
77
+ return { noop: false, batch: { ...batch, branch } };
78
+ }
79
+ /** Refuse unless `candidate` is provably the batch's own merged PR `pr`; returns its merge time. */
80
+ function verifyCandidate(candidate, batch, pr) {
81
+ if (candidate.number !== pr) {
82
+ throw refusal(batch.id, pr, `gh answered for PR ${JSON.stringify(candidate.number)}, not #${pr}`);
83
+ }
84
+ if (candidate.state !== 'MERGED') {
85
+ throw refusal(batch.id, pr, `it is ${JSON.stringify(candidate.state)}, not MERGED`);
86
+ }
87
+ // Positive evidence only: `checkBatchPrCandidate` treats a MISSING flag as
88
+ // "not a fork" (its #789 list convention); an operator-named PR must be
89
+ // read as same-repo explicitly.
90
+ if (candidate.isCrossRepository !== false) {
91
+ throw refusal(batch.id, pr, candidate.isCrossRepository === true
92
+ ? CHECK_REFUSALS.fork(candidate, batch)
93
+ : 'gh did not report whether it is from the project repository or a fork (isCrossRepository unreadable)');
94
+ }
95
+ const verdict = (0, groundtruth_1.checkBatchPrCandidate)(candidate, batch.branch, batch.base_branch, Date.parse(batch.created_at));
96
+ if (!verdict.ok)
97
+ throw refusal(batch.id, pr, CHECK_REFUSALS[verdict.check](candidate, batch));
98
+ return verdict.mergedAt;
99
+ }
100
+ /** The `pr-attached` journal line's `detail`. */
101
+ function attachedDetail(prRef, mergedAt, batch, clearedAmbiguousTicks) {
102
+ return (`operator attached ${prRef} (MERGED ${mergedAt}, head=${batch.branch}, base=${batch.base_branch}, same repository) — recorded batch.pr` +
103
+ (clearedAmbiguousTicks > 0
104
+ ? `; cleared the pr-detect-ambiguous streak (${clearedAmbiguousTicks} tick(s))`
105
+ : '') +
106
+ '; the next engine tick reconciles the batch on pr-merged evidence — the anchor is left to #768');
107
+ }
108
+ /**
109
+ * Record an operator-named PR as `batch.pr` after verifying it (see the module
110
+ * comment). Throws `SchedNotFoundError` — nothing written, nothing journaled —
111
+ * on every refusal; returns `already-attached` (also nothing written) when the
112
+ * same PR is already recorded.
113
+ */
114
+ function attachBatchPr(deps, batchId, pr, now = new Date()) {
115
+ if (!Number.isInteger(pr) || pr <= 0) {
116
+ throw new types_1.SchedNotFoundError(`PR must be a positive integer, got ${pr}`);
117
+ }
118
+ const first = precheck(deps.store.load(), batchId, pr);
119
+ if (first.noop)
120
+ return { outcome: 'already-attached', pr };
121
+ const repo = deps.repo ?? 'the project repository';
122
+ const read = deps.groundTruth.batchPrCandidate;
123
+ if (read === undefined) {
124
+ throw refusal(batchId, pr, "the project's GitHub repository is not verified, so the PR cannot be read with a pinned -R — run from the project's own checkout (or check --project)");
125
+ }
126
+ const candidate = read(pr);
127
+ if (candidate === undefined) {
128
+ throw refusal(batchId, pr, `could not read it from ${repo} (\`gh pr view ${pr} -R ${repo}\` failed or returned an unusable payload — see the stderr line above) — nothing recorded`);
129
+ }
130
+ verifyCandidate(candidate, first.batch, pr);
131
+ // The GitHub read ran outside the lock: re-run the ledger checks AND the
132
+ // candidate checks against the fresh batch, so a batch that left `blocked`,
133
+ // had a PR recorded, or changed its branch/base meanwhile is refused rather
134
+ // than written over.
135
+ const applied = deps.store.withLock((s) => {
136
+ const fresh = precheck(s, batchId, pr);
137
+ if (fresh.noop)
138
+ return { state: s, result: null };
139
+ const mergedAt = verifyCandidate(candidate, fresh.batch, pr);
140
+ // `updated_at` IS touched (unlike the dedup writes): an operator attach is
141
+ // real activity, and it re-arms the stale-blocked reconcile window
142
+ // (measured from `updated_at`), so the PR is acted on even when the batch
143
+ // had been blocked longer than that window.
144
+ return {
145
+ state: (0, state_1.patchBatch)(s, batchId, { pr, ...state_1.CLEARED_PR_DETECT_AMBIGUOUS_FIELDS }, now),
146
+ result: {
147
+ batch: fresh.batch,
148
+ mergedAt,
149
+ clearedAmbiguousTicks: fresh.batch.pr_detect_ambiguous_ticks,
150
+ },
151
+ };
152
+ });
153
+ if (applied === null)
154
+ return { outcome: 'already-attached', pr };
155
+ deps.journal.append((0, journal_1.unitEvent)('pr-attached', `batch:${batchId}`, {
156
+ pr,
157
+ mergedAt: applied.mergedAt,
158
+ branch: applied.batch.branch,
159
+ detail: attachedDetail(deps.repo !== undefined ? `${deps.repo}#${pr}` : `PR #${pr}`, applied.mergedAt, applied.batch, applied.clearedAmbiguousTicks),
160
+ }), now);
161
+ return {
162
+ outcome: 'attached',
163
+ pr,
164
+ mergedAt: applied.mergedAt,
165
+ clearedAmbiguousTicks: applied.clearedAmbiguousTicks,
166
+ };
167
+ }
168
+ //# sourceMappingURL=attach-pr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach-pr.js","sourceRoot":"","sources":["../src/attach-pr.ts"],"names":[],"mappings":"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"}
@@ -250,6 +250,14 @@ export declare function resumeBlockedGate(deps: BatchDispatchDeps, config: Sched
250
250
  * ~375k tokens, stopped only by hand).
251
251
  */
252
252
  export declare const MAX_BATCH_AGENT_RESPAWNS = 2;
253
+ /**
254
+ * The surviving members `everySurvivingMemberDispatched` finds NOT yet
255
+ * dispatched — the members that keep a blocked batch out of
256
+ * `reconcileStaleBlockedBatches` (#686). Exported for `sched attach-pr`
257
+ * (#824), which refuses to record a PR the reconcile would then never act on,
258
+ * and names these members in the refusal.
259
+ */
260
+ export declare function undispatchedSurvivingMembers(state: SchedState, batch: BatchEntry): number[];
253
261
  /**
254
262
  * #686: reconcile a `blocked` batch whose work demonstrably shipped anyway.
255
263
  * `reconcilePrWatch` above only watches `awaiting-merge` batches, and
@@ -292,6 +300,56 @@ export declare const MAX_BATCH_AGENT_RESPAWNS = 2;
292
300
  * #632 lesson: no journal inside a loop that doesn't terminate the unit).
293
301
  */
294
302
  export declare function reconcileStaleBlockedBatches(deps: BatchDispatchDeps, now: Date, result: BatchTickResult, anchorCtx: AnchorTickContext | undefined): void;
303
+ /**
304
+ * #791: once a `done` batch's kept worktree (left in place by the
305
+ * `members-closed` branch above) is DEFINITIVELY gone — the path no longer
306
+ * exists on disk, or the pool itself now reports it back as a warm spare —
307
+ * clear the ledger's `worktree`/`pool_claimed` (and, separately,
308
+ * `member_worktree`/`member_pool_claimed`, and — #834 — each live
309
+ * `member_runs[]` entry's `torn_down`) fields so `sched status`'s
310
+ * `kept-worktree` warning does not persist after the operator's cleanup
311
+ * already happened. Ledger-only, by construction: this function reads
312
+ * `fsExists` and `worktree-pool status --json` (both read-only) and never
313
+ * builds a `git worktree remove` / `worktree-pool return` argv — the actual
314
+ * removal stays entirely the operator's call, exactly like the warning
315
+ * itself. Bounded the same way the warning's own candidate scan is: only
316
+ * `done` batches carrying a kept worktree are considered, which in practice
317
+ * is a small set.
318
+ *
319
+ * #834: `MemberRun.worktree` is a required `string` (unlike
320
+ * `BatchEntry.worktree: string | null`) — there is no null to clear it to,
321
+ * so "cleared" for a member run means `torn_down: true`, the same flag
322
+ * `markTornDown` sets after a real teardown. Set here it means the same
323
+ * thing evidentially (nothing is holding this path any more), just reached
324
+ * by definitive evidence instead of by running `teardownParallelRuns`.
325
+ *
326
+ * #834 discovery: unlike `worktree`/`member_worktree`, a terminal batch's
327
+ * live `member_runs[]` entries are ALSO reached by the unconditional
328
+ * terminal-batch arm in `runBatchTick` (search `TERMINAL_BATCH_STATUSES.has(batch.status)`
329
+ * above the call site below) — added by #809 for `sched stop`/`abandon`,
330
+ * but written generically enough that it fires for EVERY terminal batch,
331
+ * `members-closed`-reconciled ones included, and it runs earlier in the
332
+ * same tick than this function. It calls `teardownParallelRuns` (an actual
333
+ * teardown attempt, unlike this read-only reconcile) and — notably — calls
334
+ * `markTornDown` unconditionally, even when that teardown attempt itself
335
+ * FAILS (e.g. `isSafeWorktree` rejects the path). So in the normal
336
+ * scheduler-ticking case this function's own `member_runs[]` branch below
337
+ * is effectively a backstop, not the primary path: by the time it runs,
338
+ * `torn_down` is usually already `true` — correctly when the tree really
339
+ * is gone, but also when a real teardown attempt failed and left the tree
340
+ * on disk — filed as #855 (out of scope here; #834 only extends the
341
+ * evidence-based ledger reconcile, it does not change teardown behavior).
342
+ * This function's own branch still matters for the window before that
343
+ * safety net's next tick runs, and for `sched status` reads taken while
344
+ * the scheduler isn't ticking at all.
345
+ *
346
+ * Exported (not part of the package's public `index.ts` surface — same
347
+ * test-only convention as `reconcileStaleBlockedBatches`) so `member_runs[]`
348
+ * tests can call it directly, isolated from the terminal-batch safety net
349
+ * described above, which otherwise always beats it to `member_runs[]` when
350
+ * both run inside one `runBatchTick` call.
351
+ */
352
+ export declare function reconcileKeptWorktrees(deps: BatchDispatchDeps, now: Date): void;
295
353
  /**
296
354
  * #768: what one tick's anchor work reads GitHub and git through — built once
297
355
  * per tick so the stale-blocked reconcile and the anchor close never read the
@@ -1 +1 @@
1
- {"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"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"}
@@ -100,7 +100,9 @@ exports.MAX_BATCH_AGENT_RESPAWNS = exports.tailMembersRefusal = void 0;
100
100
  exports.memberBranchFor = memberBranchFor;
101
101
  exports.evictMemberAndContinue = evictMemberAndContinue;
102
102
  exports.resumeBlockedGate = resumeBlockedGate;
103
+ exports.undispatchedSurvivingMembers = undispatchedSurvivingMembers;
103
104
  exports.reconcileStaleBlockedBatches = reconcileStaleBlockedBatches;
105
+ exports.reconcileKeptWorktrees = reconcileKeptWorktrees;
104
106
  exports.memberDispatchModeFor = memberDispatchModeFor;
105
107
  exports.runBatchTick = runBatchTick;
106
108
  const fs = __importStar(require("node:fs"));
@@ -3141,7 +3143,18 @@ const MEMBER_RECONCILE_CHAIN = {
3141
3143
  * and the branch is exactly what the merge evidence below covers.
3142
3144
  */
3143
3145
  function everySurvivingMemberDispatched(state, batch) {
3146
+ return undispatchedSurvivingMembers(state, batch).length === 0;
3147
+ }
3148
+ /**
3149
+ * The surviving members `everySurvivingMemberDispatched` finds NOT yet
3150
+ * dispatched — the members that keep a blocked batch out of
3151
+ * `reconcileStaleBlockedBatches` (#686). Exported for `sched attach-pr`
3152
+ * (#824), which refuses to record a PR the reconcile would then never act on,
3153
+ * and names these members in the refusal.
3154
+ */
3155
+ function undispatchedSurvivingMembers(state, batch) {
3144
3156
  const evicted = new Set(batch.evictions.map((e) => e.issue));
3157
+ const undispatched = [];
3145
3158
  for (const issue of batch.members) {
3146
3159
  const entry = (0, state_1.findEntry)(state, issue);
3147
3160
  if (!entry)
@@ -3159,9 +3172,9 @@ function everySurvivingMemberDispatched(state, batch) {
3159
3172
  // completion — everything the entry can reach afterwards means the
3160
3173
  // member was dispatched and its work is on the branch.
3161
3174
  if (!DISPATCHED_MEMBER_STATUSES.has(entry.status))
3162
- return false;
3175
+ undispatched.push(issue);
3163
3176
  }
3164
- return true;
3177
+ return undispatched;
3165
3178
  }
3166
3179
  /** Slot-line statuses that prove a member was actually dispatched (see above). */
3167
3180
  const DISPATCHED_MEMBER_STATUSES = new Set([
@@ -3228,6 +3241,26 @@ function clearPrDetectAmbiguous(deps, batch, now) {
3228
3241
  */
3229
3242
  function recordPrDetectAmbiguous(deps, batch, matches, now) {
3230
3243
  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);
3244
+ // #824 review: the lookup above ran outside the lock — an operator's
3245
+ // `sched attach-pr` (or a resume/abandon) may have landed meanwhile. Write
3246
+ // the streak (and journal it) only while the batch is STILL blocked with no
3247
+ // PR; never rewrite the marker an attach just cleared, nor tell the
3248
+ // operator to run `sched attach-pr` after they already did.
3249
+ const written = deps.store.withLock((s) => {
3250
+ const b = (0, state_1.findBatch)(s, batch.id);
3251
+ if (!b || b.status !== 'blocked' || b.pr !== null)
3252
+ return { state: s, result: false };
3253
+ return {
3254
+ state: (0, state_1.patchBatch)(s, batch.id, {
3255
+ pr_detect_ambiguous_reason: state_1.PR_DETECT_AMBIGUOUS_REASON,
3256
+ pr_detect_ambiguous_since: since,
3257
+ pr_detect_ambiguous_ticks: ticks,
3258
+ }, now, false),
3259
+ result: true,
3260
+ };
3261
+ });
3262
+ if (!written)
3263
+ return;
3231
3264
  if (announce) {
3232
3265
  journalEvent(deps, 'pr-detect-ambiguous', unit(batch.id), {
3233
3266
  reason: state_1.PR_DETECT_AMBIGUOUS_REASON,
@@ -3236,21 +3269,14 @@ function recordPrDetectAmbiguous(deps, batch, matches, now) {
3236
3269
  at: now.toISOString(),
3237
3270
  since,
3238
3271
  ticks_persisted: ticks,
3239
- // #789 review: neither "record it explicitly" (no command exists) nor
3240
- // "close the extras" (a MERGED PR cannot be closed, so it never leaves
3241
- // `gh pr list --state merged`) was ever an actionable remedy — state
3242
- // what actually resolves it instead. #824 tracks the missing verb.
3243
- 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`,
3272
+ // #789 review: "close the extras" (a MERGED PR cannot be closed, so it
3273
+ // never leaves `gh pr list --state merged`) was never an actionable
3274
+ // remedy — state what actually resolves it instead: the operator names
3275
+ // the batch's own PR with `sched attach-pr` (#824), which holds it to
3276
+ // these same candidate checks.
3277
+ 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 the candidates and record the batch's own with \`sched attach-pr --batch ${batch.id} <pr>\`, or \`sched abandon --batch ${batch.id}\``,
3244
3278
  });
3245
3279
  }
3246
- deps.store.withLock((s) => ({
3247
- state: (0, state_1.patchBatch)(s, batch.id, {
3248
- pr_detect_ambiguous_reason: state_1.PR_DETECT_AMBIGUOUS_REASON,
3249
- pr_detect_ambiguous_since: since,
3250
- pr_detect_ambiguous_ticks: ticks,
3251
- }, now, false),
3252
- result: undefined,
3253
- }));
3254
3280
  }
3255
3281
  /**
3256
3282
  * #686: reconcile a `blocked` batch whose work demonstrably shipped anyway.
@@ -3382,7 +3408,7 @@ function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
3382
3408
  // #789 review: what the lock ACTUALLY applied, not what the outer
3383
3409
  // snapshot predicted — re-derived from `b` (read under the lock) below,
3384
3410
  // since `batch.pr` could in principle have changed between the snapshot
3385
- // above and the lock acquiring (e.g. a future `sched attach-pr`, #824).
3411
+ // above and the lock acquiring (e.g. `sched attach-pr`, #824).
3386
3412
  let effectivePr = null;
3387
3413
  let prWasRecorded = false;
3388
3414
  deps.store.withLock((s) => {
@@ -3480,7 +3506,8 @@ function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
3480
3506
  * `members-closed` branch above) is DEFINITIVELY gone — the path no longer
3481
3507
  * exists on disk, or the pool itself now reports it back as a warm spare —
3482
3508
  * clear the ledger's `worktree`/`pool_claimed` (and, separately,
3483
- * `member_worktree`/`member_pool_claimed`) fields so `sched status`'s
3509
+ * `member_worktree`/`member_pool_claimed`, and — #834 — each live
3510
+ * `member_runs[]` entry's `torn_down`) fields so `sched status`'s
3484
3511
  * `kept-worktree` warning does not persist after the operator's cleanup
3485
3512
  * already happened. Ledger-only, by construction: this function reads
3486
3513
  * `fsExists` and `worktree-pool status --json` (both read-only) and never
@@ -3489,15 +3516,50 @@ function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
3489
3516
  * itself. Bounded the same way the warning's own candidate scan is: only
3490
3517
  * `done` batches carrying a kept worktree are considered, which in practice
3491
3518
  * is a small set.
3519
+ *
3520
+ * #834: `MemberRun.worktree` is a required `string` (unlike
3521
+ * `BatchEntry.worktree: string | null`) — there is no null to clear it to,
3522
+ * so "cleared" for a member run means `torn_down: true`, the same flag
3523
+ * `markTornDown` sets after a real teardown. Set here it means the same
3524
+ * thing evidentially (nothing is holding this path any more), just reached
3525
+ * by definitive evidence instead of by running `teardownParallelRuns`.
3526
+ *
3527
+ * #834 discovery: unlike `worktree`/`member_worktree`, a terminal batch's
3528
+ * live `member_runs[]` entries are ALSO reached by the unconditional
3529
+ * terminal-batch arm in `runBatchTick` (search `TERMINAL_BATCH_STATUSES.has(batch.status)`
3530
+ * above the call site below) — added by #809 for `sched stop`/`abandon`,
3531
+ * but written generically enough that it fires for EVERY terminal batch,
3532
+ * `members-closed`-reconciled ones included, and it runs earlier in the
3533
+ * same tick than this function. It calls `teardownParallelRuns` (an actual
3534
+ * teardown attempt, unlike this read-only reconcile) and — notably — calls
3535
+ * `markTornDown` unconditionally, even when that teardown attempt itself
3536
+ * FAILS (e.g. `isSafeWorktree` rejects the path). So in the normal
3537
+ * scheduler-ticking case this function's own `member_runs[]` branch below
3538
+ * is effectively a backstop, not the primary path: by the time it runs,
3539
+ * `torn_down` is usually already `true` — correctly when the tree really
3540
+ * is gone, but also when a real teardown attempt failed and left the tree
3541
+ * on disk — filed as #855 (out of scope here; #834 only extends the
3542
+ * evidence-based ledger reconcile, it does not change teardown behavior).
3543
+ * This function's own branch still matters for the window before that
3544
+ * safety net's next tick runs, and for `sched status` reads taken while
3545
+ * the scheduler isn't ticking at all.
3546
+ *
3547
+ * Exported (not part of the package's public `index.ts` surface — same
3548
+ * test-only convention as `reconcileStaleBlockedBatches`) so `member_runs[]`
3549
+ * tests can call it directly, isolated from the terminal-batch safety net
3550
+ * described above, which otherwise always beats it to `member_runs[]` when
3551
+ * both run inside one `runBatchTick` call.
3492
3552
  */
3493
3553
  function reconcileKeptWorktrees(deps, now) {
3494
3554
  const state = deps.store.load();
3495
3555
  const fsExists = deps.fsExists ?? ((p) => fs.existsSync(p));
3496
- const candidates = state.batches.filter((b) => b.status === 'done' && (b.worktree !== null || b.member_worktree !== null));
3556
+ const candidates = state.batches.filter((b) => b.status === 'done' &&
3557
+ (b.worktree !== null || b.member_worktree !== null || b.member_runs.some((r) => !r.torn_down)));
3497
3558
  if (candidates.length === 0)
3498
3559
  return;
3499
3560
  const needsPoolCheck = candidates.some((b) => (b.worktree !== null && b.pool_claimed) ||
3500
- (b.member_worktree !== null && b.member_pool_claimed));
3561
+ (b.member_worktree !== null && b.member_pool_claimed) ||
3562
+ b.member_runs.some((r) => !r.torn_down && r.pool_claimed));
3501
3563
  // One pool query per TICK, not per candidate — `worktree-pool status` is a
3502
3564
  // real subprocess (`npx ...`), and this reconcile is meant to be cheap.
3503
3565
  const poolStatusJson = needsPoolCheck
@@ -3525,16 +3587,54 @@ function reconcileKeptWorktrees(deps, now) {
3525
3587
  clears.member_pool_claimed = false;
3526
3588
  clearedFields.push('member_worktree', 'member_pool_claimed');
3527
3589
  }
3528
- if (clearedFields.length === 0)
3590
+ // #834: unlike `worktree`/`member_worktree`, `member_runs[]` is actively
3591
+ // mutated by many OTHER code paths while a tick runs (member dispatch,
3592
+ // eviction, `markTornDown`, `sched attach-pr`, ...), so its evidence
3593
+ // check must NOT be computed from this function's own outer, unlocked
3594
+ // `batch` snapshot: `patchBatch` merges `member_runs` as a whole-array
3595
+ // replacement, not per-entry, so writing an array built from a stale
3596
+ // snapshot would silently discard a concurrent writer's change to a
3597
+ // SIBLING entry in the gap between this snapshot and the lock below
3598
+ // (a lost update — found in review). Only a coarse "is there anything
3599
+ // to look at" pre-check runs out here, to decide whether entering the
3600
+ // lock is worth it at all; the real per-run evidence check re-reads
3601
+ // `member_runs` fresh, from the batch `withLock` reloads below.
3602
+ const mightClearRuns = batch.member_runs.some((r) => !r.torn_down);
3603
+ if (clearedFields.length === 0 && !mightClearRuns)
3529
3604
  continue;
3605
+ const clearedRunIssues = [];
3530
3606
  deps.store.withLock((s) => {
3531
3607
  const b = (0, state_1.findBatch)(s, batch.id);
3532
3608
  // Re-check under the lock: another tick, or an operator command, may
3533
3609
  // have already moved this batch or cleared the fields itself.
3534
3610
  if (!b || b.status !== 'done')
3535
3611
  return { state: s, result: undefined };
3536
- return { state: (0, state_1.patchBatch)(s, batch.id, clears, now, false), result: undefined };
3612
+ const runClears = {};
3613
+ if (mightClearRuns) {
3614
+ const updatedRuns = b.member_runs.map((run) => {
3615
+ if (run.torn_down)
3616
+ return run;
3617
+ if (!goneOrReturned(run.worktree, run.pool_claimed))
3618
+ return run;
3619
+ clearedRunIssues.push(run.issue);
3620
+ return { ...run, torn_down: true };
3621
+ });
3622
+ if (clearedRunIssues.length > 0)
3623
+ runClears.member_runs = updatedRuns;
3624
+ }
3625
+ if (Object.keys(clears).length === 0 && Object.keys(runClears).length === 0) {
3626
+ return { state: s, result: undefined };
3627
+ }
3628
+ return {
3629
+ state: (0, state_1.patchBatch)(s, batch.id, { ...clears, ...runClears }, now, false),
3630
+ result: undefined,
3631
+ };
3537
3632
  });
3633
+ if (clearedRunIssues.length > 0) {
3634
+ clearedFields.push(`member_runs[${clearedRunIssues.join(',')}].torn_down`);
3635
+ }
3636
+ if (clearedFields.length === 0)
3637
+ continue;
3538
3638
  journalEvent(deps, 'kept-worktree-cleared', unit(batch.id), {
3539
3639
  cleared: clearedFields.join(','),
3540
3640
  detail: `kept-worktree ledger field(s) cleared on definitive evidence (${clearedFields.join(', ')}) — path gone or pool reports it returned; no removal command was run`,