← back to Ticket System
Backfill blocked ticket classifications
94c9fbd3c6b791d32c77f1d5212fbe710ad39d53 · 2026-08-31 10:25:04 -0700 · Steve Abrams
Files touched
M board.htmlA scripts/backfill-blockers.mjs
Diff
commit 94c9fbd3c6b791d32c77f1d5212fbe710ad39d53
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 31 10:25:04 2026 -0700
Backfill blocked ticket classifications
---
board.html | 4 ++--
scripts/backfill-blockers.mjs | 46 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 48 insertions(+), 2 deletions(-)
diff --git a/board.html b/board.html
index 2a172c55..4cb6c7ff 100644
--- a/board.html
+++ b/board.html
@@ -225,7 +225,7 @@ function match(t){if(VIEW!=='all'&&(t.kind||'task')!==VIEW)return false;if(!q)re
const thread=[...(t.comments||[]).map(c=>c.text),...(t.actions||[]).map(a=>a.text)].join(' ');
const hay=(t.id+' '+t.title+' '+(t.project||'')+' '+(t.assignee||'')+' '+t.status+' '+(t.kind||'task')+' '+JSON.stringify(t.schedule||{})+' '+thread).toLowerCase();
return q.toLowerCase().split(/\s+/).every(w=>hay.includes(w));}
-const groupLabel=(k,v)=>k==='status'?(v==='stopped'?'TicketStopped':v):k==='assignee'?(v||'unassigned'):(v||'—');
+const groupLabel=(k,v)=>k==='status'?(v==='stopped'?'TicketStopped':v):k==='blocker'?(BLOCKER_FACE[v]||v||'Unclassified'):k==='assignee'?(v||'unassigned'):(v||'—');
function render(){
renderHead();
const cols=ordered(); const span=cols.length+1;
@@ -251,7 +251,7 @@ function render(){
for(const t of rows)rowTr(t);
}else{
const groups=new Map();
- for(const t of rows){const gv=GROUP==='status'?t.status:GROUP==='project'?(t.project||''):(t.assignee||'');(groups.get(gv)||groups.set(gv,[]).get(gv)).push(t);}
+ for(const t of rows){const gv=GROUP==='status'?t.status:GROUP==='blocker'?(t.blocker?.type||''):GROUP==='project'?(t.project||''):(t.assignee||'');(groups.get(gv)||groups.set(gv,[]).get(gv)).push(t);}
// order groups: for status use canonical order, else alpha
const order=GROUP==='status'?['open','doing','blocked','done','stopped']:[...groups.keys()].sort((a,b)=>String(a).localeCompare(String(b)));
for(const gv of order){const list=groups.get(gv);if(!list)continue;
diff --git a/scripts/backfill-blockers.mjs b/scripts/backfill-blockers.mjs
new file mode 100644
index 00000000..87ffc3e3
--- /dev/null
+++ b/scripts/backfill-blockers.mjs
@@ -0,0 +1,46 @@
+#!/usr/bin/env node
+import { createRequire } from 'node:module';
+const require = createRequire(import.meta.url);
+const tk = require('../lib.js');
+
+const apply = process.argv.includes('--apply');
+const force = process.argv.includes('--force');
+const now = new Date();
+const isoAfter = days => new Date(now.getTime() + days * 86400000).toISOString();
+const allText = t => [t.title, ...(t.comments || []).map(x => x.text), ...(t.actions || []).map(x => x.text)].join(' ');
+const latestText = t => [...(t.comments || []), ...(t.actions || [])].sort((a,b) => a.ts.localeCompare(b.ts)).at(-1)?.text || t.title;
+
+function classify(t) {
+ const text = allText(t);
+ if (/standing directive|kill[- ]switch|purpose is to keep .* blocked|intentional (fail|guardrail)|do not (run|send|publish)/i.test(text)) return 'intentional_guardrail';
+ if (t.project === 'cron-fire-canary' || /^Cron issue:/i.test(t.title)) return 'technical_dependency';
+ if (/send-to-list|destructive|identity|console-only|login|2FA|KYC|spend|real-money|sudo|delete|purge|history rewrite/i.test(text)) return 'steve_action';
+ if (/awaiting .*vendor|vendor .*cost list|cost list .*arriv|upstream .*unavailable|platform review|app review|trademark gate|legal gate|external .*wait/i.test(text)) return 'external_wait';
+ if (/\bSteve('|’)?s?\b|approval|customer-facing|production write|prod write|live Shopify|publish authorization/i.test(text)) return 'steve_action';
+ return 'technical_dependency';
+}
+
+function nextAction(type) {
+ if (type === 'steve_action') return 'Surface in the next ranked Steve decision batch; execute only after explicit approval or identity action.';
+ if (type === 'external_wait') return 'Recheck the named external dependency on schedule; attach fresh evidence when it changes.';
+ if (type === 'intentional_guardrail') return 'Keep the guardrail armed; recheck only after an explicit Steve directive changes it.';
+ return 'Owner reproduces the dependency, records exact evidence, and advances only reversible non-gated remediation.';
+}
+
+const rows = [...tk.tickets().values()].filter(t => t.status === 'blocked' && (force || !t.blocker));
+const summary = {};
+for (const t of rows) {
+ const type = classify(t); summary[type] = (summary[type] || 0) + 1;
+ const blocker = {
+ type,
+ condition: latestText(t).replace(/\s+/g, ' ').slice(0, 500),
+ next_action: nextAction(type),
+ owner: type === 'steve_action' ? 'steve' : (t.assignee || 'unassigned'),
+ evidence_at: t.updated_at,
+ recheck_at: type === 'technical_dependency' ? isoAfter(1) : type === 'external_wait' ? isoAfter(7) : undefined,
+ steve_one_action: type === 'steve_action',
+ };
+ if (apply) tk.ticketBlocker(t.id, blocker, { agent:'codex' });
+ console.log(`${apply ? 'APPLY' : 'DRY'}\t${t.id}\t${type}\t${blocker.owner}`);
+}
+console.error(JSON.stringify({ apply, tickets:rows.length, lanes:summary }));
← 2d6d9cea Add actionable blocker lanes
·
back to Ticket System
·
Record blocker lanes E2E proof 219bfbe0 →