@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 +44 -6
- package/dist/attach-pr.d.ts +52 -0
- package/dist/attach-pr.d.ts.map +1 -0
- package/dist/attach-pr.js +168 -0
- package/dist/attach-pr.js.map +1 -0
- package/dist/batch-dispatch.d.ts +58 -0
- package/dist/batch-dispatch.d.ts.map +1 -1
- package/dist/batch-dispatch.js +121 -21
- package/dist/batch-dispatch.js.map +1 -1
- package/dist/groundtruth.d.ts +63 -12
- package/dist/groundtruth.d.ts.map +1 -1
- package/dist/groundtruth.js +104 -35
- package/dist/groundtruth.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/status.d.ts +18 -13
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +37 -11
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +17 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
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
|
|
1137
|
-
// verified `repo`
|
|
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` —
|
|
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
|
|
1561
|
-
|
|
1562
|
-
|
|
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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"}
|
package/dist/batch-dispatch.d.ts
CHANGED
|
@@ -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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"}
|
package/dist/batch-dispatch.js
CHANGED
|
@@ -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
|
-
|
|
3175
|
+
undispatched.push(issue);
|
|
3163
3176
|
}
|
|
3164
|
-
return
|
|
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:
|
|
3240
|
-
//
|
|
3241
|
-
//
|
|
3242
|
-
//
|
|
3243
|
-
|
|
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.
|
|
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
|
|
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' &&
|
|
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
|
-
|
|
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
|
-
|
|
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`,
|