← back to Ticket System
dtd-run: concurrency guard (refuse to clobber a live sweep) + --merge (overlay subset verdicts) + --cap/--chunk flags; fixes the board-button-vs-CLI race that corrupted dtd-verdicts.json
1f89978333a9c0b7179fdbc095f3863555cb86a7 · 2026-08-13 10:07:27 -0700 · Steve Abrams
Files touched
Diff
commit 1f89978333a9c0b7179fdbc095f3863555cb86a7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 13 10:07:27 2026 -0700
dtd-run: concurrency guard (refuse to clobber a live sweep) + --merge (overlay subset verdicts) + --cap/--chunk flags; fixes the board-button-vs-CLI race that corrupted dtd-verdicts.json
---
dtd-run.js | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/dtd-run.js b/dtd-run.js
index 739dcc8f..88fc3984 100755
--- a/dtd-run.js
+++ b/dtd-run.js
@@ -33,6 +33,7 @@ const flag = f => { const i = argv.indexOf(f); if (i === -1) return false; argv.
const opt = f => { const i = argv.indexOf(f); if (i === -1) return undefined; const v = argv[i + 1]; argv.splice(i, 2); return v; };
const RECENT = flag('--recent');
const ANNOTATE = flag('--annotate');
+const MERGE = flag('--merge'); // overlay verdicts onto the existing file (for re-scoring a subset)
const DAYS = parseInt(opt('--days') || '14', 10);
const CAP = parseInt(opt('--cap') || '40', 10); // max tickets scored per sweep (bump to score all)
const CHUNK = parseInt(opt('--chunk') || '33', 10); // tickets per panel.sh call (keeps local-model JSON un-truncated)
@@ -132,6 +133,20 @@ ${digest}`;
return { dir, verdicts };
}
+// ── concurrency guard: never clobber a live run (fixed 2026-08-13 after a board-
+// triggered detached run raced a CLI run on the single unlocked verdicts file and
+// corrupted it to 1 ticket). If a RUNNING marker is FRESH (<8min; panel watchdog
+// is 5.5min so a real run finishes inside that), another dtd-run is active — abort
+// rather than overwrite. A stale marker (>8min = crashed run) is ignored. --force
+// overrides. NEVER `rm -f` the marker by hand; that's what defeated the guard. ──
+if (!flag('--force')) {
+ try {
+ const st = fs.statSync(RUNNING);
+ const ageMin = (Date.now() - st.mtimeMs) / 60000;
+ if (ageMin < 8) { console.error(`dtd-run: another sweep is active (marker ${ageMin.toFixed(1)}m old) — aborting to avoid clobbering. Wait or pass --force.`); process.exit(3); }
+ } catch { /* no marker → clear to run */ }
+}
+
// ── run in panel-sized chunks so a large set doesn't truncate local-model JSON ──
const chunks = [];
for (let i = 0; i < chosen.length; i += CHUNK) chunks.push(chosen.slice(i, i + CHUNK));
@@ -154,6 +169,18 @@ try {
console.error('dtd-run: panel.sh failed:', e.message); process.exit(1);
}
+// --merge: overlay these (deeper) verdicts onto the existing file instead of
+// replacing it — so re-scoring a SUBSET (e.g. just the YES candidates at smaller
+// chunks) keeps every other ticket's prior verdict on the board.
+if (MERGE) {
+ try {
+ const prior = JSON.parse(fs.readFileSync(OUT, 'utf8'));
+ result.tickets = Object.assign({}, prior.tickets || {}, result.tickets);
+ result.count = Object.keys(result.tickets).length;
+ result.merged = (chosen || []).length;
+ if (Array.isArray(prior.dirs)) result.dirs = [...prior.dirs, ...result.dirs].slice(-12);
+ } catch (e) { /* no prior file — just write fresh */ }
+}
fs.writeFileSync(OUT, JSON.stringify(result, null, 2));
try { fs.unlinkSync(RUNNING); } catch {}
← a3720755 dtd-run: --cap flag + auto-chunk large sweeps (33/panel call
·
back to Ticket System
·
chore: session-close quality gate — refactor (hoist resolveL f8c7ca9f →