← back to Approval Verifier
auto-save: 2026-06-24T17:58:43 (2 files) — verify.mjs ledger.mjs
47d898658bd50eef89aabcd198449f61ef279911 · 2026-06-24 17:58:46 -0700 · Steve Abrams
Files touched
Diff
commit 47d898658bd50eef89aabcd198449f61ef279911
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jun 24 17:58:46 2026 -0700
auto-save: 2026-06-24T17:58:43 (2 files) — verify.mjs ledger.mjs
---
ledger.mjs | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
verify.mjs | 26 +++++++++++-
2 files changed, 159 insertions(+), 2 deletions(-)
diff --git a/ledger.mjs b/ledger.mjs
new file mode 100644
index 0000000..cd41273
--- /dev/null
+++ b/ledger.mjs
@@ -0,0 +1,135 @@
+#!/usr/bin/env node
+// approvals ledger — the single canonical source of truth for "following approvals".
+// DTD-A (2026-06-24, 3/3): one local approvals.json that every tool reads/writes;
+// CNCP renders a board from it; the yolo loop reads status; verify.mjs writes
+// landed/failed back. Collapses the 4 drifting state sources into one file.
+//
+// Usage:
+// node ledger.mjs # build/refresh approvals.json, then print the board
+// node ledger.mjs --build # build/refresh only
+// node ledger.mjs --board # print board from existing approvals.json (no rebuild)
+// node ledger.mjs --json # dump approvals.json to stdout
+// node ledger.mjs --status drafted,stale # board filtered to statuses
+
+import { readFileSync, writeFileSync, readdirSync, existsSync, statSync } from "node:fs";
+import { homedir } from "node:os";
+import { join, basename } from "node:path";
+
+const HOME = homedir();
+const QUEUE = join(HOME, ".claude/yolo-queue");
+const LEDGER = join(QUEUE, "approvals.json");
+const DAY = 86400000;
+const STALE_DAYS = 21;
+
+const args = process.argv.slice(2);
+const flag = (n) => args.includes(n);
+const opt = (n, d) => { const i = args.indexOf(n); return i >= 0 && args[i + 1] ? args[i + 1] : d; };
+
+// directory → base lifecycle status
+const DIRS = {
+ "pending-approval": "drafted",
+ "tasks": "approved", // promoted into the run queue
+ "done": "executed", // processed — needs verify to become 'landed'
+ "failed": "failed",
+};
+
+// --- classification heuristics (title + body) ---
+const CATS = [
+ ["fleet", /\bfleet|registry|watchdog|pm2|launchd|reconcile|keepalive|keep-alive|zombie|dark\b/i],
+ ["secrets", /\bsecret|credential|\bcred\b|rotate|token|password|\.env\b/i],
+ ["shopify", /\bshopify|theme|liquid|metafield|\bsku\b|product|variant|cadence|storefront\b/i],
+ ["dns", /\bdns|domain|cloudflare|nameserver|\bns\b|certbot|ssl\b/i],
+ ["email", /\bemail|purelymail|gmail|george|\bmx\b|dkim|dmarc|mailbox\b/i],
+ ["catalog", /\bcatalog|scrape|vendor|import|dedup|enrich\b/i],
+ ["cncp", /\bcncp|approval|dashboard\b/i],
+];
+// Explicit safety markers the memos use in their TITLES — strong low-risk signal.
+const SAFE = /\bREAD-?ONLY|DRAFTS? ONLY|propose|proposal|\baudit\b|dry.?run|report only|never (send|push|publish|delete)|SAMPLE-GATE|recommend\b/i;
+const HIGH = /\b(delete|deleting|\brm\b|\bdrop\b|rotate|\bprod\b|dw_unified|sudo|publish|push to (live|main)|send to|mailer|blast|nameserver|customer-facing|irreversible|\bforce\b)\b/i;
+const MED = /\b(install|launchctl|bootstrap|bootout|restart|pm2 (start|stop|delete|restart)|activate|enable|migrate|\bwire\b|stop\b|archive|retire)\b/i;
+const REVERSIBLE_BAD = /\b(delete|deleting|\brm\b|\bdrop\b|rotate|publish|push to (live|main)|send to|mailer|blast)\b/i;
+
+// classify on the TITLE (curated + concise) — body is too noisy and over-flags.
+function classify(title, body) {
+ const category = (CATS.find(([, re]) => re.test(title)) || ["other"])[0];
+ const safe = SAFE.test(title);
+ const risk = safe ? "low" : HIGH.test(title) ? "high" : MED.test(title) ? "med" : "low";
+ const reversible = safe || !REVERSIBLE_BAD.test(title);
+ const verifyM = body.match(/<!--\s*VERIFY:\s*([^]*?)-->/) || body.match(/```verify\s*([^]*?)```/);
+ const verify = verifyM ? verifyM[1].trim().split("\n")[0].trim() : null;
+ return { category, risk, reversible, verify };
+}
+
+function scanDir(dir, baseStatus) {
+ const p = join(QUEUE, dir);
+ if (!existsSync(p)) return [];
+ return readdirSync(p).filter(f => f.endsWith(".md")).map(f => {
+ const fp = join(p, f);
+ const body = readFileSync(fp, "utf8");
+ const title = (body.match(/^#[^#].*/m) || [f])[0].replace(/^#+\s*/, "").slice(0, 100);
+ const mtime = statSync(fp).mtime;
+ const age_days = Math.floor((Date.now() - mtime.getTime()) / DAY);
+ const { category, risk, reversible, verify } = classify(title, body);
+ let status = baseStatus;
+ if (status === "drafted" && age_days > STALE_DAYS) status = "stale";
+ return {
+ id: basename(f, ".md"), title, category, risk, reversible,
+ created: mtime.toISOString(), age_days, verify,
+ status, location: dir, last_checked: null, evidence: [],
+ };
+ });
+}
+
+function build() {
+ // preserve prior verify results (last_checked/evidence/landed) when refreshing
+ let prior = {};
+ if (existsSync(LEDGER)) { try { for (const e of JSON.parse(readFileSync(LEDGER, "utf8")).entries) prior[e.id] = e; } catch {} }
+ const entries = [];
+ for (const [dir, st] of Object.entries(DIRS)) {
+ for (const e of scanDir(dir, st)) {
+ const p = prior[e.id];
+ if (p && p.last_checked) { e.last_checked = p.last_checked; e.evidence = p.evidence;
+ if (["landed", "failed", "no-op"].includes(p.status) && e.status === "executed") e.status = p.status; }
+ entries.push(e);
+ }
+ }
+ const ledger = { generated: new Date().toISOString(), source: QUEUE.replace(HOME, "~"),
+ counts: entries.reduce((a, e) => (a[e.status] = (a[e.status] || 0) + 1, a), {}), entries };
+ writeFileSync(LEDGER, JSON.stringify(ledger, null, 2));
+ return ledger;
+}
+
+function board(ledger, statuses) {
+ const C = { drafted: "\x1b[37m", stale: "\x1b[90m", approved: "\x1b[36m", executed: "\x1b[36m",
+ landed: "\x1b[32m", failed: "\x1b[31m", "no-op": "\x1b[33m" };
+ const R = { high: "\x1b[31mHIGH\x1b[0m", med: "\x1b[33mmed \x1b[0m", low: "\x1b[32mlow \x1b[0m" };
+ const NC = "\x1b[0m";
+ // default view = the ACTIONABLE backlog (awaiting Steve), not the done/ history
+ const DEFAULT = ["drafted", "stale"];
+ const want = statuses?.length ? statuses : DEFAULT;
+ let rows = ledger.entries.filter(e => want.includes(e.status));
+ const order = { high: 0, med: 1, low: 2 };
+ rows.sort((a, b) => order[a.risk] - order[b.risk] || b.age_days - a.age_days);
+ console.log(`\n📋 approvals board — ${ledger.entries.length} items (${ledger.generated.slice(0,16)})\n`);
+ console.log(` ${"status".padEnd(9)} ${"risk".padEnd(4)} rev vfy ${"cat".padEnd(8)} age title`);
+ console.log(" " + "─".repeat(86));
+ for (const e of rows.slice(0, 60)) {
+ console.log(` ${C[e.status]||""}${e.status.padEnd(9)}${NC} ${R[e.risk]} ${e.reversible?" ✓ ":" ✗ "} ${e.verify?"✓":"·"} ${e.category.padEnd(8)} ${String(e.age_days).padStart(3)}d ${e.title.slice(0,46)}`);
+ }
+ console.log("\n── by status ──");
+ for (const [k, v] of Object.entries(ledger.counts).sort()) console.log(` ${C[k]||""}${k.padEnd(9)}${NC} ${v}`);
+ // the safe-to-batch bucket
+ const safe = ledger.entries.filter(e => ["drafted"].includes(e.status) && e.risk === "low" && e.reversible);
+ const verifiable = ledger.entries.filter(e => e.verify).length;
+ console.log(`\n ✅ safe-batch (drafted · low-risk · reversible): ${safe.length}`);
+ console.log(` 🔍 machine-verifiable (has VERIFY line): ${verifiable}/${ledger.entries.length} ← the rest need a VERIFY line or manual check`);
+ console.log(`\n rev=reversible vfy=has VERIFY line. Approve the safe-batch bucket first; never "approve all".`);
+}
+
+// --- run ---
+let ledger;
+if (flag("--board") && existsSync(LEDGER)) ledger = JSON.parse(readFileSync(LEDGER, "utf8"));
+else ledger = build();
+if (flag("--json")) { console.log(JSON.stringify(ledger, null, 2)); process.exit(0); }
+if (flag("--build")) { console.log(`built ${LEDGER.replace(HOME, "~")} — ${ledger.entries.length} entries`); process.exit(0); }
+board(ledger, (opt("--status", "") || "").split(",").map(s => s.trim()).filter(Boolean));
diff --git a/verify.mjs b/verify.mjs
index 102ba93..534af75 100644
--- a/verify.mjs
+++ b/verify.mjs
@@ -28,7 +28,7 @@
// --done also scan ~/.claude/yolo-queue/done
// Writes a JSONL ledger to ~/.claude/yolo-queue/.approval-verify-ledger.jsonl
-import { readFileSync, readdirSync, existsSync, appendFileSync, statSync } from "node:fs";
+import { readFileSync, writeFileSync, readdirSync, existsSync, appendFileSync, statSync } from "node:fs";
import { execSync } from "node:child_process";
import { homedir } from "node:os";
import { join, basename } from "node:path";
@@ -175,8 +175,30 @@ if (wantJson) {
console.log(`UNVERIFIABLE/OBSERVED = needs a VERIFY block or manual check (add <!-- VERIFY: <shell test> --> to the memo).`);
}
-// append ledger (skip in single/json ad-hoc mode)
+// append run ledger (skip in single/json ad-hoc mode)
if (!single && !wantJson) {
const ts = new Date().toISOString();
try { appendFileSync(LEDGER, rows.map(r => JSON.stringify({ ts, name: r.name, status: r.status, conf: r.conf })).join("\n") + "\n"); } catch {}
}
+
+// --write-ledger: merge verify results back into the canonical approvals.json (closes the loop).
+// Only flips POST-approval entries (executed/approved) — never a 'drafted' item.
+if (flag("--write-ledger")) {
+ const APPROVALS = join(QUEUE, "approvals.json");
+ const MAP = { LANDED: "landed", "LIKELY-LANDED": "landed", FAILED: "failed", "NO-OP": "no-op", "LIKELY-NOOP": "no-op" };
+ if (existsSync(APPROVALS)) {
+ try {
+ const L = JSON.parse(readFileSync(APPROVALS, "utf8"));
+ const byId = new Map(rows.map(r => [r.name, r]));
+ let n = 0;
+ for (const e of L.entries) {
+ const r = byId.get(e.id); if (!r) continue;
+ e.last_checked = new Date().toISOString(); e.evidence = r.evidence;
+ if (MAP[r.status] && ["executed", "approved"].includes(e.status)) { e.status = MAP[r.status]; n++; }
+ }
+ L.counts = L.entries.reduce((a, e) => (a[e.status] = (a[e.status] || 0) + 1, a), {});
+ writeFileSync(APPROVALS, JSON.stringify(L, null, 2));
+ console.log(`\n↪ wrote verify results into approvals.json (${n} status transitions)`);
+ } catch (e) { console.error("ledger write failed:", e.message); }
+ } else console.error("approvals.json not found — run `node ledger.mjs --build` first");
+}
← ce0abc9 approval-verifier: per-item end-to-end completion checker fo
·
back to Approval Verifier
·
(newest)