dorfl 0.13.2 → 0.13.4
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/dist/claim-cas.d.ts.map +1 -1
- package/dist/claim-cas.js +39 -0
- package/dist/claim-cas.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +10 -1
- package/dist/cli.js.map +1 -1
- package/dist/complete.d.ts.map +1 -1
- package/dist/complete.js +9 -2
- package/dist/complete.js.map +1 -1
- package/dist/cwd-section.d.ts +40 -0
- package/dist/cwd-section.d.ts.map +1 -1
- package/dist/cwd-section.js +111 -5
- package/dist/cwd-section.js.map +1 -1
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +56 -0
- package/dist/format.js.map +1 -1
- package/dist/frontmatter.d.ts.map +1 -1
- package/dist/frontmatter.js +16 -4
- package/dist/frontmatter.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/item-lock.d.ts +167 -0
- package/dist/item-lock.d.ts.map +1 -1
- package/dist/item-lock.js +255 -1
- package/dist/item-lock.js.map +1 -1
- package/dist/needs-attention.d.ts +216 -1
- package/dist/needs-attention.d.ts.map +1 -1
- package/dist/needs-attention.js +595 -2
- package/dist/needs-attention.js.map +1 -1
- package/dist/reconcile-terminal.d.ts +97 -0
- package/dist/reconcile-terminal.d.ts.map +1 -0
- package/dist/reconcile-terminal.js +88 -0
- package/dist/reconcile-terminal.js.map +1 -0
- package/dist/scan.d.ts +17 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +51 -2
- package/dist/scan.js.map +1 -1
- package/dist/status.d.ts +28 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +79 -2
- package/dist/status.js.map +1 -1
- package/package.json +1 -1
- package/src/claim-cas.ts +40 -0
- package/src/cli.ts +18 -1
- package/src/complete.ts +9 -2
- package/src/cwd-section.ts +157 -4
- package/src/format.ts +72 -0
- package/src/frontmatter.ts +16 -4
- package/src/index.ts +2 -0
- package/src/item-lock.ts +369 -1
- package/src/needs-attention.ts +783 -0
- package/src/reconcile-terminal.ts +180 -0
- package/src/scan.ts +82 -5
- package/src/status.ts +131 -6
package/dist/status.d.ts
CHANGED
|
@@ -83,6 +83,15 @@ export interface RepoLockEntries {
|
|
|
83
83
|
/** The held lock entries (action × state + reason), sorted by entry. */
|
|
84
84
|
entries: LockEntry[];
|
|
85
85
|
}
|
|
86
|
+
/** One repo's STALE per-item locks: entries whose item has already come to rest
|
|
87
|
+
* in a terminal folder on that repo's `main`, so the hold is finished work
|
|
88
|
+
* awaiting release rather than an in-flight claim. */
|
|
89
|
+
export interface RepoStaleLocks {
|
|
90
|
+
/** The repo path whose lock refs were classified (a hub-mirror path). */
|
|
91
|
+
repoPath: string;
|
|
92
|
+
/** The stale lock `<entry>` names, sorted. */
|
|
93
|
+
entries: string[];
|
|
94
|
+
}
|
|
86
95
|
/** One repo's one-slug-one-folder LINT result, surfaced for the dashboard. */
|
|
87
96
|
export interface RepoLedgerDuplicates {
|
|
88
97
|
/** The repo path whose `work/` ledger was linted (a hub-mirror path). */
|
|
@@ -109,6 +118,16 @@ export interface StatusReport {
|
|
|
109
118
|
* populates it (possibly empty).
|
|
110
119
|
*/
|
|
111
120
|
lockHeld?: RepoLockEntries[];
|
|
121
|
+
/**
|
|
122
|
+
* Per registered hub mirror, the lock `<entry>` names whose item is already at
|
|
123
|
+
* REST in a terminal folder on that mirror's `main`, finished work (typically
|
|
124
|
+
* a merged propose PR) whose lock has not been released yet. Reported
|
|
125
|
+
* SEPARATELY from {@link StatusReport.lockHeld}, and deliberately excluded from
|
|
126
|
+
* it, so completed work never reads as in-progress (the symptom of the
|
|
127
|
+
* propose-path lock leak). `status` RELEASES nothing by default, it is
|
|
128
|
+
* read-only; these drain on the next claim, or under `--reconcile-locks`.
|
|
129
|
+
*/
|
|
130
|
+
staleLocks?: RepoStaleLocks[];
|
|
112
131
|
/**
|
|
113
132
|
* The one-slug-one-folder LINT (spec `ledger-integrity` story 3): per registered
|
|
114
133
|
* hub mirror, any slug present in MORE THAN ONE `work/` status folder (a corrupt
|
|
@@ -155,6 +174,15 @@ export interface StatusOptions {
|
|
|
155
174
|
* Omitted ⇒ only the job worktrees are reported.
|
|
156
175
|
*/
|
|
157
176
|
mirrorPaths?: string[];
|
|
177
|
+
/**
|
|
178
|
+
* OPT-IN WRITE (`status --reconcile-locks`). `false`/omitted (the DEFAULT)
|
|
179
|
+
* keeps `status` strictly READ-ONLY: stale locks are classified and REPORTED
|
|
180
|
+
* via {@link StatusReport.staleLocks} and nothing on any arbiter is touched.
|
|
181
|
+
* `true` additionally RELEASES them. Not needed for routine convergence, the
|
|
182
|
+
* claim path already sweeps on every unit of work; this is the manual
|
|
183
|
+
* "drain them now" lever.
|
|
184
|
+
*/
|
|
185
|
+
reconcileLocks?: boolean;
|
|
158
186
|
/**
|
|
159
187
|
* Sink for the fetch-first fall-back warning (ADR §5/§6): when a mirror's `main`
|
|
160
188
|
* cannot be fetched, `status` warns through this and reads that mirror's
|
package/dist/status.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,OAAO,EAAC,MAAM,cAAc,CAAC;AAG1D,OAAO,iBAAiB,CAAC;AACzB,OAAO,EAAC,KAAK,QAAQ,EAAC,MAAM,gBAAgB,CAAC;AAE7C,OAAO,EAAsB,KAAK,mBAAmB,EAAC,MAAM,cAAc,CAAC;AAC3E,OAAO,
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"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"}
|
package/dist/status.js
CHANGED
|
@@ -5,7 +5,7 @@ import { resolveHarness } from './harness.js';
|
|
|
5
5
|
import './pi-harness.js';
|
|
6
6
|
import { fetchMirrorMainOrWarn } from './repo-mirror.js';
|
|
7
7
|
import { formatArbiterStatus } from './arbiter.js';
|
|
8
|
-
import {
|
|
8
|
+
import { classifyTerminalItemLocks, reconcileTerminalItemLocks, } from './item-lock.js';
|
|
9
9
|
import { formatCwdSection, formatLockEntryLines } from './format.js';
|
|
10
10
|
import { lintRefLedger, formatDuplicateWarnings, } from './ledger-lint.js';
|
|
11
11
|
/**
|
|
@@ -42,22 +42,73 @@ export async function status(options) {
|
|
|
42
42
|
// held (`active` = in-progress) AND stuck (`needs-attention`) entries + their
|
|
43
43
|
// reasons/questions.
|
|
44
44
|
const lockHeld = [];
|
|
45
|
+
const staleLocks = [];
|
|
45
46
|
const ledgerDuplicates = [];
|
|
46
47
|
for (const mirrorPath of options.mirrorPaths ?? []) {
|
|
47
48
|
// Fetch-first (ADR §5/§6): refresh this mirror's `main` so the duplicate lint
|
|
48
49
|
// reflects the remote truth. Never fatal — a failed fetch WARNS and falls back
|
|
49
50
|
// to the mirror's last-known `main`.
|
|
50
51
|
fetchMirrorMainOrWarn({ mirrorPath, warn: options.warn, env: options.env });
|
|
52
|
+
// CLASSIFY the mirror's held locks against its `main` (fix for the propose-path
|
|
53
|
+
// lock leak; observation
|
|
54
|
+
// `every-completed-task-leaves-its-lock-ref-reporting-in-progress`). A
|
|
55
|
+
// `complete --propose` KEEPS its lock held across the open PR and defers the
|
|
56
|
+
// release to the merge, an event no dorfl process is present for (no hook, no
|
|
57
|
+
// daemon), so the release never fired and every completed item reported
|
|
58
|
+
// in-progress for ever.
|
|
59
|
+
//
|
|
60
|
+
// `status` is READ-ONLY and stays that way: by DEFAULT we only classify, so a
|
|
61
|
+
// lock whose item has come to REST in a terminal folder on the mirror's `main`
|
|
62
|
+
// is reported as STALE rather than listed in-flight. The refs themselves are
|
|
63
|
+
// drained by the paths that already write on every unit of work (the claim
|
|
64
|
+
// path), or here under the explicit `--reconcile-locks` opt-in. An item on an
|
|
65
|
+
// OPEN PR (still in the pool on `main`) stays in-flight either way.
|
|
66
|
+
// Best-effort and never throws, any fault treats the lock as HELD.
|
|
67
|
+
//
|
|
68
|
+
// A hub mirror is a BARE clone with no `refs/remotes/*` namespace, so its copy
|
|
69
|
+
// of the arbiter's main is the plain `main` ref (the SAME ref
|
|
70
|
+
// `fetchMirrorMainOrWarn` above and `lintRefLedger` below read). Passing the
|
|
71
|
+
// default `origin/main` here would fail EVERY probe with `invalid object name`
|
|
72
|
+
// and silently classify every lock as in-flight.
|
|
73
|
+
const MIRROR_MAIN = { mainRef: 'main' };
|
|
74
|
+
let staleEntries = [];
|
|
75
|
+
let entries;
|
|
76
|
+
if (options.reconcileLocks === true) {
|
|
77
|
+
const reconciled = await reconcileTerminalItemLocks(mirrorPath, 'origin', options.env, MIRROR_MAIN);
|
|
78
|
+
if (reconciled.released.length > 0) {
|
|
79
|
+
options.warn?.(`${mirrorPath}: released ${reconciled.released.length} stale per-item ` +
|
|
80
|
+
'lock(s) whose item is terminal on main (work landed; the ' +
|
|
81
|
+
`propose PR merged out-of-band): ${reconciled.released.join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
// Under an EXPLICIT drain request a refusal must be reported: the operator
|
|
84
|
+
// asked for these to go away, so silence would be a lie.
|
|
85
|
+
for (const e of reconciled.errors) {
|
|
86
|
+
options.warn?.(`${mirrorPath}: could not release '${e.entry}': ${e.message}`);
|
|
87
|
+
}
|
|
88
|
+
entries = reconciled.stillHeld;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const classified = await classifyTerminalItemLocks(mirrorPath, 'origin', options.env, MIRROR_MAIN);
|
|
92
|
+
staleEntries = classified.terminal.map((l) => l.entry);
|
|
93
|
+
entries = classified.inFlight;
|
|
94
|
+
}
|
|
51
95
|
// The PER-ITEM LOCK in-flight view (spec US #8): read the mirror's lock refs to
|
|
52
96
|
// surface held (`active` = in-progress) and stuck (`needs-attention`) entries +
|
|
53
97
|
// reasons/questions. A bare hub mirror's arbiter is its `origin` (the SAME
|
|
54
98
|
// handle the `scan` held-slug subtraction reads). Best-effort: a fetch/read
|
|
55
99
|
// fault yields an EMPTY list (see {@link listItemLockEntries}), so this
|
|
56
100
|
// read-only view degrades to "no in-flight locks" rather than erroring.
|
|
57
|
-
|
|
101
|
+
// `entries` comes STRAIGHT from the classification above (its `inFlight`
|
|
102
|
+
// partition, or `stillHeld` after a sweep), so the stale ones are already
|
|
103
|
+
// excluded: finished work is not an in-flight hold, and listing it as one is
|
|
104
|
+
// the exact symptom being fixed. Reusing that result also avoids a SECOND
|
|
105
|
+
// `ls-remote` + fetch per mirror, and the TOCTOU window between two reads.
|
|
58
106
|
if (entries.length > 0) {
|
|
59
107
|
lockHeld.push({ repoPath: mirrorPath, entries });
|
|
60
108
|
}
|
|
109
|
+
if (staleEntries.length > 0) {
|
|
110
|
+
staleLocks.push({ repoPath: mirrorPath, entries: staleEntries });
|
|
111
|
+
}
|
|
61
112
|
// The one-slug-one-folder LINT (spec story 3): derive any slug residing in >1
|
|
62
113
|
// status folder from the SAME freshly-fetched `main` ref, surfaced LOUDLY.
|
|
63
114
|
const dups = lintRefLedger('main', mirrorPath, options.env);
|
|
@@ -66,11 +117,13 @@ export async function status(options) {
|
|
|
66
117
|
}
|
|
67
118
|
}
|
|
68
119
|
lockHeld.sort((a, b) => a.repoPath.localeCompare(b.repoPath));
|
|
120
|
+
staleLocks.sort((a, b) => a.repoPath.localeCompare(b.repoPath));
|
|
69
121
|
ledgerDuplicates.sort((a, b) => a.repoPath.localeCompare(b.repoPath));
|
|
70
122
|
return {
|
|
71
123
|
active,
|
|
72
124
|
attention,
|
|
73
125
|
lockHeld,
|
|
126
|
+
staleLocks,
|
|
74
127
|
ledgerDuplicates,
|
|
75
128
|
...(options.arbiter ? { arbiter: options.arbiter } : {}),
|
|
76
129
|
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
@@ -120,11 +173,17 @@ export function formatStatus(report) {
|
|
|
120
173
|
const cwdLines = report.cwd !== undefined ? formatCwdSection(report.cwd) : [];
|
|
121
174
|
const lockHeld = report.lockHeld ?? [];
|
|
122
175
|
const lockCount = lockHeld.reduce((sum, r) => sum + r.entries.length, 0);
|
|
176
|
+
const staleLocks = report.staleLocks ?? [];
|
|
177
|
+
const staleCount = staleLocks.reduce((sum, r) => sum + r.entries.length, 0);
|
|
123
178
|
const ledgerDuplicates = report.ledgerDuplicates ?? [];
|
|
124
179
|
const dupCount = ledgerDuplicates.reduce((sum, r) => sum + r.duplicates.length, 0);
|
|
125
180
|
if (report.active.length === 0 &&
|
|
126
181
|
report.attention.length === 0 &&
|
|
127
182
|
lockCount === 0 &&
|
|
183
|
+
// A repo whose ONLY locks are stale must not print "the work area is empty":
|
|
184
|
+
// that would turn the old "wrongly shown as in-progress" bug into a worse
|
|
185
|
+
// "not shown at all" one.
|
|
186
|
+
staleCount === 0 &&
|
|
128
187
|
dupCount === 0 &&
|
|
129
188
|
report.arbiter === undefined &&
|
|
130
189
|
cwdLines.length === 0) {
|
|
@@ -170,6 +229,24 @@ export function formatStatus(report) {
|
|
|
170
229
|
}
|
|
171
230
|
}
|
|
172
231
|
}
|
|
232
|
+
// STALE locks: finished work whose lock has not been released yet (the
|
|
233
|
+
// propose-path lock leak). Rendered as its OWN block and deliberately NOT under
|
|
234
|
+
// "In-flight locks" above, because reporting completed work as in-progress for
|
|
235
|
+
// ever was the symptom. `status` is read-only, so it names the state and how it
|
|
236
|
+
// clears rather than clearing it.
|
|
237
|
+
if (staleCount > 0) {
|
|
238
|
+
lines.push('');
|
|
239
|
+
lines.push(`Completed, lock not yet released (${staleCount}; the item is at rest on ` +
|
|
240
|
+
'main, so this is NOT in flight):');
|
|
241
|
+
for (const repo of staleLocks) {
|
|
242
|
+
lines.push(` ${repo.repoPath}`);
|
|
243
|
+
for (const entry of repo.entries) {
|
|
244
|
+
lines.push(` ${entry}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
lines.push(' These clear automatically on the next claim. To drain them now: ' +
|
|
248
|
+
'`dorfl status --reconcile-locks`.');
|
|
249
|
+
}
|
|
173
250
|
// The one-slug-one-folder LINT (spec `ledger-integrity` story 3): WARN LOUDLY
|
|
174
251
|
// about every slug residing in >1 status folder of a registered mirror's ledger
|
|
175
252
|
// (a corrupt ledger — never a silent pass). A human must resolve each.
|
package/dist/status.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.js","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAa,MAAM,SAAS,CAAC;AACjD,OAAO,EAAC,cAAc,EAAe,MAAM,cAAc,CAAC;AAC1D,8EAA8E;AAC9E,2EAA2E;AAC3E,OAAO,iBAAiB,CAAC;AAEzB,OAAO,EAAC,qBAAqB,EAAC,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAC,mBAAmB,EAA2B,MAAM,cAAc,CAAC;AAC3E,OAAO,
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/claim-cas.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {runAsync, type RunResult} from './git.js';
|
|
2
2
|
import {acquireItemLock, releaseItemLock, heldTaskSlugs} from './item-lock.js';
|
|
3
|
+
import {reconcileTerminalState} from './reconcile-terminal.js';
|
|
3
4
|
import {extractPromptSection} from './prompt.js';
|
|
4
5
|
import {resolveReadiness} from './readiness.js';
|
|
5
6
|
import {workBranchRef} from './slug-namespace.js';
|
|
@@ -386,6 +387,45 @@ async function runClaim(
|
|
|
386
387
|
}
|
|
387
388
|
}
|
|
388
389
|
|
|
390
|
+
// LAZY RECONCILIATION of TERMINAL-STATE RESIDUE: the stale per-item lock a
|
|
391
|
+
// propose PR leaves behind (observation
|
|
392
|
+
// `every-completed-task-leaves-its-lock-ref-reporting-in-progress`) AND the
|
|
393
|
+
// stranded sidecar + `needsAnswers` flag a bounced-then-rebuilt item leaves
|
|
394
|
+
// behind (observation
|
|
395
|
+
// `a-rebuilt-task-leaves-its-bounce-question-asking-to-cancel-a-merged-task`).
|
|
396
|
+
// ONE pass, because they are the SAME defect wearing two hats: both are cleared
|
|
397
|
+
// by a step that only runs on a path the item did not take, both become moot at
|
|
398
|
+
// exactly the same moment (the done-move landing on `main`), and both are
|
|
399
|
+
// otherwise detectable only from a loop the manual path never enters.
|
|
400
|
+
//
|
|
401
|
+
// `complete --propose` deliberately KEEPS its per-item lock held across the open
|
|
402
|
+
// PR and promises "It is released when the PR merges (reconciled against main)".
|
|
403
|
+
// That release could never fire: no dorfl process runs when a human clicks merge
|
|
404
|
+
// on GitHub, there is no merge hook, and there is no daemon, so every completed
|
|
405
|
+
// propose item leaked its lock ref permanently.
|
|
406
|
+
//
|
|
407
|
+
// THIS is where the release belongs, and why it is here rather than on `status`:
|
|
408
|
+
// the merge EVENT is unobservable, but its CONSEQUENCE on `main` is durable, so
|
|
409
|
+
// any later sweep converges just as well as a timely one. `claim` is the natural
|
|
410
|
+
// host, it ALREADY writes to the arbiter (it is about to acquire a lock), it
|
|
411
|
+
// ALREADY fetched `main` just above, and it runs on EVERY unit of work, so the
|
|
412
|
+
// leaked set drains continuously without a human ever being asked to run a
|
|
413
|
+
// clean-up verb. Keeping the release on a write path is what makes the fix real:
|
|
414
|
+
// `gc --ledger` had been reporting these very locks and printing the
|
|
415
|
+
// `release-lock` command all along, and that offer is precisely what nobody was
|
|
416
|
+
// routed to. The read-only surfaces (`status`, `scan`) therefore only CLASSIFY.
|
|
417
|
+
//
|
|
418
|
+
// It acts EXACTLY on items that have come to REST in a terminal folder on
|
|
419
|
+
// `<arbiter>/main`; an item on an OPEN PR still shows its body in the pool and
|
|
420
|
+
// keeps both its lock and its question state, as does a genuinely stuck one.
|
|
421
|
+
// Clearing a live `needsAnswers` would be as bad as releasing a live lock: it
|
|
422
|
+
// would disarm a gate and hand gated work to agents. Best-effort and never throws:
|
|
423
|
+
// any fault leaves every lock HELD (the safe direction) and never fails the
|
|
424
|
+
// claim, which is unrelated work, this sweep is opportunistic hygiene, not a
|
|
425
|
+
// precondition. It also cannot affect THIS claim's outcome: an item terminal on
|
|
426
|
+
// `main` is not in the claimable pool anyway.
|
|
427
|
+
await reconcileTerminalState({cwd, arbiter, env});
|
|
428
|
+
|
|
389
429
|
// Acquire the lock FIRST. A lock `lost` (someone already holds this SAME item's
|
|
390
430
|
// lock) makes claim lose DEFINITIVELY — exit 2, NO retry budget (a per-item ref
|
|
391
431
|
// never falsely contends, so a rejection is a GENUINE same-item conflict the
|
package/src/cli.ts
CHANGED
|
@@ -167,6 +167,7 @@ interface ScanFlags {
|
|
|
167
167
|
autoBuild?: boolean;
|
|
168
168
|
json?: boolean;
|
|
169
169
|
here?: boolean;
|
|
170
|
+
reconcileLocks?: boolean;
|
|
170
171
|
arbiterRemote?: string;
|
|
171
172
|
arbiter?: string;
|
|
172
173
|
}
|
|
@@ -1029,6 +1030,7 @@ interface StatusFlags {
|
|
|
1029
1030
|
arbiter?: string;
|
|
1030
1031
|
noArbiter?: boolean;
|
|
1031
1032
|
here?: boolean;
|
|
1033
|
+
reconcileLocks?: boolean;
|
|
1032
1034
|
json?: boolean;
|
|
1033
1035
|
}
|
|
1034
1036
|
|
|
@@ -1385,6 +1387,10 @@ export function buildProgram(): Command {
|
|
|
1385
1387
|
'--here',
|
|
1386
1388
|
'report ONLY the current repo (the cwd working tree, fetch-first): skip the cross-repo registry loop entirely. The fast, focused path — no N-mirror fetches.',
|
|
1387
1389
|
)
|
|
1390
|
+
.option(
|
|
1391
|
+
'--reconcile-locks',
|
|
1392
|
+
'WRITE: release the per-item locks whose item is already at rest in a terminal folder on the arbiter’s main (e.g. a merged propose PR left its lock behind). Without this flag `scan` is strictly read-only. Not normally needed, the claim path releases them automatically on every unit of work. An item that is NOT terminal on main (a live build, an open PR, a stuck item) is NEVER released, with or without the flag.',
|
|
1393
|
+
)
|
|
1388
1394
|
.option('--json', 'output the raw report as JSON')
|
|
1389
1395
|
.action(async (flags: ScanFlags, command: Commander) => {
|
|
1390
1396
|
const fileConfig = loadConfig(flags.config);
|
|
@@ -1403,6 +1409,7 @@ export function buildProgram(): Command {
|
|
|
1403
1409
|
override,
|
|
1404
1410
|
arbiterRemote: flags.arbiterRemote,
|
|
1405
1411
|
lockArbiterRemote: flags.arbiter ?? 'origin',
|
|
1412
|
+
reconcileLocks: flags.reconcileLocks === true,
|
|
1406
1413
|
warn,
|
|
1407
1414
|
});
|
|
1408
1415
|
// `--here`: report ONLY the cwd — skip the registry loop ENTIRELY (the fast,
|
|
@@ -1425,7 +1432,11 @@ export function buildProgram(): Command {
|
|
|
1425
1432
|
}
|
|
1426
1433
|
return;
|
|
1427
1434
|
}
|
|
1428
|
-
const report = await scan(config, {
|
|
1435
|
+
const report = await scan(config, {
|
|
1436
|
+
warn,
|
|
1437
|
+
override,
|
|
1438
|
+
reconcileLocks: flags.reconcileLocks === true,
|
|
1439
|
+
});
|
|
1429
1440
|
// The cwd-local section: resolve it ONLY when a participating cwd is NOT
|
|
1430
1441
|
// already covered by a registered mirror. A FETCH-FREE pre-check
|
|
1431
1442
|
// (`cwdSectionDisposition`) decides this with zero network I/O; an
|
|
@@ -3908,6 +3919,10 @@ export function buildProgram(): Command {
|
|
|
3908
3919
|
'--here',
|
|
3909
3920
|
'report ONLY the current repo (the cwd working tree, fetch-first): skip the jobs, registry-mirror, and arbiter sections entirely. "This repo, nothing else" — the fast, focused path.',
|
|
3910
3921
|
)
|
|
3922
|
+
.option(
|
|
3923
|
+
'--reconcile-locks',
|
|
3924
|
+
'WRITE: release the per-item locks reported as "Completed, lock not yet released" (item already at rest in a terminal folder on the arbiter’s main, e.g. a merged propose PR). Without this flag `status` is strictly read-only and only REPORTS them. Not normally needed, the claim path releases them automatically on every unit of work; this is the manual “drain them now” lever. An item that is NOT terminal on main (a live build, an open PR, a stuck item) is NEVER released, with or without the flag.',
|
|
3925
|
+
)
|
|
3911
3926
|
.option('--json', 'output the raw report as JSON')
|
|
3912
3927
|
.action(async (flags: StatusFlags) => {
|
|
3913
3928
|
const config = resolveGlobalConfig(loadConfig(flags.config), {});
|
|
@@ -3923,6 +3938,7 @@ export function buildProgram(): Command {
|
|
|
3923
3938
|
override,
|
|
3924
3939
|
arbiterRemote: flags.arbiterRemote ?? DEFAULT_ARBITER_REMOTE,
|
|
3925
3940
|
lockArbiterRemote: flags.arbiter ?? 'origin',
|
|
3941
|
+
reconcileLocks: flags.reconcileLocks === true,
|
|
3926
3942
|
warn,
|
|
3927
3943
|
});
|
|
3928
3944
|
// `--here`: report ONLY the cwd — skip the jobs, registry-mirror, and arbiter
|
|
@@ -3976,6 +3992,7 @@ export function buildProgram(): Command {
|
|
|
3976
3992
|
mirrorPaths,
|
|
3977
3993
|
arbiter,
|
|
3978
3994
|
cwd: cwdSection,
|
|
3995
|
+
reconcileLocks: flags.reconcileLocks === true,
|
|
3979
3996
|
warn,
|
|
3980
3997
|
});
|
|
3981
3998
|
if (flags.json) {
|
package/src/complete.ts
CHANGED
|
@@ -1260,10 +1260,17 @@ async function releaseClaimLockAfterDurableMove(
|
|
|
1260
1260
|
if (!durablyOnMain) {
|
|
1261
1261
|
// PROPOSE, PR open: the done-move is on the PR branch, not on `main`. KEEP the
|
|
1262
1262
|
// lock held so the in-flight task is not re-claimed during the review window.
|
|
1263
|
+
// The wording used to promise "It is released when the PR merges (reconciled
|
|
1264
|
+
// against main)", which was FALSE: no dorfl process is present at merge time
|
|
1265
|
+
// (no hook, no daemon), so nothing could act on that event and every propose
|
|
1266
|
+
// build leaked its lock ref for ever. The release actually happens on the next
|
|
1267
|
+
// command that writes to the arbiter (the claim path sweeps stale terminal
|
|
1268
|
+
// locks), so the message now names a trigger that really fires.
|
|
1263
1269
|
note(
|
|
1264
1270
|
`'${slug}': keeping the per-item lock HELD (propose PR open; the work is ` +
|
|
1265
|
-
'not yet on main).
|
|
1266
|
-
'
|
|
1271
|
+
'not yet on main). Once the PR merges, the item comes to rest on main and ' +
|
|
1272
|
+
'the lock is released by the next claim (or now, with `dorfl status ' +
|
|
1273
|
+
'--reconcile-locks`).',
|
|
1267
1274
|
);
|
|
1268
1275
|
return;
|
|
1269
1276
|
}
|
package/src/cwd-section.ts
CHANGED
|
@@ -8,7 +8,14 @@ import {
|
|
|
8
8
|
heldTaskSlugsStrict,
|
|
9
9
|
heldSpecSlugsStrict,
|
|
10
10
|
listItemLockEntries,
|
|
11
|
+
classifyTerminalItemLocks,
|
|
12
|
+
reconcileTerminalItemLocks,
|
|
13
|
+
type LockEntry,
|
|
11
14
|
} from './item-lock.js';
|
|
15
|
+
import {
|
|
16
|
+
classifyTerminalQuestionResidue,
|
|
17
|
+
reconcileTerminalQuestionResidue,
|
|
18
|
+
} from './needs-attention.js';
|
|
12
19
|
import type {Config} from './config.js';
|
|
13
20
|
import type {ConfigOverrideMap} from './config-override.js';
|
|
14
21
|
|
|
@@ -105,6 +112,36 @@ export interface CwdSection {
|
|
|
105
112
|
registeredMirrorPath?: string;
|
|
106
113
|
/** The cwd repo's arbiter + divergence (fetch-first). Absent when no arbiter. */
|
|
107
114
|
arbiter?: CwdArbiter;
|
|
115
|
+
/**
|
|
116
|
+
* Lock `<entry>` names whose item is already at REST in a terminal folder on
|
|
117
|
+
* `<arbiter>/main`, finished work (typically a merged propose PR) whose lock
|
|
118
|
+
* has not been released yet. These are STALE, not in flight, and are reported
|
|
119
|
+
* separately from {@link RepoReport.lockHeld} so completed work never reads as
|
|
120
|
+
* in-progress. Purely a CLASSIFICATION: `status` releases nothing (it is
|
|
121
|
+
* read-only). They are released automatically by the next claim, or on demand
|
|
122
|
+
* via `--reconcile-locks` / `dorfl gc --ledger --reap-stale-locks`.
|
|
123
|
+
*/
|
|
124
|
+
staleLocks?: string[];
|
|
125
|
+
/**
|
|
126
|
+
* Lock `<entry>` names this call actually RELEASED. Populated ONLY under the
|
|
127
|
+
* explicit `reconcileLocks` opt-in (`status --reconcile-locks`), which is the
|
|
128
|
+
* one way this otherwise read-only resolver writes to the arbiter. Empty/absent
|
|
129
|
+
* ⇒ nothing was written.
|
|
130
|
+
*/
|
|
131
|
+
reconciledLocks?: string[];
|
|
132
|
+
/**
|
|
133
|
+
* Items resting in a TERMINAL folder on `main` that still carry question state
|
|
134
|
+
* (a stale bounce sidecar, and often a stranded `needsAnswers:true` gate over
|
|
135
|
+
* shipped work). Reported so the residue is visible; `status` clears nothing.
|
|
136
|
+
* These drain on the next claim.
|
|
137
|
+
*/
|
|
138
|
+
staleQuestions?: string[];
|
|
139
|
+
/**
|
|
140
|
+
* Terminal items whose sidecar carries a human's ANSWER that was never applied.
|
|
141
|
+
* Deliberately NEVER auto-drained (the answer is prose the tool did not
|
|
142
|
+
* author), so it is surfaced for a human instead.
|
|
143
|
+
*/
|
|
144
|
+
unappliedAnswers?: string[];
|
|
108
145
|
}
|
|
109
146
|
|
|
110
147
|
/** True iff the working repo has a git remote with name `remote`. The held-lock
|
|
@@ -204,6 +241,16 @@ export interface ResolveCwdSectionOptions {
|
|
|
204
241
|
* arbiter even when the divergence remote is absent. Default `origin`.
|
|
205
242
|
*/
|
|
206
243
|
lockArbiterRemote?: string;
|
|
244
|
+
/**
|
|
245
|
+
* OPT-IN WRITE (`status --reconcile-locks`). `false`/omitted (the DEFAULT)
|
|
246
|
+
* keeps this resolver strictly READ-ONLY: stale locks are classified and
|
|
247
|
+
* REPORTED via {@link CwdSection.staleLocks}, and nothing on the arbiter is
|
|
248
|
+
* touched. `true` additionally RELEASES the locks whose item is terminal on
|
|
249
|
+
* `<arbiter>/main`, reporting them via {@link CwdSection.reconciledLocks}.
|
|
250
|
+
* Not needed for routine convergence, the claim path already sweeps on every
|
|
251
|
+
* unit of work; this is the manual "drain them now" lever.
|
|
252
|
+
*/
|
|
253
|
+
reconcileLocks?: boolean;
|
|
207
254
|
/** Sink for the fetch-first fall-back warning (warn + last-known, never error). */
|
|
208
255
|
warn?: (message: string) => void;
|
|
209
256
|
env?: NodeJS.ProcessEnv;
|
|
@@ -261,6 +308,102 @@ export async function resolveCwdSection(
|
|
|
261
308
|
// nothing to fail against — it keeps the empty set.
|
|
262
309
|
const lockRemote = options.lockArbiterRemote ?? 'origin';
|
|
263
310
|
const hasLockRemote = remoteExists(cwd, lockRemote, env);
|
|
311
|
+
|
|
312
|
+
// 2a. CLASSIFY the held locks against `main` (fix for the propose-path lock
|
|
313
|
+
// leak; observation
|
|
314
|
+
// `every-completed-task-leaves-its-lock-ref-reporting-in-progress`).
|
|
315
|
+
// `complete --propose` deliberately keeps the lock HELD across the open PR
|
|
316
|
+
// and promises it is "released when the PR merges (reconciled against
|
|
317
|
+
// main)", but NO dorfl process runs when a human clicks merge on GitHub,
|
|
318
|
+
// and there is no daemon, so that release could never fire. Every completed
|
|
319
|
+
// propose item therefore kept its lock ref and was reported in-progress for
|
|
320
|
+
// ever.
|
|
321
|
+
//
|
|
322
|
+
// `status` is READ-ONLY and stays that way: by DEFAULT this only
|
|
323
|
+
// CLASSIFIES, splitting the held locks into genuinely in-flight ones and
|
|
324
|
+
// ones whose item has come to REST in a terminal folder on
|
|
325
|
+
// `<arbiter>/main`. That alone fixes the MIS-REPORTING half of the bug
|
|
326
|
+
// finished work stops reading as in-progress, without this command
|
|
327
|
+
// mutating the arbiter. The REF is drained by the paths that already write
|
|
328
|
+
// on every unit of work (the claim path), or on demand here via the
|
|
329
|
+
// explicit `reconcileLocks` opt-in below.
|
|
330
|
+
//
|
|
331
|
+
// The terminal test is the item's POSITION ON MAIN, never a branch or a
|
|
332
|
+
// PR, so an item on an OPEN PR (body still in the pool on `main`) stays
|
|
333
|
+
// in-flight, as does a genuinely stuck one. Ordered BEFORE the held-slug
|
|
334
|
+
// reads below so, under the opt-in, the subtraction and the in-flight
|
|
335
|
+
// surface both see the reconciled truth in the SAME run.
|
|
336
|
+
//
|
|
337
|
+
// Best-effort by construction: neither call throws, and every uncertainty
|
|
338
|
+
// resolves to KEEP, so an offline or push-denied checkout degrades to the
|
|
339
|
+
// previous behaviour.
|
|
340
|
+
//
|
|
341
|
+
// The cwd is a WORKING CLONE, so the arbiter's main is the remote-tracking
|
|
342
|
+
// ref `<lockRemote>/main`, which is the default. (A bare hub mirror would
|
|
343
|
+
// need `mainRef: 'main'`; see the mirror callers in `status`/`scan`.)
|
|
344
|
+
let staleLocks: string[] = [];
|
|
345
|
+
let reconciledLocks: string[] = [];
|
|
346
|
+
// The in-flight entries, taken straight from the classification so the surface
|
|
347
|
+
// below needs no second `ls-remote` + fetch. `undefined` = not read at all.
|
|
348
|
+
let inFlightLocks: LockEntry[] | undefined;
|
|
349
|
+
if (hasLockRemote) {
|
|
350
|
+
if (options.reconcileLocks === true) {
|
|
351
|
+
// The ONLY write this resolver performs, and only when explicitly asked.
|
|
352
|
+
const swept = await reconcileTerminalItemLocks(cwd, lockRemote, env);
|
|
353
|
+
reconciledLocks = swept.released;
|
|
354
|
+
inFlightLocks = swept.stillHeld;
|
|
355
|
+
// Under an EXPLICIT drain request a refusal must be reported: the operator
|
|
356
|
+
// asked for these to go away, so silence would be a lie. (On the claim
|
|
357
|
+
// path the same errors are ignored on purpose: there the sweep is
|
|
358
|
+
// opportunistic hygiene, not something the operator asked for.)
|
|
359
|
+
for (const e of swept.errors) {
|
|
360
|
+
options.warn?.(`could not release lock '${e.entry}': ${e.message}`);
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
const classified = await classifyTerminalItemLocks(cwd, lockRemote, env);
|
|
364
|
+
staleLocks = classified.terminal.map((l) => l.entry);
|
|
365
|
+
inFlightLocks = classified.inFlight;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// 2b. The OTHER half of the same terminal residue: a bounced-then-rebuilt item
|
|
370
|
+
// whose sidecar + `needsAnswers` flag were never cleared (observation
|
|
371
|
+
// `a-rebuilt-task-leaves-its-bounce-question-asking-to-cancel-a-merged-task`).
|
|
372
|
+
// READ-ONLY here, exactly like the lock half: `status` reports it and the
|
|
373
|
+
// claim path drains it. Best-effort; a fault yields an empty report.
|
|
374
|
+
let staleQuestions: string[] = [];
|
|
375
|
+
let unappliedAnswers: string[] = [];
|
|
376
|
+
if (hasLockRemote && options.reconcileLocks !== true) {
|
|
377
|
+
const residue = await classifyTerminalQuestionResidue({
|
|
378
|
+
cwd,
|
|
379
|
+
arbiter: lockRemote,
|
|
380
|
+
mainRef: `${lockRemote}/main`,
|
|
381
|
+
env,
|
|
382
|
+
});
|
|
383
|
+
// BOTH halves of the residue are reported under one heading: a stale sidecar
|
|
384
|
+
// (with or without its flag) and an armed gate with no sidecar left are the
|
|
385
|
+
// same defect seen from either side, and both drain on the same pass.
|
|
386
|
+
staleQuestions = [
|
|
387
|
+
...residue.drainable.map((r) => r.item),
|
|
388
|
+
...residue.staleFlags.map((r) => r.item),
|
|
389
|
+
];
|
|
390
|
+
unappliedAnswers = residue.answeredHeld.map((r) => r.item);
|
|
391
|
+
} else if (hasLockRemote) {
|
|
392
|
+
// Under the explicit opt-in the question residue is DRAINED alongside the
|
|
393
|
+
// locks, through the one shared pass.
|
|
394
|
+
const drained = await reconcileTerminalQuestionResidue({
|
|
395
|
+
cwd,
|
|
396
|
+
arbiter: lockRemote,
|
|
397
|
+
mainRef: `${lockRemote}/main`,
|
|
398
|
+
env,
|
|
399
|
+
note: options.warn,
|
|
400
|
+
});
|
|
401
|
+
unappliedAnswers = drained.answeredHeld;
|
|
402
|
+
for (const e of drained.errors) {
|
|
403
|
+
options.warn?.(`could not drain questions for '${e.item}': ${e.message}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
264
407
|
const heldSlugs = hasLockRemote
|
|
265
408
|
? await heldTaskSlugsStrict(cwd, lockRemote, env)
|
|
266
409
|
: new Set<string>();
|
|
@@ -307,10 +450,16 @@ export async function resolveCwdSection(
|
|
|
307
450
|
// different fault models (selection fails closed; the surface degrades).
|
|
308
451
|
// Read from the SAME coordination remote the subtraction used. A repo with no
|
|
309
452
|
// coordination remote has no lock refs to read — empty surface.
|
|
310
|
-
if (repo !== undefined &&
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
453
|
+
if (repo !== undefined && inFlightLocks !== undefined) {
|
|
454
|
+
// Taken STRAIGHT from the classification in 2a, so the stale ones are already
|
|
455
|
+
// excluded. This is the reporting half of the propose-lock-leak fix: a lock
|
|
456
|
+
// whose item is already at REST on `main` is finished work, NOT an in-flight
|
|
457
|
+
// hold, so it must not appear under "In progress" (the symptom being fixed:
|
|
458
|
+
// completed tasks reported in-progress for ever). It is surfaced under
|
|
459
|
+
// `staleLocks` instead. Reusing that result also avoids a second `ls-remote` +
|
|
460
|
+
// fetch and the TOCTOU window between two separate reads.
|
|
461
|
+
if (inFlightLocks.length > 0) {
|
|
462
|
+
repo.lockHeld = inFlightLocks;
|
|
314
463
|
}
|
|
315
464
|
}
|
|
316
465
|
|
|
@@ -326,6 +475,10 @@ export async function resolveCwdSection(
|
|
|
326
475
|
path: cwd,
|
|
327
476
|
participating: true,
|
|
328
477
|
repo,
|
|
478
|
+
staleLocks,
|
|
479
|
+
reconciledLocks,
|
|
480
|
+
staleQuestions,
|
|
481
|
+
unappliedAnswers,
|
|
329
482
|
totalItems: localReport.totalItems,
|
|
330
483
|
totalEligible: localReport.totalEligible,
|
|
331
484
|
alsoRegistered,
|