[object Object]

← back to Ticket System

dtd-run: --cap flag + auto-chunk large sweeps (33/panel call) so 99+ tickets score without local-model JSON truncation; verified 99->3 chunks 19 YES/80 no

a3720755dfc4b12118fff83b48973ea4aa842664 · 2026-08-13 09:03:34 -0700 · Steve Abrams

Files touched

Diff

commit a3720755dfc4b12118fff83b48973ea4aa842664
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 13 09:03:34 2026 -0700

    dtd-run: --cap flag + auto-chunk large sweeps (33/panel call) so 99+ tickets score without local-model JSON truncation; verified 99->3 chunks 19 YES/80 no
---
 dtd-run.js | 119 +++++++++++++++++++++++++++++++++----------------------------
 1 file changed, 64 insertions(+), 55 deletions(-)

diff --git a/dtd-run.js b/dtd-run.js
index 9ae1ea38..739dcc8f 100755
--- a/dtd-run.js
+++ b/dtd-run.js
@@ -34,6 +34,8 @@ const opt  = f => { const i = argv.indexOf(f); if (i === -1) return undefined; c
 const RECENT = flag('--recent');
 const ANNOTATE = flag('--annotate');
 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)
 const explicitRefs = argv.slice();
 
 // ── pick the ticket set ──────────────────────────────────────────────────────
@@ -65,43 +67,9 @@ if (!chosen.length) {
   fs.writeFileSync(OUT, JSON.stringify({ ranAt: new Date().toISOString(), count: 0, tickets: {}, note: 'no eligible tickets' }, null, 2));
   console.log('dtd-run: no eligible tickets'); process.exit(0);
 }
-// cap so one sweep can't build a giant prompt (and blow the panel timeout)
-const CAP = 40;
+// cap the sweep (newest-first). Default 40; pass --cap to score more/all.
 if (chosen.length > CAP) { chosen.sort((a, b) => lastTs(b) - lastTs(a)); chosen = chosen.slice(0, CAP); }
 
-// ── frame the ONE batched decision question ──────────────────────────────────
-const digest = chosen.map(t => {
-  const idleH = ((Date.now() - lastTs(t)) / 3600000).toFixed(0);
-  return `- ${t.id} [${t.status}] {${t.project || 'no-project'}} owner=${t.assignee || 'unassigned'} idle=${idleH}h :: ${t.title} :: last="${(lastText(t) || '').slice(0, 160).replace(/"/g, "'")}"`;
-}).join('\n');
-
-const QUESTION = `You are the DTD decision panel. For EACH ticket below, decide RUN NOW = "yes" or "no".
-
-RUN NOW = yes  -> this ticket is actionable RIGHT NOW: clearly scoped, unblocked, worth spawning a dedicated Claude session to drive to done.
-RUN NOW = no   -> not now: it is blocked/waiting on a human gate or another ticket, too vague to act on, already effectively finished, a duplicate, or stale/abandoned and should be reconsidered rather than run.
-
-Be decisive — every ticket gets a clear yes or no, no "maybe".
-
-Return ONLY a JSON array, nothing else, one object per ticket, exactly this shape:
-[{"id":"TK-#####","run_now":"yes","reason":"<=12 words"}, ...]
-
-TICKETS:
-${digest}`;
-
-// ── run the panel ────────────────────────────────────────────────────────────
-fs.writeFileSync(RUNNING, JSON.stringify({ startedAt: new Date().toISOString(), count: chosen.length }));
-console.log(`dtd-run: fanning ${chosen.length} tickets to the panel…`);
-let dir = null;
-try {
-  const out = execFileSync('bash', [PANEL, QUESTION], { encoding: 'utf8', timeout: 360000, maxBuffer: 32 * 1024 * 1024 });
-  const m = out.match(/^DIR=(.+)$/m);
-  dir = m ? m[1].trim() : null;
-} catch (e) {
-  try { fs.unlinkSync(RUNNING); } catch {}
-  fs.writeFileSync(OUT, JSON.stringify({ ranAt: new Date().toISOString(), error: 'panel failed: ' + e.message, count: 0, tickets: {} }, null, 2));
-  console.error('dtd-run: panel.sh failed:', e.message); process.exit(1);
-}
-
 // ── parse each panelist's JSON verdict array (tolerant) ──────────────────────
 const numOf = id => parseInt(String(id).replace(/^TK-/i, ''), 10); // stops at slug dash
 function parseVotes(text) {
@@ -120,29 +88,70 @@ function parseVotes(text) {
   }
   return votes;
 }
+const panelists = ['codex', 'qwen', 'grok', 'kimi', 'muse', 'exo'];
 
