← back to Ticket System
reopen-from-audit: Step 3 reopen tool for TK-11903 (dry-run default, per-ticket prior status, ledgered undo)
f2531c3c8f1081d4ba1162ae31e5e54f2d8b892a · 2026-09-18 08:21:47 -0700 · Steve Abrams
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE
Files touched
A scripts/reopen-from-audit.mjs
Diff
commit f2531c3c8f1081d4ba1162ae31e5e54f2d8b892a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Sep 18 08:21:47 2026 -0700
reopen-from-audit: Step 3 reopen tool for TK-11903 (dry-run default, per-ticket prior status, ledgered undo)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE
---
scripts/reopen-from-audit.mjs | 93 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 93 insertions(+)
diff --git a/scripts/reopen-from-audit.mjs b/scripts/reopen-from-audit.mjs
new file mode 100644
index 00000000..59fa1f5b
--- /dev/null
+++ b/scripts/reopen-from-audit.mjs
@@ -0,0 +1,93 @@
+#!/usr/bin/env node
+// reopen-from-audit.mjs — Step 3 of TK-11903: reopen overnight closes that the
+// evidence join (audit-overnight-closes.mjs) could not verify.
+//
+// Reads the audit table, applies optional contrarian corrections, and for every
+// ticket whose final verdict is NO-EVIDENCE / SELF-REPORTED / RAN-BUT-GATED-REMAINDER
+// restores its PRIOR status (per ticket, from the event stream) with one comment
+// naming the audit ticket + reason, and ledgers each reopen with an undo command.
+//
+// DRY-RUN by default. `--apply` executes. Never touches VERIFIED-RAN / ALREADY-REOPENED.
+//
+// Usage:
+// node scripts/reopen-from-audit.mjs [--table data/audit-....json]
+// [--corrections data/audit-corrections.jsonl] [--exclude TK-1,TK-2] [--apply]
+import fs from 'node:fs';
+import path from 'node:path';
+import os from 'node:os';
+import { execFileSync } from 'node:child_process';
+
+const AUDIT_TICKET = 'TK-11903-overnight-close-audit-2026-09-18-verify';
+const AGENT = 'overnight-close-audit';
+const REOPEN_VERDICTS = new Set(['NO-EVIDENCE', 'SELF-REPORTED', 'RAN-BUT-GATED-REMAINDER']);
+
+const args = process.argv.slice(2);
+const opt = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d; };
+const APPLY = args.includes('--apply');
+const HERE = path.dirname(new URL(import.meta.url).pathname);
+const ROOT = path.resolve(HERE, '..');
+const TABLE = path.resolve(ROOT, opt('--table', 'data/audit-overnight-closes-2026-09-18.json'));
+const CORR = opt('--corrections', null);
+const EXCLUDE = new Set((opt('--exclude', '') || '').split(',').filter(Boolean).map(num));
+const TK = path.join(ROOT, 'tk');
+const LOGEXEC = path.join(os.homedir(), '.claude/yolo-queue/executed-reversible/log-exec.mjs');
+
+function num(id) { const m = String(id).match(/TK-0*(\d+)/); return m ? `TK-${m[1]}` : id; }
+
+const raw = JSON.parse(fs.readFileSync(TABLE, 'utf8'));
+const rows = Array.isArray(raw) ? raw : raw.rows;
+const corrections = new Map();
+if (CORR && fs.existsSync(CORR)) {
+ for (const line of fs.readFileSync(CORR, 'utf8').split('\n')) {
+ if (!line.trim()) continue;
+ try { const c = JSON.parse(line); corrections.set(num(c.ticket), c); } catch {}
+ }
+}
+
+const env = { ...process.env, TK_AGENT: AGENT };
+const tk = (...a) => execFileSync(TK, a, { env, encoding: 'utf8' });
+
+const plan = [];
+for (const r of rows) {
+ const id = r.ticket;
+ const n = num(id);
+ if (EXCLUDE.has(n)) continue;
+ let verdict = r.verdict;
+ const c = corrections.get(n);
+ if (c && c.to) verdict = c.to;
+ if (!REOPEN_VERDICTS.has(verdict)) continue;
+ if (r.final_status && r.final_status !== 'done') continue; // someone already moved it
+ let prior = r.prior_status;
+ if (!prior || prior === 'done' || prior === 'none') prior = 'blocked'; // re-close / never-statused → park it
+ const reason = c && c.evidence ? `${verdict} (contrarian: ${c.evidence})` : `${verdict}${r.gated_wording ? ' + gated wording' : ''}`;
+ plan.push({ id, n, verdict, prior, reason, close_agent: r.close_agent, close_ts: r.close_ts });
+}
+
+const byV = {};
+for (const p of plan) byV[p.verdict] = (byV[p.verdict] || 0) + 1;
+console.log(`${APPLY ? 'APPLY' : 'DRY-RUN'}: ${plan.length} tickets to reopen`, byV);
+for (const p of plan) console.log(` ${p.verdict.padEnd(24)} ${p.prior.padEnd(8)} ${p.id}`);
+if (!APPLY) { console.log('\n(no changes made — pass --apply to execute)'); process.exit(0); }
+
+let ok = 0, fail = 0;
+for (const p of plan) {
+ try {
+ const cur = tk('show', p.id).split('\n')[0];
+ if (!/\[done\]/.test(cur)) { console.log(` skip ${p.id}: no longer [done] → ${cur.slice(0, 80)}`); continue; }
+ tk('comment', p.id,
+ `REOPENED by ${AUDIT_TICKET}: closed overnight ${p.close_ts} by ${p.close_agent} with verdict ${p.reason}. ` +
+ `No independent artifact (ledger row / commit / evidence dir / re-measurable state) backs the close. ` +
+ `Restored to prior status '${p.prior}'. If the work genuinely landed, re-close with: tk done ${p.id} --force "<artifact>". ` +
+ `Table: ~/Projects/ticket-system/data/audit-overnight-closes-2026-09-18.json`);
+ tk('status', p.id, p.prior);
+ execFileSync('node', [LOGEXEC,
+ '--agent', AGENT, '--ticket', AUDIT_TICKET,
+ '--action', `reopen ${p.id} done->${p.prior} (overnight close unverified: ${p.verdict})`,
+ '--blast-radius', '1',
+ '--undo', `TK_AGENT=${AGENT} ${TK} done ${p.id} --force 'audit undo TK-11903'`,
+ '--verify', `${TK} show ${p.id} | head -1 shows [${p.prior}]`], { env, stdio: 'ignore' });
+ ok++;
+ } catch (e) { fail++; console.log(` FAIL ${p.id}: ${String(e.message || e).split('\n')[0]}`); }
+}
+console.log(`reopened=${ok} failed=${fail}`);
+fs.writeFileSync(path.join(ROOT, 'data', 'audit-overnight-reopened-2026-09-18.json'), JSON.stringify(plan, null, 2));
← f231c870 audit-overnight-closes: evidence join for overnight done eve
·
back to Ticket System
·
desktop-bar: stop re-parsing the 70MB ticket ledger every 4s e983cf75 →