-const panelists = ['codex', 'qwen', 'grok', 'kimi', 'muse'];
-const perPanel = {};
-if (dir) for (const p of panelists) {
-  try { perPanel[p] = parseVotes(fs.readFileSync(path.join(dir, `${p}.txt`), 'utf8')); }
-  catch { perPanel[p] = new Map(); }
-}
+// One panel.sh call over a CHUNK of tickets -> committed per-ticket verdicts.
+function scoreChunk(batch) {
+  const digest = batch.map(t => {
+    const idleH = ((Date.now() - lastTs(t)) / 3600000).toFixed(0);
+    return `- ${t.id} [${t.status}] {${t.project || 'no-project'}} owner=${t.assignee || 'unassigned'} idle=${idleH}h :: ${t.title} :: last="${(lastText(t) || '').slice(0, 160).replace(/"/g, "'")}"`;
+  }).join('\n');
+  const QUESTION = `You are the DTD decision panel. For EACH ticket below, decide RUN NOW = "yes" or "no".
+
+RUN NOW = yes  -> this ticket is actionable RIGHT NOW: clearly scoped, unblocked, worth spawning a dedicated Claude session to drive to done.
+RUN NOW = no   -> not now: it is blocked/waiting on a human gate or another ticket, too vague to act on, already effectively finished, a duplicate, or stale/abandoned and should be reconsidered rather than run.
+
+Be decisive — every ticket gets a clear yes or no, no "maybe".
 
-// ── tally + COMMIT a verdict per ticket ──────────────────────────────────────
-const result = { ranAt: new Date().toISOString(), dir, count: chosen.length, tickets: {} };
-for (const t of chosen) {
-  const n = numOf(t.id);
-  let yes = 0, no = 0; const voters = [];
-  for (const p of panelists) {
-    const v = perPanel[p] && perPanel[p].get(n);
-    if (v === 'yes') { yes++; voters.push(`${p}:yes`); }
-    else if (v === 'no') { no++; voters.push(`${p}:no`); }
+Return ONLY a JSON array, nothing else, one object per ticket, exactly this shape:
+[{"id":"TK-#####","run_now":"yes","reason":"<=12 words"}, ...]
+
+TICKETS:
+${digest}`;
+  let dir = null;
+  const out = execFileSync('bash', [PANEL, QUESTION], { encoding: 'utf8', timeout: 360000, maxBuffer: 32 * 1024 * 1024 });
+  const m = out.match(/^DIR=(.+)$/m); dir = m ? m[1].trim() : null;
+  const perPanel = {};
+  if (dir) for (const p of panelists) {
+    try { perPanel[p] = parseVotes(fs.readFileSync(path.join(dir, `${p}.txt`), 'utf8')); } catch { perPanel[p] = new Map(); }
+  }
+  const verdicts = {};
+  for (const t of batch) {
+    const n = numOf(t.id);
+    let yes = 0, no = 0; const voters = [];
+    for (const p of panelists) {
+      const v = perPanel[p] && perPanel[p].get(n);
+      if (v === 'yes') { yes++; voters.push(`${p}:yes`); }
+      else if (v === 'no') { no++; voters.push(`${p}:no`); }
+    }
+    const total = yes + no;
+    const verdict = total === 0 ? 'unknown' : (yes > no ? 'yes' : 'no'); // tie/zero -> conservative "no"/unknown; never auto-run on a tie
+    const confidence = total === 0 ? 0 : +(Math.max(yes, no) / total).toFixed(2);
+    verdicts[t.id] = { verdict, yes, no, total, confidence, voters, title: t.title, status: t.status };
   }
-  const total = yes + no;
-  // COMMIT: majority; tie or zero votes -> conservative "no" (never auto-run on a tie)
-  const verdict = total === 0 ? 'unknown' : (yes > no ? 'yes' : 'no');
-  const confidence = total === 0 ? 0 : +(Math.max(yes, no) / total).toFixed(2);
-  result.tickets[t.id] = { verdict, yes, no, total, confidence, voters, title: t.title, status: t.status };
+  return { dir, verdicts };
+}
+
+// ── 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));
+fs.writeFileSync(RUNNING, JSON.stringify({ startedAt: new Date().toISOString(), count: chosen.length, chunks: chunks.length }));
+console.log(`dtd-run: ${chosen.length} tickets in ${chunks.length} chunk(s) of ≤${CHUNK}…`);
+
+const result = { ranAt: new Date().toISOString(), count: chosen.length, chunks: chunks.length, dirs: [], tickets: {} };
+try {
+  chunks.forEach((batch, i) => {
+    console.log(`  chunk ${i + 1}/${chunks.length} (${batch.length} tickets)…`);
+    const { dir, verdicts } = scoreChunk(batch);
+    if (dir) result.dirs.push(dir);
+    Object.assign(result.tickets, verdicts);
+  });
+} catch (e) {
+  try { fs.unlinkSync(RUNNING); } catch {}
+  // keep whatever chunks already committed; record the error
+  result.error = 'panel failed on a chunk: ' + e.message;
+  fs.writeFileSync(OUT, JSON.stringify(result, null, 2));
+  console.error('dtd-run: panel.sh failed:', e.message); process.exit(1);
 }
 
 fs.writeFileSync(OUT, JSON.stringify(result, null, 2));

← 10915c3e fix(board): DTD chip stuck on 'analyzing…' — start poll on l  ·  back to Ticket System  ·  dtd-run: concurrency guard (refuse to clobber a live sweep) 1f899783 →