[object Object]

← back to Ticket System

Verify ticket API timeout recovery and stage bounded session probe fix

00189757698e3bb6786edb5d27a9daf4ba82b32e · 2026-09-11 08:19:53 -0700 · Steve Abrams

Files touched

Diff

commit 00189757698e3bb6786edb5d27a9daf4ba82b32e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 11 08:19:53 2026 -0700

    Verify ticket API timeout recovery and stage bounded session probe fix
---
 verification/tk11372/APPROVAL.md          |  24 ++
 verification/tk11372/api-original.cjs     |  72 +++++
 verification/tk11372/candidate.patch      |  85 ++++++
 verification/tk11372/dtd/claude.txt       |   1 +
 verification/tk11372/dtd/codex-debate.txt |   9 +
 verification/tk11372/dtd/codex.txt        |   1 +
 verification/tk11372/dtd/grok.txt         |   1 +
 verification/tk11372/dtd/kimi.txt         |   1 +
 verification/tk11372/dtd/muse.txt         |   1 +
 verification/tk11372/dtd/question.txt     |   1 +
 verification/tk11372/dtd/qwen.txt         |   1 +
 verification/tk11372/e2e-proof.json       |  74 +++++
 verification/tk11372/server.baseline.cjs  | 467 +++++++++++++++++++++++++++++
 verification/tk11372/server.candidate.cjs | 476 ++++++++++++++++++++++++++++++
 verification/tk11372/stages.cjs           | 105 +++++++
 15 files changed, 1319 insertions(+)

diff --git a/verification/tk11372/APPROVAL.md b/verification/tk11372/APPROVAL.md
new file mode 100644
index 00000000..fc24e9fe
--- /dev/null
+++ b/verification/tk11372/APPROVAL.md
@@ -0,0 +1,24 @@
+# TK-11372 — approval to integrate and activate the verified timeout fix
+
+Status: HOLD. Local candidate passes all four subprocess timeout/recovery stages and nine original HTTP cases. Production source remains unchanged. Earlier c045ffd approval draft is superseded by this candidate, which also fixes the reproduced surviving session-ps child.
+
+## Reviewable scope
+
+Apply only `verification/tk11372/candidate.patch` to `server.js`: asynchronous lsof/ps, one refresh in flight, immediate cached responses, preserve last-good data on collection failure, direct session ps with no shell pipeline.
+
+Candidate SHA256: `57baae6e06577798fa003588c37611c529f3a0bb14a0e6ce1a4fdff40b7bfe2b`.
+Required pre-activation server SHA256: `eaff74765854acbbf59a3790e1529c9a91ba2109575367c7dca84efdd486f524`.
+Evidence: `/Users/macstudio3/Projects/ticket-system/verification/tk11372/e2e-proof.json` and adjacent results. DTD 2/2 available votes A; adversarial review KEEP. No paid calls.
+
+## Exact requested approval
+
+Approve local integration of that patch and restart **only PM2 ticket-board ID 57**, PM2_HOME `/Users/macstudio3/.pm2`, after revalidating that ID/name/script match the current :9794 listener and the current RPC socket belongs to its supervisor. Most recent listener PID 36347; prior PID 94235 exited externally. Observed supervisor PID 27622. These PIDs must be refreshed, never assumed stable.
+
+1. Verify live source hash above, fresh clean ownership, and `git apply --check verification/tk11372/candidate.patch`.
+2. Retain baseline backup, apply only the patch, syntax-check, and commit that source change locally.
+3. With the exact daemon/ID identity confirmed, activation command:
+   `PM2_HOME=/Users/macstudio3/.pm2 node /Users/macstudio3/.claude/skills/keep-alive/proposals/TK-10970/pm2-serialized.js restart 57`
+4. Verify real healthz, unauthorized 401, authenticated tickets and concurrent running reads over several cache refreshes. Stop on mismatch; do not restart any other service or resurrect a fleet.
+5. If activation fails, restore the exact baseline server from `verification/tk11372/server.baseline.cjs` after hash verification and restart the same verified service (rollback included in requested scope). Verify rollback health and report failure; do not close ticket.
+
+This approval does not authorize remote pushes, broad PM2 recovery, tunnel changes, or other production edits. Mark ticket done only after live proof passes. Sole cause of the live stalls remains unproven; PM2-wrapper grandchildren and synchronous ticket reduction remain limits of this increment.
diff --git a/verification/tk11372/api-original.cjs b/verification/tk11372/api-original.cjs
new file mode 100644
index 00000000..2236db7c
--- /dev/null
+++ b/verification/tk11372/api-original.cjs
@@ -0,0 +1,72 @@
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const http = require('http');
+const { fork, execFileSync } = require('child_process');
+const assert = require('assert/strict');
+const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tk11372-original-runtime-'));
+fs.copyFileSync(path.join(__dirname, '../../lib.js'), path.join(root, 'lib.js'));
+fs.copyFileSync(path.join(__dirname, 'server.baseline.cjs'), path.join(root, 'server.baseline.js'));
+fs.copyFileSync(path.join(__dirname, 'server.candidate.cjs'), path.join(root, 'server.js'));
+const base = fs.mkdtempSync(path.join(os.tmpdir(), 'tk11372-http-'));
+const results = [];
+if (!fs.existsSync(path.join(root, 'server.baseline.js'))) fs.writeFileSync(path.join(root, 'server.baseline.js'), execFileSync('git', ['show', 'd68a46cf9dd68f09c18ef1e791691dbb4875aae2:server.js'], {cwd:root}));
+const delay = ms => new Promise(r => setTimeout(r, ms));
+const auth = 'Basic ' + Buffer.from('fixture:fixture').toString('base64');
+function request(port, pathname, authorized = true, timeout = 1500) {
+  const start = Date.now();
+  return new Promise(resolve => {
+    const req = http.get({ host: '127.0.0.1', port, path: pathname, headers: authorized ? { authorization: auth } : {} }, res => {
+      let body = ''; res.on('data', b => body += b); res.on('end', () => resolve({ status: res.statusCode, body, ms: Date.now() - start }));
+    });
+    req.setTimeout(timeout, () => req.destroy(new Error('timeout')));
+    req.on('error', e => resolve({ error: e.message, ms: Date.now() - start }));
+  });
+}
+async function launch(filename, label) {
+  const home = path.join(base, label); fs.mkdirSync(home, { recursive: true });
+  const bin = path.join(home, 'bin'); fs.mkdirSync(bin);
+  fs.mkdirSync(path.join(home, '.pm2')); fs.writeFileSync(path.join(home, '.pm2', 'rpc.sock'), 'fixture');
+  const data = path.join(home, 'tickets'); fs.mkdirSync(data);
+  fs.writeFileSync(path.join(data, 'events.jsonl'), JSON.stringify({type:'create', id:'TK-1-fixture', title:'fixture', agent:'fixture', ts:new Date().toISOString()})+'\n');
+  const mode = path.join(home, 'mode'); fs.writeFileSync(mode, 'hung');
+  const trace = path.join(home, 'trace.jsonl'); fs.writeFileSync(trace, '');
+  const probe = `#!${process.execPath}\nconst fs=require('fs');const p=require('path');const mode=fs.readFileSync(process.env.API_FIXTURE_MODE,'utf8');const command=p.basename(process.argv[1]);fs.appendFileSync(process.env.API_FIXTURE_TRACE,JSON.stringify({command,mode,at:Date.now(),pid:process.pid})+'\\n');if(command==='lsof'){if(mode==='hung') Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,6000);if(mode==='missing') process.exit(1);console.log('fixture socket holder');}else console.log('1 PM2 v6.0: God Daemon ('+process.env.PM2_HOME+')');\n`;
+  for (const command of ['lsof','ps']) { const file=path.join(bin,command);fs.writeFileSync(file,probe);fs.chmodSync(file,0o755); }
+  const wrapper = path.join(home,'.claude','skills','keep-alive','proposals','TK-10970');fs.mkdirSync(wrapper,{recursive:true});
+  fs.writeFileSync(path.join(wrapper,'pm2-serialized.js'), `const fs=require('fs');const mode=fs.readFileSync(process.env.API_FIXTURE_MODE,'utf8');fs.appendFileSync(process.env.API_FIXTURE_TRACE,JSON.stringify({command:'jlist',mode,at:Date.now()})+'\\n');if(mode==='bad-json') console.log('bad');else console.log(JSON.stringify([{name:mode,pm2_env:{status:'online',pm_uptime:42,restart_time:0},monit:{cpu:1,memory:1048576}}]));`);
+  const preload = path.join(home, 'preload.cjs');fs.writeFileSync(preload, `const http=require('http');const listen=http.Server.prototype.listen;http.Server.prototype.listen=function(...args){this.once('listening',()=>process.send({port:this.address().port}));return listen.apply(this,args);};`);
+  const child = fork(path.join(root,filename), [], { execArgv:['--require',preload], env:{...process.env,HOME:home,PM2_HOME:path.join(home,'.pm2'),TICKET_DATA_DIR:data,TK_AUTH:'fixture:fixture',PORT:'0',PATH:bin+':'+process.env.PATH,API_FIXTURE_MODE:mode,API_FIXTURE_TRACE:trace}, stdio:['ignore','pipe','pipe','ipc'] });
+  let output='';child.stdout.on('data',b=>output+=b);child.stderr.on('data',b=>output+=b);
+  const port=await new Promise((resolve,reject)=>{ const timeout=setTimeout(()=>reject(new Error('listen timeout')),15000);child.once('message',m=>{clearTimeout(timeout);resolve(m.port)});child.once('error',reject);});
+  return {child,port,home,mode,trace,data,output:()=>output};
+}
+async function until(fn, predicate, max=20000) {
+  const end=Date.now()+max;let r;
+  while(Date.now()<end){r=await fn();if(predicate(r))return r;await delay(300);}
+  throw new Error('condition not reached: '+JSON.stringify(r));
+}
+(async()=>{
+  let live=[];
+  try {
+    const baseline=await launch('server.baseline.js','baseline');live.push(baseline.child);
+    const blocked=await request(baseline.port,'/healthz');assert.equal(blocked.error,'timeout');results.push({name:'baseline startup health blocked by hung probe',verdict:'PASS',response:blocked});baseline.child.kill('SIGTERM');
+    const candidate=await launch('server.js','candidate');live.push(candidate.child);
+    const first=await Promise.all([request(candidate.port,'/healthz'),request(candidate.port,'/api/tickets',false),request(candidate.port,'/api/tickets'),...Array.from({length:12},()=>request(candidate.port,'/api/running'))]);
+    assert.equal(first[0].status,200);assert.equal(first[1].status,401);assert.equal(JSON.parse(first[2].body)[0].title,'fixture');assert(first.slice(3).every(r=>r.status===200&&JSON.parse(r.body).at===null));
+    results.push({name:'candidate first-start health auth tickets and twelve overlapping running reads',verdict:'PASS',responses:first});
+    await delay(1000);let trace=fs.readFileSync(candidate.trace,'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);assert.equal(trace.filter(x=>x.command==='lsof').length,1);results.push({name:'singleflight during startup and overlapping requests',verdict:'PASS',trace});
+    await delay(3500);fs.writeFileSync(candidate.mode,'healthy');
+    const healthy=await until(()=>request(candidate.port,'/api/running'),r=>r.status===200&&JSON.parse(r.body).pm2[0]?.name==='healthy');results.push({name:'probe timeout clears singleflight and recovers',verdict:'PASS',response:healthy});
+    fs.writeFileSync(candidate.mode,'missing');
+    await until(async()=>{await request(candidate.port,'/api/running');return fs.readFileSync(candidate.trace,'utf8');},t=>t.includes('\"mode\":\"missing\"'));await delay(500);
+    const fallback=await request(candidate.port,'/api/running');assert.equal(JSON.parse(fallback.body).pm2[0].name,'healthy');assert.equal(JSON.parse(fallback.body).at,JSON.parse(healthy.body).at);results.push({name:'unreachable probe preserves last-good list and original freshness timestamp',verdict:'PASS',response:fallback});
+    fs.writeFileSync(candidate.mode,'bad-json');
+    await until(async()=>{await request(candidate.port,'/api/running');return fs.readFileSync(candidate.trace,'utf8');},t=>t.includes('\"command\":\"jlist\",\"mode\":\"bad-json\"'));await delay(500);
+    const malformed=await request(candidate.port,'/api/running');assert.equal(JSON.parse(malformed.body).pm2[0].name,'healthy');results.push({name:'malformed jlist preserves last-good list',verdict:'PASS',response:malformed});
+    fs.writeFileSync(candidate.mode,'recovered');const recovered=await until(()=>request(candidate.port,'/api/running'),r=>r.status===200&&JSON.parse(r.body).pm2[0]?.name==='recovered');results.push({name:'recovery after fallback and malformed jlist',verdict:'PASS',response:recovered});
+    trace=fs.readFileSync(candidate.trace,'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);assert.equal(trace.filter(t=>t.command==='jlist'&&t.mode==='missing').length,0);results.push({name:'unreachable daemon never invokes jlist',verdict:'PASS',trace});
+    assert.equal(fs.readFileSync(path.join(candidate.data,'events.jsonl'),'utf8').trim().split('\n').length,1);results.push({name:'fixture ticket data remains one original event',verdict:'PASS'});
+  } finally { for(const child of live)child.kill('SIGTERM');fs.writeFileSync(path.join(base,'results.json'),JSON.stringify({base,results},null,2)); }
+  console.log(JSON.stringify({status:'PASS',base,results},null,2));
+})().catch(e=>{console.error(e.stack);process.exitCode=1;});
diff --git a/verification/tk11372/candidate.patch b/verification/tk11372/candidate.patch
new file mode 100644
index 00000000..622c980f
--- /dev/null
+++ b/verification/tk11372/candidate.patch
@@ -0,0 +1,85 @@
+--- a/server.js
++++ b/server.js
+@@ -37,42 +37,51 @@
+ // of socket-less orphan daemons, so we only jlist when the rpc.sock exists AND a live God daemon
+ // holds it; otherwise return the cached/empty pm2 list (never fork).
+ const PM2_HOME_TS = process.env.PM2_HOME || require('path').join(require('os').homedir(), '.pm2');
+-function pm2DaemonReachable() {
+-  try {
+-    const rpc = require('path').join(PM2_HOME_TS, 'rpc.sock');
+-    if (!require('fs').existsSync(rpc)) return false;
+-    const { execSync } = require('child_process');
+-    const held = execSync(`lsof -nP ${JSON.stringify(rpc)} 2>/dev/null`, { timeout: 4000 }).toString().trim();
+-    if (!held) return false;
+-    const ps = execSync('ps ax -o pid,command 2>/dev/null', { maxBuffer: 8 * 1024 * 1024, timeout: 4000 }).toString();
+-    return ps.split('\n').some(l => /PM2 v[\d.]+: God Daemon/.test(l) && l.includes(PM2_HOME_TS));
+-  } catch (_) { return false; }
++function pm2DaemonReachable(cb) {
++  const rpc = path.join(PM2_HOME_TS, 'rpc.sock');
++  if (!fs.existsSync(rpc)) return cb(false);
++  const options = { timeout: 4000, killSignal: 'SIGKILL', maxBuffer: 8 * 1024 * 1024 };
++  execFile('lsof', ['-nP', rpc], options, (error, held) => {
++    if (error || !String(held).trim()) return cb(false);
++    execFile('ps', ['ax', '-o', 'pid,command'], options, (error, output) => {
++      cb(!error && String(output).split('\n').some(line => /PM2 v[\d.]+: God Daemon/.test(line) && line.includes(PM2_HOME_TS)));
++    });
++  });
+ }
+ let runCache = { ts: 0, data: { pm2: [], sessions: 0, at: null } };
++let runRefresh = false;
+ const PM2_SERIALIZED_TS = path.join(os.homedir(), '.claude', 'skills', 'keep-alive', 'proposals', 'TK-10970', 'pm2-serialized.js');
+ function getRunning(cb) {
+-  if (Date.now() - runCache.ts < 5000) return cb(runCache.data);
+-  if (!pm2DaemonReachable()) {
+-    // socket not reachable — do NOT fork a daemon; serve last-known (or empty) pm2 list
+-    runCache = { ts: Date.now(), data: { pm2: runCache.data.pm2 || [], sessions: runCache.data.sessions || 0, at: new Date().toISOString() } };
+-    return cb(runCache.data);
+-  }
+-  execFile(process.execPath, [PM2_SERIALIZED_TS, 'jlist'], { maxBuffer: 16 * 1024 * 1024, timeout: 22000, killSignal: 'SIGKILL' }, (e, out) => {
+-      let pm2 = [];
+-      if (!e) { try {
+-        pm2 = JSON.parse(out).filter(p => p.pm2_env && p.pm2_env.status === 'online')
+-          .map(p => ({ name: p.name, cpu: (p.monit && p.monit.cpu) || 0,
+-            mem: Math.round(((p.monit && p.monit.memory) || 0) / 1048576),
+-            up: p.pm2_env.pm_uptime || 0, restarts: p.pm2_env.restart_time || 0 }))
+-          .sort((a, b) => a.name < b.name ? -1 : 1);
+-      } catch (_) {} }
+-      // count live `claude` CLI sessions (exclude skills-dir helpers), best-effort
+-      exec("ps -Ao command | grep '[c]laude' | grep -v 'skills/' | wc -l", { timeout: 4000 }, (e2, out2) => {
+-        const sessions = e2 ? 0 : (parseInt(String(out2).trim(), 10) || 0);
+-        runCache = { ts: Date.now(), data: { pm2, sessions, at: new Date().toISOString() } };
+-        cb(runCache.data);
++  if (Date.now() - runCache.ts >= 5000 && !runRefresh) {
++    runRefresh = true;
++    const finish = (data = runCache.data) => {
++      runCache = { ts: Date.now(), data };
++      runRefresh = false;
++    };
++    pm2DaemonReachable(reachable => {
++      if (!reachable) return finish();
++      execFile(process.execPath, [PM2_SERIALIZED_TS, 'jlist'], { maxBuffer: 16 * 1024 * 1024, timeout: 22000, killSignal: 'SIGKILL' }, (error, output) => {
++        if (error) return finish();
++        let pm2;
++        try {
++          pm2 = JSON.parse(output).filter(p => p.pm2_env && p.pm2_env.status === 'online')
++            .map(p => ({ name: p.name, cpu: (p.monit && p.monit.cpu) || 0,
++              mem: Math.round(((p.monit && p.monit.memory) || 0) / 1048576),
++              up: p.pm2_env.pm_uptime || 0, restarts: p.pm2_env.restart_time || 0 }))
++            .sort((a, b) => a.name < b.name ? -1 : 1);
++        } catch { return finish(); }
++        // Count in-process so the timeout targets ps itself, not a shell whose
++        // children could survive and keep the refresh pipes open indefinitely.
++        execFile('ps', ['-Ao', 'command'], { timeout: 4000, killSignal: 'SIGKILL', maxBuffer: 8 * 1024 * 1024 }, (error, output) => {
++          const sessions = error ? runCache.data.sessions : String(output).split('\n')
++            .filter(line => line.includes('claude') && !line.includes('skills/')).length;
++          finish({ pm2, sessions, at: new Date().toISOString() });
++        });
+       });
+-  });
++    });
++  }
++  // Monitoring subprocesses must never delay health, auth, or first-start requests.
++  cb(runCache.data);
+ }
+ 
+ const OFFICE_HTML = path.join(__dirname, 'office.html');
diff --git a/verification/tk11372/dtd/claude.txt b/verification/tk11372/dtd/claude.txt
new file mode 100644
index 00000000..19b5d169
--- /dev/null
+++ b/verification/tk11372/dtd/claude.txt
@@ -0,0 +1 @@
+[claude disabled: DTD_ZERO_COST=1]
diff --git a/verification/tk11372/dtd/codex-debate.txt b/verification/tk11372/dtd/codex-debate.txt
new file mode 100644
index 00000000..5d2d0495
--- /dev/null
+++ b/verification/tk11372/dtd/codex-debate.txt
@@ -0,0 +1,9 @@
+**PROSECUTOR:** Overturn A. “Extend isolated tests” risks substituting test activity for operational proof. An async probe can return on timeout while leaving its child process alive, accumulating overlapping probes, or continuing to display stale data as healthy. Isolated tests may miss interactions among hung `ps`, `jlist`, and session collection. A also specifies no exit criteria, so “before gated production adoption” could become indefinite delay. The stronger approach is a bounded integration exercise with simultaneous failures, resource accounting, and explicit acceptance criteria. Only two models voted; neither supplied supporting test results.
+
+**DEFENDER:** Those objections establish what A’s tests must demonstrate, not why extending them is wrong. A explicitly includes hangs and recovery; meaningful coverage must verify child-process cleanup, bounded concurrency, stale-data signaling, responsiveness of unaffected collectors, and successful collection after recovery. Isolation can encompass an integrated candidate running outside production—it need not mean disconnected unit tests. The supplied record establishes neither that these checks already pass nor that production exposure is necessary to perform them. Advancing adoption without that evidence would leave the prosecutor’s strongest failure modes unresolved.
+
+**JUDGE:** The prosecutor defeats reliance on the vote count and exposes the danger of vague testing, but does not defeat A’s sequencing. Its proposed integration exercise is compatible with extending isolated candidate tests. A should have a finite completion condition: the named hang and recovery scenarios pass, cleanup and resource bounds hold, and failures remain visible. These are required checks, not results established by this record.
+
+FINAL: KEEP
+
+The controlling argument is that the strongest objection identifies missing failure-containment evidence that can be obtained before production adoption. It therefore supports making A’s tests bounded and operationally meaningful rather than overturning the decision. Keep A because its sequence addresses the concrete risks raised; the two affirmative votes alone do not establish readiness.
\ No newline at end of file
diff --git a/verification/tk11372/dtd/codex.txt b/verification/tk11372/dtd/codex.txt
new file mode 100644
index 00000000..35ed1f18
--- /dev/null
+++ b/verification/tk11372/dtd/codex.txt
@@ -0,0 +1 @@
+VERDICT: A. Extend isolated async probe candidate tests for hung ps, jlist, sessions, and recovery before gated production adoption.
\ No newline at end of file
diff --git a/verification/tk11372/dtd/grok.txt b/verification/tk11372/dtd/grok.txt
new file mode 100644
index 00000000..7b3ad95a
--- /dev/null
+++ b/verification/tk11372/dtd/grok.txt
@@ -0,0 +1 @@
+[grok unavailable]
diff --git a/verification/tk11372/dtd/kimi.txt b/verification/tk11372/dtd/kimi.txt
new file mode 100644
index 00000000..ea4b5a56
--- /dev/null
+++ b/verification/tk11372/dtd/kimi.txt
@@ -0,0 +1 @@
+[kimi unavailable]
diff --git a/verification/tk11372/dtd/muse.txt b/verification/tk11372/dtd/muse.txt
new file mode 100644
index 00000000..3a84ae18
--- /dev/null
+++ b/verification/tk11372/dtd/muse.txt
@@ -0,0 +1 @@
+[muse unavailable]
diff --git a/verification/tk11372/dtd/question.txt b/verification/tk11372/dtd/question.txt
new file mode 100644
index 00000000..c5ef5014
--- /dev/null
+++ b/verification/tk11372/dtd/question.txt
@@ -0,0 +1 @@
+Choose A: extend isolated async probe candidate tests for hung ps, jlist, sessions and recovery before gated production adoption; B: request restart now with missing coverage; C: close based on existing nine isolated tests. Live healthz hangs but sole cause unproven. No production approval. Pick exactly one option. Begin with VERDICT: <option>. No tools requested.
\ No newline at end of file
diff --git a/verification/tk11372/dtd/qwen.txt b/verification/tk11372/dtd/qwen.txt
new file mode 100644
index 00000000..1083ee38
--- /dev/null
+++ b/verification/tk11372/dtd/qwen.txt
@@ -0,0 +1 @@
+VERDICT: A
diff --git a/verification/tk11372/e2e-proof.json b/verification/tk11372/e2e-proof.json
new file mode 100644
index 00000000..b06e5a2a
--- /dev/null
+++ b/verification/tk11372/e2e-proof.json
@@ -0,0 +1,74 @@
+{
+  "intent": "Keep HTTP health/auth/ticket/status reads responsive during process-discovery hangs and recover without leaking the direct probe child",
+  "risk": "R1 isolated candidate; production activation R4 remains gated",
+  "timestamp": "2026-09-11T15:19:20.631499+00:00",
+  "candidate_sha256": "57baae6e06577798fa003588c37611c529f3a0bb14a0e6ce1a4fdff40b7bfe2b",
+  "live_source_sha256": "eaff74765854acbbf59a3790e1529c9a91ba2109575367c7dca84efdd486f524",
+  "patch_sha256": "1daf0642dc325b4da53bd37924df0336fe8df64187aa1df48bb56198781d4438",
+  "environment": "Disposable HOME, PM2_HOME, TICKET_DATA_DIR; loopback random ports; executable fake lsof/ps/jlist; real server and ticket reducer. Configured 4s/22s timeouts unchanged. No real PM2 command executed.",
+  "commands": [
+    "STAGES=sessions EXPECT_SESSION_LEAK=1 RESULT_FILE=verification/tk11372/session-defect.json node verification/tk11372/stages.cjs",
+    "node verification/tk11372/stages.cjs",
+    "node verification/tk11372/api-original.cjs"
+  ],
+  "assertions": [
+    {
+      "name": "Original candidate session child survival reproduced",
+      "verdict": "PASS",
+      "evidence": "session-defect.json"
+    },
+    {
+      "name": "All four bounded hang/auth/HTTP/cache/singleflight/direct-child cleanup/recovery flows",
+      "verdict": "PASS",
+      "evidence": "stage-results.json"
+    },
+    {
+      "name": "Nine original HTTP cases, missing daemon no-jlist and bad JSON fallback included",
+      "verdict": "PASS",
+      "evidence": "original-results.json"
+    },
+    {
+      "name": "Live server.js unchanged from baseline",
+      "verdict": "PASS"
+    },
+    {
+      "name": "Sole live causality and approved activation with live recovery",
+      "verdict": "SKIP",
+      "reason": "Production approval absent; live listener changed overnight without this agent intervening. Old PID stack sample unavailable."
+    },
+    {
+      "name": "Final production healthz",
+      "verdict": "FAIL",
+      "reason": "curl28 HTTP000 zero bytes after8.006033s; live operational outcome still blocked"
+    },
+    {
+      "name": "Candidate syntax, patch applicability, whitespace",
+      "verdict": "PASS",
+      "commands": [
+        "node --check verification/tk11372/server.candidate.cjs",
+        "git apply --check verification/tk11372/candidate.patch",
+        "git diff --check"
+      ]
+    }
+  ],
+  "cleanup": "Test servers and tracked fixture probes terminated; temporary fixture data retained as evidence. Canonical ticket events unaffected by tests. Only requested tk logs/ownership/preferences changed.",
+  "limitations": [
+    "Tests kill the direct jlist wrapper fixture; real PM2-wrapper descendant cleanup is not established.",
+    "Existing synchronous ticket reducer remains outside this patch.",
+    "Session timeout preserves last count while PM2 data timestamp advances; individual session freshness is not represented."
+  ],
+  "DTD": {
+    "valid_votes": 2,
+    "votes": {
+      "Codex": "A",
+      "Qwen": "A",
+      "Claude": "abstain zero-cost mode",
+      "Grok": "unavailable",
+      "Kimi": "unavailable",
+      "Muse": "unavailable"
+    },
+    "post_decision": "KEEP",
+    "evidence": "dtd/"
+  },
+  "verdict": "PARTIAL: isolated candidate verified, live activation gated"
+}
diff --git a/verification/tk11372/server.baseline.cjs b/verification/tk11372/server.baseline.cjs
new file mode 100644
index 00000000..b29f5bc2
--- /dev/null
+++ b/verification/tk11372/server.baseline.cjs
@@ -0,0 +1,467 @@
+// Ticket board viewer — kanban over the shared ticket store. :9794, basic-auth admin/DW2024!, open /healthz.
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+const { exec, execFile, spawn } = require('child_process');
+const { tickets, STATUSES, messages, resolveId, append, withLock, IDRE, REFRE, resolveList } = require('./lib.js');
+
+// ── ticket-run + DTD wiring (TK-10527) ──
+const DATA_DIR = path.join(os.homedir(), '.claude', 'tickets');
+const VERDICTS = path.join(DATA_DIR, 'dtd-verdicts.json');       // last batched dtd run-now verdicts
+const DTD_RUNNING = path.join(DATA_DIR, 'dtd-verdicts.running'); // present while a sweep is in flight
+const RUN_SH = path.join(__dirname, 'run-ticket.sh');           // opens an iTerm2 Claude session
+const DTD_RUN = path.join(__dirname, 'dtd-run.js');             // batched panel.sh sweep
+const RUN_PROFILES = new Set(['claude-sonnet', 'claude-opus', 'claude-haiku', 'claude-opus-5', 'claude-sonnet-5', 'claude-fable', 'codex', 'codex-gpt6', 'codex-gpt52', 'local-qwen-27b', 'local-qwen-14b']);
+const DEFAULT_RUN_PROFILE = 'codex';
+const RUN_PROFILE_OVERRIDE = '/tmp/ticket-run-profile-override.json';
+
+// A bounded operator override wins over stale browser localStorage. The file is
+// intentionally self-expiring, so no cleanup job is required to restore the
+// normal default after a short Codex-only launch window.
+function effectiveRunProfile(requested) {
+  try {
+    const override = JSON.parse(fs.readFileSync(RUN_PROFILE_OVERRIDE, 'utf8'));
+    if (RUN_PROFILES.has(override.profile) && Date.parse(override.until) > Date.now()) return override.profile;
+  } catch {}
+  return String(requested || DEFAULT_RUN_PROFILE);
+}
+// IDRE / REFRE / resolveList now live in ./lib.js (co-located with resolveId).
+const json = (res, code, obj) => { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };
+function readJson(req, cb) { let b = ''; req.on('data', d => { b += d; if (b.length > 1e6) req.destroy(); }); req.on('end', () => { try { cb(JSON.parse(b || '{}')); } catch { cb(null); } }); }
+
+// ── live "running" signal: online pm2 processes + live claude CLI sessions ──
+// Cached 5s so N polling browsers don't each spawn a `pm2 jlist` on a busy box.
+// TK-10970 daemon-fracture guard: a bare `pm2 jlist` FORKS a new "God" daemon when the rpc.sock is
+// transiently unreachable (not only when pm2 is absent). This always-up board must not be a source
+// of socket-less orphan daemons, so we only jlist when the rpc.sock exists AND a live God daemon
+// holds it; otherwise return the cached/empty pm2 list (never fork).
+const PM2_HOME_TS = process.env.PM2_HOME || require('path').join(require('os').homedir(), '.pm2');
+function pm2DaemonReachable() {
+  try {
+    const rpc = require('path').join(PM2_HOME_TS, 'rpc.sock');
+    if (!require('fs').existsSync(rpc)) return false;
+    const { execSync } = require('child_process');
+    const held = execSync(`lsof -nP ${JSON.stringify(rpc)} 2>/dev/null`, { timeout: 4000 }).toString().trim();
+    if (!held) return false;
+    const ps = execSync('ps ax -o pid,command 2>/dev/null', { maxBuffer: 8 * 1024 * 1024, timeout: 4000 }).toString();
+    return ps.split('\n').some(l => /PM2 v[\d.]+: God Daemon/.test(l) && l.includes(PM2_HOME_TS));
+  } catch (_) { return false; }
+}
+let runCache = { ts: 0, data: { pm2: [], sessions: 0, at: null } };
+const PM2_SERIALIZED_TS = path.join(os.homedir(), '.claude', 'skills', 'keep-alive', 'proposals', 'TK-10970', 'pm2-serialized.js');
+function getRunning(cb) {
+  if (Date.now() - runCache.ts < 5000) return cb(runCache.data);
+  if (!pm2DaemonReachable()) {
+    // socket not reachable — do NOT fork a daemon; serve last-known (or empty) pm2 list
+    runCache = { ts: Date.now(), data: { pm2: runCache.data.pm2 || [], sessions: runCache.data.sessions || 0, at: new Date().toISOString() } };
+    return cb(runCache.data);
+  }
+  execFile(process.execPath, [PM2_SERIALIZED_TS, 'jlist'], { maxBuffer: 16 * 1024 * 1024, timeout: 22000, killSignal: 'SIGKILL' }, (e, out) => {
+      let pm2 = [];
+      if (!e) { try {
+        pm2 = JSON.parse(out).filter(p => p.pm2_env && p.pm2_env.status === 'online')
+          .map(p => ({ name: p.name, cpu: (p.monit && p.monit.cpu) || 0,
+            mem: Math.round(((p.monit && p.monit.memory) || 0) / 1048576),
+            up: p.pm2_env.pm_uptime || 0, restarts: p.pm2_env.restart_time || 0 }))
+          .sort((a, b) => a.name < b.name ? -1 : 1);
+      } catch (_) {} }
+      // count live `claude` CLI sessions (exclude skills-dir helpers), best-effort
+      exec("ps -Ao command | grep '[c]laude' | grep -v 'skills/' | wc -l", { timeout: 4000 }, (e2, out2) => {
+        const sessions = e2 ? 0 : (parseInt(String(out2).trim(), 10) || 0);
+        runCache = { ts: Date.now(), data: { pm2, sessions, at: new Date().toISOString() } };
+        cb(runCache.data);
+      });
+  });
+}
+
+const OFFICE_HTML = path.join(__dirname, 'office.html');
+const BOARD_HTML = path.join(__dirname, 'board.html');
+const SKILL_ROOTS = [
+  path.join(os.homedir(), '.agents', 'skills'),
+  path.join(os.homedir(), '.codex', 'skills'),
+];
+
+function installedSkills() {
+  const found = new Map();
+  for (const root of SKILL_ROOTS) {
+    let names = []; try { names = fs.readdirSync(root); } catch { continue; }
+    for (const dir of names) {
+      const file = path.join(root, dir, 'SKILL.md');
+      let body, st; try { body = fs.readFileSync(file, 'utf8'); st = fs.statSync(file); } catch { continue; }
+      const fm = body.match(/^---\s*\n([\s\S]*?)\n---/);
+      const meta = fm ? fm[1] : '';
+      const name = (meta.match(/^name:\s*["']?(.+?)["']?\s*$/m) || [])[1] || dir;
+      const rawDesc = (meta.match(/^description:\s*[>|-]?\s*["']?(.+?)["']?\s*$/m) || [])[1] || '';
+      const key = String(name).trim().toLowerCase();
+      if (!found.has(key)) found.set(key, {
+        name: String(name).trim(), slug: dir, description: String(rawDesc).trim(),
+        root: root.includes('.agents') ? 'agents' : 'codex', path: file,
+        created_at: (st.birthtime || st.mtime).toISOString(), updated_at: st.mtime.toISOString(),
+      });
+    }
+  }
+  return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
+}
+
+function ticketAgents() {
+  const map = new Map();
+  const touch = (name, ts, role, ticket) => {
+    if (!name) return;
+    let a = map.get(name); if (!a) a = { name, assigned: 0, actions: 0, comments: 0, tickets: new Set(), first_at: ts, last_at: ts };
+    a.tickets.add(ticket.id); if (role === 'assigned') a.assigned++; else a[role]++;
+    if (ts && (!a.first_at || ts < a.first_at)) a.first_at = ts;
+    if (ts && (!a.last_at || ts > a.last_at)) a.last_at = ts;
+    map.set(name, a);
+  };
+  for (const t of tickets().values()) {
+    touch(t.assignee, t.updated_at || t.created_at, 'assigned', t);
+    for (const a of (t.actions || [])) touch(a.agent, a.ts, 'actions', t);
+    for (const c of (t.comments || [])) touch(c.agent, c.ts, 'comments', t);
+  }
+  return [...map.values()].map(a => ({ ...a, tickets: a.tickets.size, created_at: a.first_at, updated_at: a.last_at }))
+    .sort((a, b) => b.tickets - a.tickets || a.name.localeCompare(b.name));
+}
+
+const PORT = process.env.PORT || 9794;
+const AUTH = 'Basic ' + Buffer.from(process.env.TK_AUTH || 'admin:DW2024!').toString('base64');
+
+// ── brute-force lockout (interim hardening while CF Zero Trust Access is pending) ──
+// The board is now PUBLICLY exposed via the dedicated `tickets` tunnel and its
+// authenticated endpoints spawn Claude sessions (/api/run) — a lockout-less shared
+// Basic cred on the open internet is dictionary-attackable at line speed. Track failed
+// auths per client IP; after FAIL_MAX inside FAIL_WINDOW_MS, that IP is 429'd for LOCK_MS.
+// A correct auth clears the record. In-memory only, pruned so the map can't grow.
+// Loopback (direct 127.0.0.1, not tunnel-proxied) is exempt so local use never locks.
+// Ported from ~/Projects/dw-pitch-followup/server.js. Reversible: delete this block +
+// restore the plain auth check below.
+const authFails = new Map(); // ip -> { count, first, until }
+const FAIL_MAX = 10, FAIL_WINDOW_MS = 15 * 60 * 1000, LOCK_MS = 15 * 60 * 1000;
+const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
+function clientIp(req) {
+  return req.headers['cf-connecting-ip']
+    || (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
+    || (req.socket && req.socket.remoteAddress) || 'unknown';
+}
+// Returns null if the request may proceed to the auth check, or a {code,msg} to reject.
+function lockoutGate(req) {
+  const remote = req.socket && req.socket.remoteAddress;
+  const proxied = req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'];
+  if (LOOPBACK.has(remote) && !proxied) return null;      // direct local access — never locked
+  const ip = clientIp(req), now = Date.now();
+  const rec = authFails.get(ip);
+  if (rec && rec.until && now < rec.until) return { code: 429, msg: 'too many failed attempts — try again later', retry: Math.ceil((rec.until - now) / 1000) };
+  return null;
+}
+function noteAuth(req, ok) {
+  const remote = req.socket && req.socket.remoteAddress;
+  const proxied = req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'];
+  if (LOOPBACK.has(remote) && !proxied) return;
+  const ip = clientIp(req), now = Date.now();
+  if (ok) { authFails.delete(ip); return; }
+  let rec = authFails.get(ip);
+  if (!rec || (now - rec.first) > FAIL_WINDOW_MS) rec = { count: 0, first: now, until: 0 };
+  rec.count++;
+  if (rec.count >= FAIL_MAX) rec.until = now + LOCK_MS;
+  authFails.set(ip, rec);
+  // prune only truly-inactive records: expired locks OR window-stale (count would reset anyway).
+  // NOT active in-window records (until==0, count<MAX) — deleting those resets a live attacker's count.
+  if (authFails.size > 5000) for (const [k, v] of authFails) if ((v.until && now > v.until) || (now - v.first) > FAIL_WINDOW_MS) authFails.delete(k);
+}
+// Claude-spawning endpoints (/api/run, /api/dtd) must run from a LOCAL operator only.
+// Over the public `tickets` tunnel these would let a remote actor (past the shared cred)
+// spawn Claude sessions on this Mac — Cody Hole 2. Viewing stays public; execution stays
+// local. Reversible: delete this guard's calls to re-open remote exec (do that behind CF
+// Access, not the bare tunnel). Set TK_ALLOW_TUNNEL_EXEC=1 to override (mirrors the
+// dw-pitch-followup ALLOW_TUNNEL_SEND escape hatch).
+const ALLOW_TUNNEL_EXEC = (process.env.TK_ALLOW_TUNNEL_EXEC || '') === '1';
+function execBlockedForRemote(req, res) {
+  if (ALLOW_TUNNEL_EXEC) return false;
+  const remote = req.socket && req.socket.remoteAddress;
+  const proxied = req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'];
+  if (LOOPBACK.has(remote) && !proxied) return false;    // local operator — allowed
+  json(res, 403, { error: 'execution endpoints are local-operator only over the public tunnel; run locally or enable behind CF Access' });
+  return true;
+}
+
+const esc = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
+
+// ── priority ranking + per-ticket ratings (mirrors the :9801 approvals viewer scoreGate, TK-10695) ──
+// For each OPEN/DOING/BLOCKED ticket compute 0-5 ratings + a composite priority + tier.
+// Same weighting as gated-queue-runner/server.js scoreGate: value*2.4 + urgency*2.4 + ease*1.0 + safety*0.4.
+function ticketText(t) {
+  // full corpus we score over: title + every comment + every action
+  return [t.title, ...(t.comments || []).map(c => c.text), ...(t.actions || []).map(a => a.text)].join('\n');
+}
+function nearestDeadlineDays(body) {
+  const now = Date.now(); let best = null;
+  const iso = body.match(/\b20\d\d-\d\d-\d\d\b/g) || [];
+  const named = body.match(/\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2},?\s+20\d\d\b/gi) || [];
+  for (const s of [...iso, ...named]) {
+    const t = Date.parse(s); if (isNaN(t)) continue;
+    const d = Math.round((t - now) / 86400000);
+    if (d >= -3 && (best === null || d < best)) best = d;
+  }
+  if (/\b(today|due today|expires? today)\b/i.test(body)) best = best === null ? 0 : Math.min(best, 0);
+  return best;
+}
+function parseMaxDollars(body) {
+  let max = 0;
+  for (const m of body.matchAll(/\$\s?([\d,]+(?:\.\d+)?)\s*([kKmM])?/g)) {
+    let n = parseFloat(m[1].replace(/,/g, '')); if (isNaN(n)) continue;
+    if (m[2] && /[kK]/.test(m[2])) n *= 1e3; if (m[2] && /[mM]/.test(m[2])) n *= 1e6;
+    if (n > max) max = n;
+  }
+  return max;
+}
+function lastActivityMs(t) {
+  let ms = +new Date(t.updated_at || t.created_at || 0);
+  for (const a of (t.actions || [])) ms = Math.max(ms, +new Date(a.ts));
+  for (const c of (t.comments || [])) ms = Math.max(ms, +new Date(c.ts));
+  return ms || Date.now();
+}
+function scoreTicket(t) {
+  const b = ticketText(t);
+  const now = Date.now();
+  const money = parseMaxDollars(b);
+  const days = nearestDeadlineDays(b);
+  const idleH = (now - lastActivityMs(t)) / 3600000;                 // recency of last activity (stale = higher need)
+  const ageDays = (now - +new Date(t.created_at || now)) / 86400000; // how long it's been open
+
+  // 💰 value/impact: dollars (log) OR project/keyword stakes, whichever higher
+  let value = money >= 1e5 ? 5 : money >= 1e4 ? 4 : money >= 1e3 ? 3 : money >= 100 ? 2 : money > 0 ? 1 : 0;
+  value = Math.max(value, 2); // every open ticket has baseline stakes
+  if (/\b(revenue|prod|production|customer|customer-facing|live|go-live|launch|ship|deploy)\b/i.test(b)) value = Math.min(5, value + 1);
+  if (/\b(urgent|critical|broken|down|502|500|incident|lapse|expir|money (owed|left)|five[- ]figure)\b/i.test(b)) value = Math.min(5, value + 1);
+
+  // ⏰ urgency: deadline + staleness + age + status weighting + urgent words
+  let urgency = 1;
+  if (days !== null) urgency = days <= 1 ? 5 : days <= 3 ? 4 : days <= 7 ? 3 : days <= 30 ? 2 : 1;
+  if (idleH > 72) urgency = Math.max(urgency, 4);           // stale >3d = high need
+  else if (idleH > 24) urgency = Math.max(urgency, 3);      // stale >1d
+  if (ageDays > 14) urgency = Math.min(5, urgency + 1);     // long-open drags priority up
+  if (t.status === 'blocked') urgency = Math.min(5, urgency + 2); // blocked screams for attention
+  else if (t.status === 'doing') urgency = Math.min(5, urgency + 1);
+  if (/\b(urgent|asap|lapsing|due today|deadline|expires? (today|tomorrow)|now|immediately|incident)\b/i.test(b)) urgency = Math.min(5, urgency + 1);
+  urgency = Math.max(1, Math.min(5, urgency));
+
+  // ⚡ ease (higher = quicker / lower-friction to complete)
+  let ease = 3;
+  const wc = b.split(/\s+/).length;
+  if (/\b(reversible|one[- ]click|1[- ]click|toggle|paste|quick|small|single|read-only|one[- ]line|tweak|typo)\b/i.test(b)) ease += 1;
+  if (/\b(build|migration|scrape|onboard|rebuild|multi-part|multi-step|large|backfill|thousands|batch|refactor|overhaul|end-to-end|pipeline)\b/i.test(b)) ease -= 1;
+  if ((t.actions || []).length + (t.comments || []).length <= 1 && wc < 40) ease += 1; // short/single-action ticket
+  if (wc > 400) ease -= 1;
+  ease = Math.max(1, Math.min(5, ease));
+
+  // ✅ safety/confidence (higher = safer / more reversible)
+  let safety = 3;
+  if (/\b(reversible|restore[- ]map|verified|snapshot|dry[- ]?run|git revert|rollback|local|read-only|additive)\b/i.test(b)) safety += 1;
+  if (/\b(destructive|irreversible|delete|purge|drop |wipe|history rewrite|filter-repo|unpublish|cannot be undone|force[- ]push|prod deploy|dns|spend|send-to-list)\b/i.test(b)) safety -= 2;
+  safety = Math.max(1, Math.min(5, safety));
+
+  // composite: value + urgency dominate; ease nudges; low safety slightly demotes
+  const priority = Math.round((value * 2.4 + urgency * 2.4 + ease * 1.0 + safety * 0.4) * 10) / 10;
+  const tier = priority >= 26 ? 'high' : priority >= 18 ? 'med' : 'low';
+  return { value, urgency, ease, safety, priority, tier, money: money || 0, days };
+}
+// Attach ranking/ratings to a flat ticket list. Only open/doing/blocked get ranked
+// (done/stopped are excluded from the ranking per the brief) — those get priority 0 / no rank.
+function withRanking(list) {
+  const RANKABLE = new Set(['open', 'doing', 'blocked']);
+  const scored = [];
+  for (const t of list) {
+    if ((t.kind || 'task') === 'task' && RANKABLE.has(t.status)) {
+      const s = scoreTicket(t);
+      t.priority = s.priority; t.tier = s.tier;
+      t.ratings = { value: s.value, urgency: s.urgency, ease: s.ease, safety: s.safety };
+      t.money = s.money; t.deadlineDays = s.days;
+      scored.push(t);
+    } else {
+      t.priority = 0; t.tier = null; t.rank = null;
+      t.ratings = { value: 0, urgency: 0, ease: 0, safety: 0 };
+      t.money = 0; t.deadlineDays = null;
+    }
+  }
+  scored.sort((a, b) => b.priority - a.priority || (+new Date(b.updated_at) - +new Date(a.updated_at)));
+  scored.forEach((t, i) => { t.rank = i + 1; });
+  return list;
+}
+
+function page() {
+  const cols = { open: [], doing: [], blocked: [], done: [], stopped: [] };
+  for (const t of tickets().values()) (cols[t.status] || (cols[t.status] = [])).push(t);
+  for (const k of STATUSES) cols[k].sort((a, b) => a.updated_at < b.updated_at ? 1 : -1);
+  cols.done = cols.done.slice(0, 40);
+  cols.stopped = cols.stopped.slice(0, 40);
+  const card = t => `<div class="card" onclick="this.classList.toggle('x')">
+    <div class="cid">${t.id}${t.project ? `<span class="proj">${esc(t.project)}</span>` : ''}</div>
+    <div class="ttl">${esc(t.title)}</div>
+    <div class="meta"><span class="who">${esc(t.assignee || 'unassigned')}</span>
+      <span class="when" title="${esc(t.created_at)}">🕓 ${new Date(t.created_at).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}</span></div>
+    <div class="thread">${t.comments.map(c => `<div class="c k-${c.kind}"><b>${esc(c.agent)}</b> <i>${esc(c.kind)}</i> ${esc(c.text)}<span class="cts">${new Date(c.ts).toLocaleString()}</span></div>`).join('')}
+      ${t.actions.map(a => `<div class="c k-action"><b>${esc(a.agent)}</b> <i>action</i> ${esc(a.text)}<span class="cts">${new Date(a.ts).toLocaleString()}</span></div>`).join('') || ''}
+      ${!t.comments.length && !t.actions.length ? '<div class="c none">no comments yet</div>' : ''}</div></div>`;
+  // ── Direct-message conversations, grouped into threads ──
+  const mm = messages();
+  const rootOf = mid => { let id = mid, c = mm.get(mid); const seen = new Set([id]); while (c && c.re && mm.get(c.re)) { if (seen.has(c.re)) break; seen.add(c.re); id = c.re; c = mm.get(id); } return id; };
+  const threads = new Map();
+  for (const m of mm.values()) { const r = rootOf(m.mid); (threads.get(r) || threads.set(r, []).get(r)).push(m); }
+  const convos = [...threads.values()].map(ms => ms.sort((a, b) => a.ts < b.ts ? -1 : 1))
+    .sort((a, b) => a[a.length - 1].ts < b[b.length - 1].ts ? 1 : -1).slice(0, 40);
+  const dmLine = m => `<div class="dm"><b>${esc(m.from)}</b> <span class="arw">→</span> <b>${esc(m.to)}</b>${m.ticket ? `<span class="dtk">${esc(m.ticket.replace(/^(TK-\d+).*/, '$1'))}</span>` : ''}
+    <span class="dts">${new Date(m.ts).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}</span>
+    <div class="dtx">${esc(m.text)}</div></div>`;
+  const convoCard = ms => `<div class="convo"><div class="chd">${esc([...new Set(ms.flatMap(m => [m.from, m.to]))].filter(Boolean).join(' ⇄ '))}<span class="cnt">${ms.length} msg${ms.length > 1 ? 's' : ''}</span></div>${ms.map(dmLine).join('')}</div>`;
+  const dmPanel = `<details class="dms" open><summary>DIRECT MESSAGES <small>${mm.size} total · ${convos.length} conversations · agents talk via <code>tk dm / reply / @mention</code></small></summary>
+    <div class="convos">${convos.length ? convos.map(convoCard).join('') : '<div class="c none">no direct messages yet</div>'}</div></details>`;
+  return `<!doctype html><meta charset="utf-8"><title>Fleet Tickets</title><meta http-equiv="refresh" content="30">
+<style>
+  body{margin:0;font:14px -apple-system,sans-serif;background:#0f1115;color:#e6e6e6}
+  h1{font-size:16px;margin:0;padding:14px 18px;border-bottom:1px solid #262a33;letter-spacing:.06em}
+  h1 small{color:#8a93a5;font-weight:400;margin-left:10px}
+  .board{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;padding:14px;align-items:start}
+  .col h2{font-size:12px;text-transform:uppercase;letter-spacing:.1em;color:#8a93a5;margin:4px 2px 8px}
+  .card{background:#171b22;border:1px solid #262a33;border-radius:8px;padding:10px 12px;margin-bottom:8px;cursor:pointer}
+  .cid{font-weight:600;color:#6db3f2;font-size:12px}.proj{float:right;color:#8a93a5;font-weight:400}
+  .ttl{margin:4px 0 6px}
+  .meta{display:flex;justify-content:space-between;font-size:11px;color:#8a93a5}
+  .when{white-space:nowrap}
+  .thread{display:none;margin-top:8px;border-top:1px dashed #2c3140;padding-top:6px}
+  .card.x .thread{display:block}
+  .c{font-size:12px;margin:4px 0;color:#c6ccd8}.c i{color:#8a93a5;font-style:normal;font-size:10px;margin:0 4px}
+  .c.k-note{color:#e8d48b}.c.k-action{color:#8fd49a}.c.none{color:#5b6270}
+  .c.k-win{color:#34d399}.c.k-challenge{color:#e06c75}.c.k-cody{color:#f59e0b}
+  .cts{display:block;font-size:10px;color:#5b6270}
+  .col-doing .card{border-left:3px solid #6db3f2}.col-blocked .card{border-left:3px solid #e06c75}.col-done .card{opacity:.55}
+  .col-stopped .card{opacity:.4;border-left:3px solid #6b7280}.col-stopped h2{color:#9aa2b1}
+  .dms{margin:0 14px 6px;background:#141821;border:1px solid #262a33;border-radius:8px}
+  .dms>summary{cursor:pointer;padding:10px 14px;font-size:12px;text-transform:uppercase;letter-spacing:.1em;color:#c9a4f2}
+  .dms>summary small{text-transform:none;letter-spacing:0;color:#8a93a5;margin-left:8px;font-size:11px}
+  .dms code{color:#c9a4f2;background:#1c2130;padding:1px 4px;border-radius:4px}
+  .convos{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:10px;padding:4px 14px 14px}
+  .convo{background:#171b22;border:1px solid #262a33;border-left:3px solid #a06ef2;border-radius:8px;padding:8px 10px}
+  .chd{font-size:11px;color:#c9a4f2;font-weight:600;margin-bottom:6px}.chd .cnt{float:right;color:#5b6270;font-weight:400}
+  .dm{font-size:12px;margin:5px 0;padding-top:5px;border-top:1px dashed #2c3140}.dm:first-of-type{border-top:0}
+  .dm .arw{color:#8a93a5;margin:0 3px}.dm b{color:#cdd4e0}
+  .dm .dtk{color:#6db3f2;font-size:10px;margin-left:6px}.dm .dts{float:right;color:#5b6270;font-size:10px}
+  .dm .dtx{color:#c6ccd8;margin-top:2px}
+</style>
+<h1>FLEET TICKETS<small>every agent action rides a ticket — tk new / comment / note / log / dm / inbox / reply / @mention / take / done</small><a href="/office" style="float:right;color:#c9a4f2;text-decoration:none;font-size:13px;border:1px solid #2c3140;padding:4px 10px;border-radius:6px">🏢 3D Office →</a></h1>
+${dmPanel}
+<div class="board">${STATUSES.map(s => `<div class="col col-${s}"><h2>${s} (${cols[s].length})</h2>${cols[s].map(card).join('') || '<div class="c none">empty</div>'}</div>`).join('')}</div>`;
+}
+
+http.createServer((req, res) => {
+  if (req.url === '/healthz') { res.writeHead(200); return res.end('ok'); }
+  const locked = lockoutGate(req);
+  if (locked) { res.writeHead(locked.code, { 'Retry-After': String(locked.retry) }); return res.end(locked.msg); }
+  if (req.headers.authorization !== AUTH) { noteAuth(req, false); res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="tickets"' }); return res.end('auth'); }
+  noteAuth(req, true);
+
+  // ── writes / actions (all auth-gated; ids resolved server-side from the real store) ──
+
+  // Run Now — open one iTerm2 Claude session per selected ticket (staggered so iTerm doesn't drop windows).
+  if (req.method === 'POST' && req.url === '/api/run') {
+    if (execBlockedForRemote(req, res)) return;
+    return readJson(req, body => {
+      if (!body) return json(res, 400, { error: 'bad json' });
+      const profile = effectiveRunProfile(body.profile);
+      if (!RUN_PROFILES.has(profile)) return json(res, 400, { error: 'invalid run profile' });
+      const map = tickets(); const ids = resolveList(body.ids, map);
+      const launched = [], skipped = [];
+      ids.forEach((id, i) => {
+        const t = map.get(id);
+        if (t && t.status === 'stopped') { skipped.push({ id, why: 'stopped' }); return; }
+        if (t && t.status === 'doing') { skipped.push({ id, why: 'already-doing' }); return; }
+        if (t && (t.kind || 'task') !== 'task') { skipped.push({ id, why: 'designation-' + t.kind }); return; }
+        let cwd = os.homedir();
+        const proj = t && t.project;
+        if (proj && /^[a-z0-9._-]+$/i.test(proj)) { const p = path.join(os.homedir(), 'Projects', proj); if (fs.existsSync(p)) cwd = p; }
+        setTimeout(() => execFile('bash', [RUN_SH, id, cwd, profile], { timeout: 25000 }, (err) => {
+          // Log the REAL outcome from the launch callback — never an optimistic "launched" before the window opens.
+          // If osascript/iTerm fails (app quit, Automation permission denied), surface it on the board instead of a false success.
+          withLock(() => append({ ts: new Date().toISOString(), type: 'action', id, agent: 'board',
+            text: err ? ('⚠ RUN NOW failed to launch iTerm2 session — ' + String(err.message || err).split('\n')[0])
+                      : `▶ RUN NOW — launched iTerm2 session from the board · profile=${profile}` }));
+        }), i * 1300); // stagger ~1.3s
+        launched.push(id);
+      });
+      json(res, 200, { launched, skipped, profile });
+    });
+  }
+  // Stop Forever — mark selected tickets TicketStopped (status 'stopped'); reversible via /api/reopen.
+  if (req.method === 'POST' && req.url === '/api/stop') {
+    return readJson(req, body => {
+      if (!body) return json(res, 400, { error: 'bad json' });
+      const map = tickets(); const ids = resolveList(body.ids, map);
+      withLock(() => { for (const id of ids) {
+        append({ ts: new Date().toISOString(), type: 'status', id, status: 'stopped', agent: 'board' });
+        append({ ts: new Date().toISOString(), type: 'action', id, agent: 'board', text: '⛔ TicketStopped — stopped forever from the board' });
+      } });
+      json(res, 200, { stopped: ids });
+    });
+  }
+  // Reopen — un-stop (or un-close) selected tickets back to 'open'.
+  if (req.method === 'POST' && req.url === '/api/reopen') {
+    return readJson(req, body => {
+      if (!body) return json(res, 400, { error: 'bad json' });
+      const map = tickets(); const ids = resolveList(body.ids, map);
+      withLock(() => { for (const id of ids) {
+        append({ ts: new Date().toISOString(), type: 'status', id, status: 'open', agent: 'board' });
+        append({ ts: new Date().toISOString(), type: 'action', id, agent: 'board', text: '↩ reopened from the board' });
+      } });
+      json(res, 200, { reopened: ids });
+    });
+  }
+  // DTD — trigger a batched run-now sweep (POST) / read the latest verdicts + running state (GET).
+  if (req.method === 'POST' && req.url === '/api/dtd') {
+    if (execBlockedForRemote(req, res)) return;
+    return readJson(req, body => {
+      if (fs.existsSync(DTD_RUNNING)) return json(res, 200, { started: false, already: true });
+      const args = [DTD_RUN];
+      const explicit = body && Array.isArray(body.ids) && body.ids.length ? resolveList(body.ids, tickets()) : [];
+      if (explicit.length) args.push(...explicit); else args.push('--recent');
+      try { const child = spawn(process.execPath, args, { detached: true, stdio: 'ignore', cwd: __dirname }); child.unref(); }
+      catch (e) { return json(res, 500, { started: false, error: e.message }); }
+      json(res, 200, { started: true, ids: explicit.length ? explicit : 'recent' });
+    });
+  }
+  if (req.method === 'GET' && req.url === '/api/dtd') {
+    let verdicts = null, running = false, runningInfo = null;
+    try { verdicts = JSON.parse(fs.readFileSync(VERDICTS, 'utf8')); } catch {}
+    try { runningInfo = JSON.parse(fs.readFileSync(DTD_RUNNING, 'utf8')); running = true; } catch {}
+    return json(res, 200, { running, runningInfo, verdicts });
+  }
+
+  // shared nav-agent drop-in (grid-controls standard) — static assets
+  if (req.url === '/nav-agent/nav-agent.js' || req.url === '/nav-agent/nav-agent.css') {
+    const file = path.join(__dirname, req.url.replace(/^\//, ''));
+    const type = req.url.endsWith('.css') ? 'text/css' : 'application/javascript';
+    return fs.readFile(file, (e, buf) => {
+      if (e) { res.writeHead(404); return res.end('nav-agent asset missing'); }
+      res.writeHead(200, { 'Content-Type': type + '; charset=utf-8', 'Cache-Control': 'no-store' }); res.end(buf);
+    });
+  }
+  if (req.url === '/office' || req.url === '/office.html') {
+    return fs.readFile(OFFICE_HTML, (e, buf) => {
+      if (e) { res.writeHead(500); return res.end('office view missing'); }
+      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }); res.end(buf);
+    });
+  }
+  if (req.url === '/api/running') { return getRunning(d => { res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.end(JSON.stringify(d)); }); }
+  if (req.url === '/api/tickets') { res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); return res.end(JSON.stringify(withRanking([...tickets().values()]))); }
+  if (req.url === '/api/agents') { res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); return res.end(JSON.stringify(ticketAgents())); }
+  if (req.url === '/api/skills') { res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); return res.end(JSON.stringify(installedSkills())); }
+  if (req.url === '/api/messages') { res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); return res.end(JSON.stringify([...messages().values()])); }
+  if (req.url === '/kanban') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }); return res.end(page()); }
+  // default (/) = the adjustable-columns TABLE view (Steve's list-builds rule, 2026-08-10)
+  return fs.readFile(BOARD_HTML, (e, buf) => {
+    if (e) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }); return res.end(page()); } // fall back to kanban
+    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }); res.end(buf);
+  });
+}).listen(PORT, '127.0.0.1', () => {
+  console.log('ticket board on :' + PORT);
+  getRunning(() => {});                          // warm the pm2/sessions cache so first client fetch is instant
+  setInterval(() => getRunning(() => {}), 4500); // keep it warm ahead of the 5s TTL
+});
diff --git a/verification/tk11372/server.candidate.cjs b/verification/tk11372/server.candidate.cjs
new file mode 100644
index 00000000..4ab1e7be
--- /dev/null
+++ b/verification/tk11372/server.candidate.cjs
@@ -0,0 +1,476 @@
+// Ticket board viewer — kanban over the shared ticket store. :9794, basic-auth admin/DW2024!, open /healthz.
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+const { exec, execFile, spawn } = require('child_process');
+const { tickets, STATUSES, messages, resolveId, append, withLock, IDRE, REFRE, resolveList } = require('./lib.js');
+
+// ── ticket-run + DTD wiring (TK-10527) ──
+const DATA_DIR = path.join(os.homedir(), '.claude', 'tickets');
+const VERDICTS = path.join(DATA_DIR, 'dtd-verdicts.json');       // last batched dtd run-now verdicts
+const DTD_RUNNING = path.join(DATA_DIR, 'dtd-verdicts.running'); // present while a sweep is in flight
+const RUN_SH = path.join(__dirname, 'run-ticket.sh');           // opens an iTerm2 Claude session
+const DTD_RUN = path.join(__dirname, 'dtd-run.js');             // batched panel.sh sweep
+const RUN_PROFILES = new Set(['claude-sonnet', 'claude-opus', 'claude-haiku', 'claude-opus-5', 'claude-sonnet-5', 'claude-fable', 'codex', 'codex-gpt6', 'codex-gpt52', 'local-qwen-27b', 'local-qwen-14b']);
+const DEFAULT_RUN_PROFILE = 'codex';
+const RUN_PROFILE_OVERRIDE = '/tmp/ticket-run-profile-override.json';
+
+// A bounded operator override wins over stale browser localStorage. The file is
+// intentionally self-expiring, so no cleanup job is required to restore the
+// normal default after a short Codex-only launch window.
+function effectiveRunProfile(requested) {
+  try {
+    const override = JSON.parse(fs.readFileSync(RUN_PROFILE_OVERRIDE, 'utf8'));
+    if (RUN_PROFILES.has(override.profile) && Date.parse(override.until) > Date.now()) return override.profile;
+  } catch {}
+  return String(requested || DEFAULT_RUN_PROFILE);
+}
+// IDRE / REFRE / resolveList now live in ./lib.js (co-located with resolveId).
+const json = (res, code, obj) => { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };
+function readJson(req, cb) { let b = ''; req.on('data', d => { b += d; if (b.length > 1e6) req.destroy(); }); req.on('end', () => { try { cb(JSON.parse(b || '{}')); } catch { cb(null); } }); }
+
+// ── live "running" signal: online pm2 processes + live claude CLI sessions ──
+// Cached 5s so N polling browsers don't each spawn a `pm2 jlist` on a busy box.
+// TK-10970 daemon-fracture guard: a bare `pm2 jlist` FORKS a new "God" daemon when the rpc.sock is
+// transiently unreachable (not only when pm2 is absent). This always-up board must not be a source
+// of socket-less orphan daemons, so we only jlist when the rpc.sock exists AND a live God daemon
+// holds it; otherwise return the cached/empty pm2 list (never fork).
+const PM2_HOME_TS = process.env.PM2_HOME || require('path').join(require('os').homedir(), '.pm2');
+function pm2DaemonReachable(cb) {
+  const rpc = path.join(PM2_HOME_TS, 'rpc.sock');
+  if (!fs.existsSync(rpc)) return cb(false);
+  const options = { timeout: 4000, killSignal: 'SIGKILL', maxBuffer: 8 * 1024 * 1024 };
+  execFile('lsof', ['-nP', rpc], options, (error, held) => {
+    if (error || !String(held).trim()) return cb(false);
+    execFile('ps', ['ax', '-o', 'pid,command'], options, (error, output) => {
+      cb(!error && String(output).split('\n').some(line => /PM2 v[\d.]+: God Daemon/.test(line) && line.includes(PM2_HOME_TS)));
+    });
+  });
+}
+let runCache = { ts: 0, data: { pm2: [], sessions: 0, at: null } };
+let runRefresh = false;
+const PM2_SERIALIZED_TS = path.join(os.homedir(), '.claude', 'skills', 'keep-alive', 'proposals', 'TK-10970', 'pm2-serialized.js');
+function getRunning(cb) {
+  if (Date.now() - runCache.ts >= 5000 && !runRefresh) {
+    runRefresh = true;
+    const finish = (data = runCache.data) => {
+      runCache = { ts: Date.now(), data };
+      runRefresh = false;
+    };
+    pm2DaemonReachable(reachable => {
+      if (!reachable) return finish();
+      execFile(process.execPath, [PM2_SERIALIZED_TS, 'jlist'], { maxBuffer: 16 * 1024 * 1024, timeout: 22000, killSignal: 'SIGKILL' }, (error, output) => {
+        if (error) return finish();
+        let pm2;
+        try {
+          pm2 = JSON.parse(output).filter(p => p.pm2_env && p.pm2_env.status === 'online')
+            .map(p => ({ name: p.name, cpu: (p.monit && p.monit.cpu) || 0,
+              mem: Math.round(((p.monit && p.monit.memory) || 0) / 1048576),
+              up: p.pm2_env.pm_uptime || 0, restarts: p.pm2_env.restart_time || 0 }))
+            .sort((a, b) => a.name < b.name ? -1 : 1);
+        } catch { return finish(); }
+        // Count in-process so the timeout targets ps itself, not a shell whose
+        // children could survive and keep the refresh pipes open indefinitely.
+        execFile('ps', ['-Ao', 'command'], { timeout: 4000, killSignal: 'SIGKILL', maxBuffer: 8 * 1024 * 1024 }, (error, output) => {
+          const sessions = error ? runCache.data.sessions : String(output).split('\n')
+            .filter(line => line.includes('claude') && !line.includes('skills/')).length;
+          finish({ pm2, sessions, at: new Date().toISOString() });
+        });
+      });
+    });
+  }
+  // Monitoring subprocesses must never delay health, auth, or first-start requests.
+  cb(runCache.data);
+}
+
+const OFFICE_HTML = path.join(__dirname, 'office.html');
+const BOARD_HTML = path.join(__dirname, 'board.html');
+const SKILL_ROOTS = [
+  path.join(os.homedir(), '.agents', 'skills'),
+  path.join(os.homedir(), '.codex', 'skills'),
+];
+
+function installedSkills() {
+  const found = new Map();
+  for (const root of SKILL_ROOTS) {
+    let names = []; try { names = fs.readdirSync(root); } catch { continue; }
+    for (const dir of names) {
+      const file = path.join(root, dir, 'SKILL.md');
+      let body, st; try { body = fs.readFileSync(file, 'utf8'); st = fs.statSync(file); } catch { continue; }
+      const fm = body.match(/^---\s*\n([\s\S]*?)\n---/);
+      const meta = fm ? fm[1] : '';
+      const name = (meta.match(/^name:\s*["']?(.+?)["']?\s*$/m) || [])[1] || dir;
+      const rawDesc = (meta.match(/^description:\s*[>|-]?\s*["']?(.+?)["']?\s*$/m) || [])[1] || '';
+      const key = String(name).trim().toLowerCase();
+      if (!found.has(key)) found.set(key, {
+        name: String(name).trim(), slug: dir, description: String(rawDesc).trim(),
+        root: root.includes('.agents') ? 'agents' : 'codex', path: file,
+        created_at: (st.birthtime || st.mtime).toISOString(), updated_at: st.mtime.toISOString(),
+      });
+    }
+  }
+  return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
+}
+
+function ticketAgents() {
+  const map = new Map();
+  const touch = (name, ts, role, ticket) => {
+    if (!name) return;
+    let a = map.get(name); if (!a) a = { name, assigned: 0, actions: 0, comments: 0, tickets: new Set(), first_at: ts, last_at: ts };
+    a.tickets.add(ticket.id); if (role === 'assigned') a.assigned++; else a[role]++;
+    if (ts && (!a.first_at || ts < a.first_at)) a.first_at = ts;
+    if (ts && (!a.last_at || ts > a.last_at)) a.last_at = ts;
+    map.set(name, a);
+  };
+  for (const t of tickets().values()) {
+    touch(t.assignee, t.updated_at || t.created_at, 'assigned', t);
+    for (const a of (t.actions || [])) touch(a.agent, a.ts, 'actions', t);
+    for (const c of (t.comments || [])) touch(c.agent, c.ts, 'comments', t);
+  }
+  return [...map.values()].map(a => ({ ...a, tickets: a.tickets.size, created_at: a.first_at, updated_at: a.last_at }))
+    .sort((a, b) => b.tickets - a.tickets || a.name.localeCompare(b.name));
+}
+
+const PORT = process.env.PORT || 9794;
+const AUTH = 'Basic ' + Buffer.from(process.env.TK_AUTH || 'admin:DW2024!').toString('base64');
+
+// ── brute-force lockout (interim hardening while CF Zero Trust Access is pending) ──
+// The board is now PUBLICLY exposed via the dedicated `tickets` tunnel and its
+// authenticated endpoints spawn Claude sessions (/api/run) — a lockout-less shared
+// Basic cred on the open internet is dictionary-attackable at line speed. Track failed
+// auths per client IP; after FAIL_MAX inside FAIL_WINDOW_MS, that IP is 429'd for LOCK_MS.
+// A correct auth clears the record. In-memory only, pruned so the map can't grow.
+// Loopback (direct 127.0.0.1, not tunnel-proxied) is exempt so local use never locks.
+// Ported from ~/Projects/dw-pitch-followup/server.js. Reversible: delete this block +
+// restore the plain auth check below.
+const authFails = new Map(); // ip -> { count, first, until }
+const FAIL_MAX = 10, FAIL_WINDOW_MS = 15 * 60 * 1000, LOCK_MS = 15 * 60 * 1000;
+const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
+function clientIp(req) {
+  return req.headers['cf-connecting-ip']
+    || (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
+    || (req.socket && req.socket.remoteAddress) || 'unknown';
+}
+// Returns null if the request may proceed to the auth check, or a {code,msg} to reject.
+function lockoutGate(req) {
+  const remote = req.socket && req.socket.remoteAddress;
+  const proxied = req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'];
+  if (LOOPBACK.has(remote) && !proxied) return null;      // direct local access — never locked
+  const ip = clientIp(req), now = Date.now();
+  const rec = authFails.get(ip);
+  if (rec && rec.until && now < rec.until) return { code: 429, msg: 'too many failed attempts — try again later', retry: Math.ceil((rec.until - now) / 1000) };
+  return null;
+}
+function noteAuth(req, ok) {
+  const remote = req.socket && req.socket.remoteAddress;
+  const proxied = req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'];
+  if (LOOPBACK.has(remote) && !proxied) return;
+  const ip = clientIp(req), now = Date.now();
+  if (ok) { authFails.delete(ip); return; }
+  let rec = authFails.get(ip);
+  if (!rec || (now - rec.first) > FAIL_WINDOW_MS) rec = { count: 0, first: now, until: 0 };
+  rec.count++;
+  if (rec.count >= FAIL_MAX) rec.until = now + LOCK_MS;
+  authFails.set(ip, rec);
+  // prune only truly-inactive records: expired locks OR window-stale (count would reset anyway).
+  // NOT active in-window records (until==0, count<MAX) — deleting those resets a live attacker's count.
+  if (authFails.size > 5000) for (const [k, v] of authFails) if ((v.until && now > v.until) || (now - v.first) > FAIL_WINDOW_MS) authFails.delete(k);
+}
+// Claude-spawning endpoints (/api/run, /api/dtd) must run from a LOCAL operator only.
+// Over the public `tickets` tunnel these would let a remote actor (past the shared cred)
+// spawn Claude sessions on this Mac — Cody Hole 2. Viewing stays public; execution stays
+// local. Reversible: delete this guard's calls to re-open remote exec (do that behind CF
+// Access, not the bare tunnel). Set TK_ALLOW_TUNNEL_EXEC=1 to override (mirrors the
+// dw-pitch-followup ALLOW_TUNNEL_SEND escape hatch).
+const ALLOW_TUNNEL_EXEC = (process.env.TK_ALLOW_TUNNEL_EXEC || '') === '1';
+function execBlockedForRemote(req, res) {
+  if (ALLOW_TUNNEL_EXEC) return false;
+  const remote = req.socket && req.socket.remoteAddress;
+  const proxied = req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'];
+  if (LOOPBACK.has(remote) && !proxied) return false;    // local operator — allowed
+  json(res, 403, { error: 'execution endpoints are local-operator only over the public tunnel; run locally or enable behind CF Access' });
+  return true;
+}
+
+const esc = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
+
+// ── priority ranking + per-ticket ratings (mirrors the :9801 approvals viewer scoreGate, TK-10695) ──
+// For each OPEN/DOING/BLOCKED ticket compute 0-5 ratings + a composite priority + tier.
+// Same weighting as gated-queue-runner/server.js scoreGate: value*2.4 + urgency*2.4 + ease*1.0 + safety*0.4.
+function ticketText(t) {
+  // full corpus we score over: title + every comment + every action
+  return [t.title, ...(t.comments || []).map(c => c.text), ...(t.actions || []).map(a => a.text)].join('\n');
+}
+function nearestDeadlineDays(body) {
+  const now = Date.now(); let best = null;
+  const iso = body.match(/\b20\d\d-\d\d-\d\d\b/g) || [];
+  const named = body.match(/\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2},?\s+20\d\d\b/gi) || [];
+  for (const s of [...iso, ...named]) {
+    const t = Date.parse(s); if (isNaN(t)) continue;
+    const d = Math.round((t - now) / 86400000);
+    if (d >= -3 && (best === null || d < best)) best = d;
+  }
+  if (/\b(today|due today|expires? today)\b/i.test(body)) best = best === null ? 0 : Math.min(best, 0);
+  return best;
+}
+function parseMaxDollars(body) {
+  let max = 0;
+  for (const m of body.matchAll(/\$\s?([\d,]+(?:\.\d+)?)\s*([kKmM])?/g)) {
+    let n = parseFloat(m[1].replace(/,/g, '')); if (isNaN(n)) continue;
+    if (m[2] && /[kK]/.test(m[2])) n *= 1e3; if (m[2] && /[mM]/.test(m[2])) n *= 1e6;
+    if (n > max) max = n;
+  }
+  return max;
+}
+function lastActivityMs(t) {
+  let ms = +new Date(t.updated_at || t.created_at || 0);
+  for (const a of (t.actions || [])) ms = Math.max(ms, +new Date(a.ts));
+  for (const c of (t.comments || [])) ms = Math.max(ms, +new Date(c.ts));
+  return ms || Date.now();
+}
+function scoreTicket(t) {
+  const b = ticketText(t);
+  const now = Date.now();
+  const money = parseMaxDollars(b);
+  const days = nearestDeadlineDays(b);
+  const idleH = (now - lastActivityMs(t)) / 3600000;                 // recency of last activity (stale = higher need)
+  const ageDays = (now - +new Date(t.created_at || now)) / 86400000; // how long it's been open
+
+  // 💰 value/impact: dollars (log) OR project/keyword stakes, whichever higher
+  let value = money >= 1e5 ? 5 : money >= 1e4 ? 4 : money >= 1e3 ? 3 : money >= 100 ? 2 : money > 0 ? 1 : 0;
+  value = Math.max(value, 2); // every open ticket has baseline stakes
+  if (/\b(revenue|prod|production|customer|customer-facing|live|go-live|launch|ship|deploy)\b/i.test(b)) value = Math.min(5, value + 1);
+  if (/\b(urgent|critical|broken|down|502|500|incident|lapse|expir|money (owed|left)|five[- ]figure)\b/i.test(b)) value = Math.min(5, value + 1);
+
+  // ⏰ urgency: deadline + staleness + age + status weighting + urgent words
+  let urgency = 1;
+  if (days !== null) urgency = days <= 1 ? 5 : days <= 3 ? 4 : days <= 7 ? 3 : days <= 30 ? 2 : 1;
+  if (idleH > 72) urgency = Math.max(urgency, 4);           // stale >3d = high need
+  else if (idleH > 24) urgency = Math.max(urgency, 3);      // stale >1d
+  if (ageDays > 14) urgency = Math.min(5, urgency + 1);     // long-open drags priority up
+  if (t.status === 'blocked') urgency = Math.min(5, urgency + 2); // blocked screams for attention
+  else if (t.status === 'doing') urgency = Math.min(5, urgency + 1);
+  if (/\b(urgent|asap|lapsing|due today|deadline|expires? (today|tomorrow)|now|immediately|incident)\b/i.test(b)) urgency = Math.min(5, urgency + 1);
+  urgency = Math.max(1, Math.min(5, urgency));
+
+  // ⚡ ease (higher = quicker / lower-friction to complete)
+  let ease = 3;
+  const wc = b.split(/\s+/).length;
+  if (/\b(reversible|one[- ]click|1[- ]click|toggle|paste|quick|small|single|read-only|one[- ]line|tweak|typo)\b/i.test(b)) ease += 1;
+  if (/\b(build|migration|scrape|onboard|rebuild|multi-part|multi-step|large|backfill|thousands|batch|refactor|overhaul|end-to-end|pipeline)\b/i.test(b)) ease -= 1;
+  if ((t.actions || []).length + (t.comments || []).length <= 1 && wc < 40) ease += 1; // short/single-action ticket
+  if (wc > 400) ease -= 1;
+  ease = Math.max(1, Math.min(5, ease));
+
+  // ✅ safety/confidence (higher = safer / more reversible)
+  let safety = 3;
+  if (/\b(reversible|restore[- ]map|verified|snapshot|dry[- ]?run|git revert|rollback|local|read-only|additive)\b/i.test(b)) safety += 1;
+  if (/\b(destructive|irreversible|delete|purge|drop |wipe|history rewrite|filter-repo|unpublish|cannot be undone|force[- ]push|prod deploy|dns|spend|send-to-list)\b/i.test(b)) safety -= 2;
+  safety = Math.max(1, Math.min(5, safety));
+
+  // composite: value + urgency dominate; ease nudges; low safety slightly demotes
+  const priority = Math.round((value * 2.4 + urgency * 2.4 + ease * 1.0 + safety * 0.4) * 10) / 10;
+  const tier = priority >= 26 ? 'high' : priority >= 18 ? 'med' : 'low';
+  return { value, urgency, ease, safety, priority, tier, money: money || 0, days };
+}
+// Attach ranking/ratings to a flat ticket list. Only open/doing/blocked get ranked
+// (done/stopped are excluded from the ranking per the brief) — those get priority 0 / no rank.
+function withRanking(list) {
+  const RANKABLE = new Set(['open', 'doing', 'blocked']);
+  const scored = [];
+  for (const t of list) {
+    if ((t.kind || 'task') === 'task' && RANKABLE.has(t.status)) {
+      const s = scoreTicket(t);
+      t.priority = s.priority; t.tier = s.tier;
+      t.ratings = { value: s.value, urgency: s.urgency, ease: s.ease, safety: s.safety };
+      t.money = s.money; t.deadlineDays = s.days;
+      scored.push(t);
+    } else {
+      t.priority = 0; t.tier = null; t.rank = null;
+      t.ratings = { value: 0, urgency: 0, ease: 0, safety: 0 };
+      t.money = 0; t.deadlineDays = null;
+    }
+  }
+  scored.sort((a, b) => b.priority - a.priority || (+new Date(b.updated_at) - +new Date(a.updated_at)));
+  scored.forEach((t, i) => { t.rank = i + 1; });
+  return list;
+}
+
+function page() {
+  const cols = { open: [], doing: [], blocked: [], done: [], stopped: [] };
+  for (const t of tickets().values()) (cols[t.status] || (cols[t.status] = [])).push(t);
+  for (const k of STATUSES) cols[k].sort((a, b) => a.updated_at < b.updated_at ? 1 : -1);
+  cols.done = cols.done.slice(0, 40);
+  cols.stopped = cols.stopped.slice(0, 40);
+  const card = t => `<div class="card" onclick="this.classList.toggle('x')">
+    <div class="cid">${t.id}${t.project ? `<span class="proj">${esc(t.project)}</span>` : ''}</div>
+    <div class="ttl">${esc(t.title)}</div>
+    <div class="meta"><span class="who">${esc(t.assignee || 'unassigned')}</span>
+      <span class="when" title="${esc(t.created_at)}">🕓 ${new Date(t.created_at).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}</span></div>
+    <div class="thread">${t.comments.map(c => `<div class="c k-${c.kind}"><b>${esc(c.agent)}</b> <i>${esc(c.kind)}</i> ${esc(c.text)}<span class="cts">${new Date(c.ts).toLocaleString()}</span></div>`).join('')}
+      ${t.actions.map(a => `<div class="c k-action"><b>${esc(a.agent)}</b> <i>action</i> ${esc(a.text)}<span class="cts">${new Date(a.ts).toLocaleString()}</span></div>`).join('') || ''}
+      ${!t.comments.length && !t.actions.length ? '<div class="c none">no comments yet</div>' : ''}</div></div>`;
+  // ── Direct-message conversations, grouped into threads ──
+  const mm = messages();
+  const rootOf = mid => { let id = mid, c = mm.get(mid); const seen = new Set([id]); while (c && c.re && mm.get(c.re)) { if (seen.has(c.re)) break; seen.add(c.re); id = c.re; c = mm.get(id); } return id; };
+  const threads = new Map();
+  for (const m of mm.values()) { const r = rootOf(m.mid); (threads.get(r) || threads.set(r, []).get(r)).push(m); }
+  const convos = [...threads.values()].map(ms => ms.sort((a, b) => a.ts < b.ts ? -1 : 1))
+    .sort((a, b) => a[a.length - 1].ts < b[b.length - 1].ts ? 1 : -1).slice(0, 40);
+  const dmLine = m => `<div class="dm"><b>${esc(m.from)}</b> <span class="arw">→</span> <b>${esc(m.to)}</b>${m.ticket ? `<span class="dtk">${esc(m.ticket.replace(/^(TK-\d+).*/, '$1'))}</span>` : ''}
+    <span class="dts">${new Date(m.ts).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}</span>
+    <div class="dtx">${esc(m.text)}</div></div>`;
+  const convoCard = ms => `<div class="convo"><div class="chd">${esc([...new Set(ms.flatMap(m => [m.from, m.to]))].filter(Boolean).join(' ⇄ '))}<span class="cnt">${ms.length} msg${ms.length > 1 ? 's' : ''}</span></div>${ms.map(dmLine).join('')}</div>`;
+  const dmPanel = `<details class="dms" open><summary>DIRECT MESSAGES <small>${mm.size} total · ${convos.length} conversations · agents talk via <code>tk dm / reply / @mention</code></small></summary>
+    <div class="convos">${convos.length ? convos.map(convoCard).join('') : '<div class="c none">no direct messages yet</div>'}</div></details>`;
+  return `<!doctype html><meta charset="utf-8"><title>Fleet Tickets</title><meta http-equiv="refresh" content="30">
+<style>
+  body{margin:0;font:14px -apple-system,sans-serif;background:#0f1115;color:#e6e6e6}
+  h1{font-size:16px;margin:0;padding:14px 18px;border-bottom:1px solid #262a33;letter-spacing:.06em}
+  h1 small{color:#8a93a5;font-weight:400;margin-left:10px}
+  .board{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;padding:14px;align-items:start}
+  .col h2{font-size:12px;text-transform:uppercase;letter-spacing:.1em;color:#8a93a5;margin:4px 2px 8px}
+  .card{background:#171b22;border:1px solid #262a33;border-radius:8px;padding:10px 12px;margin-bottom:8px;cursor:pointer}
+  .cid{font-weight:600;color:#6db3f2;font-size:12px}.proj{float:right;color:#8a93a5;font-weight:400}
+  .ttl{margin:4px 0 6px}
+  .meta{display:flex;justify-content:space-between;font-size:11px;color:#8a93a5}
+  .when{white-space:nowrap}
+  .thread{display:none;margin-top:8px;border-top:1px dashed #2c3140;padding-top:6px}
+  .card.x .thread{display:block}
+  .c{font-size:12px;margin:4px 0;color:#c6ccd8}.c i{color:#8a93a5;font-style:normal;font-size:10px;margin:0 4px}
+  .c.k-note{color:#e8d48b}.c.k-action{color:#8fd49a}.c.none{color:#5b6270}
+  .c.k-win{color:#34d399}.c.k-challenge{color:#e06c75}.c.k-cody{color:#f59e0b}
+  .cts{display:block;font-size:10px;color:#5b6270}
+  .col-doing .card{border-left:3px solid #6db3f2}.col-blocked .card{border-left:3px solid #e06c75}.col-done .card{opacity:.55}
+  .col-stopped .card{opacity:.4;border-left:3px solid #6b7280}.col-stopped h2{color:#9aa2b1}
+  .dms{margin:0 14px 6px;background:#141821;border:1px solid #262a33;border-radius:8px}
+  .dms>summary{cursor:pointer;padding:10px 14px;font-size:12px;text-transform:uppercase;letter-spacing:.1em;color:#c9a4f2}
+  .dms>summary small{text-transform:none;letter-spacing:0;color:#8a93a5;margin-left:8px;font-size:11px}
+  .dms code{color:#c9a4f2;background:#1c2130;padding:1px 4px;border-radius:4px}
+  .convos{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:10px;padding:4px 14px 14px}
+  .convo{background:#171b22;border:1px solid #262a33;border-left:3px solid #a06ef2;border-radius:8px;padding:8px 10px}
+  .chd{font-size:11px;color:#c9a4f2;font-weight:600;margin-bottom:6px}.chd .cnt{float:right;color:#5b6270;font-weight:400}
+  .dm{font-size:12px;margin:5px 0;padding-top:5px;border-top:1px dashed #2c3140}.dm:first-of-type{border-top:0}
+  .dm .arw{color:#8a93a5;margin:0 3px}.dm b{color:#cdd4e0}
+  .dm .dtk{color:#6db3f2;font-size:10px;margin-left:6px}.dm .dts{float:right;color:#5b6270;font-size:10px}
+  .dm .dtx{color:#c6ccd8;margin-top:2px}
+</style>
+<h1>FLEET TICKETS<small>every agent action rides a ticket — tk new / comment / note / log / dm / inbox / reply / @mention / take / done</small><a href="/office" style="float:right;color:#c9a4f2;text-decoration:none;font-size:13px;border:1px solid #2c3140;padding:4px 10px;border-radius:6px">🏢 3D Office →</a></h1>
+${dmPanel}
+<div class="board">${STATUSES.map(s => `<div class="col col-${s}"><h2>${s} (${cols[s].length})</h2>${cols[s].map(card).join('') || '<div class="c none">empty</div>'}</div>`).join('')}</div>`;
+}
+
+http.createServer((req, res) => {
+  if (req.url === '/healthz') { res.writeHead(200); return res.end('ok'); }
+  const locked = lockoutGate(req);
+  if (locked) { res.writeHead(locked.code, { 'Retry-After': String(locked.retry) }); return res.end(locked.msg); }
+  if (req.headers.authorization !== AUTH) { noteAuth(req, false); res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="tickets"' }); return res.end('auth'); }
+  noteAuth(req, true);
+
+  // ── writes / actions (all auth-gated; ids resolved server-side from the real store) ──
+
+  // Run Now — open one iTerm2 Claude session per selected ticket (staggered so iTerm doesn't drop windows).
+  if (req.method === 'POST' && req.url === '/api/run') {
+    if (execBlockedForRemote(req, res)) return;
+    return readJson(req, body => {
+      if (!body) return json(res, 400, { error: 'bad json' });
+      const profile = effectiveRunProfile(body.profile);
+      if (!RUN_PROFILES.has(profile)) return json(res, 400, { error: 'invalid run profile' });
+      const map = tickets(); const ids = resolveList(body.ids, map);
+      const launched = [], skipped = [];
+      ids.forEach((id, i) => {
+        const t = map.get(id);
+        if (t && t.status === 'stopped') { skipped.push({ id, why: 'stopped' }); return; }
+        if (t && t.status === 'doing') { skipped.push({ id, why: 'already-doing' }); return; }
+        if (t && (t.kind || 'task') !== 'task') { skipped.push({ id, why: 'designation-' + t.kind }); return; }
+        let cwd = os.homedir();
+        const proj = t && t.project;
+        if (proj && /^[a-z0-9._-]+$/i.test(proj)) { const p = path.join(os.homedir(), 'Projects', proj); if (fs.existsSync(p)) cwd = p; }
+        setTimeout(() => execFile('bash', [RUN_SH, id, cwd, profile], { timeout: 25000 }, (err) => {
+          // Log the REAL outcome from the launch callback — never an optimistic "launched" before the window opens.
+          // If osascript/iTerm fails (app quit, Automation permission denied), surface it on the board instead of a false success.
+          withLock(() => append({ ts: new Date().toISOString(), type: 'action', id, agent: 'board',
+            text: err ? ('⚠ RUN NOW failed to launch iTerm2 session — ' + String(err.message || err).split('\n')[0])
+                      : `▶ RUN NOW — launched iTerm2 session from the board · profile=${profile}` }));
+        }), i * 1300); // stagger ~1.3s
+        launched.push(id);
+      });
+      json(res, 200, { launched, skipped, profile });
+    });
+  }
+  // Stop Forever — mark selected tickets TicketStopped (status 'stopped'); reversible via /api/reopen.
+  if (req.method === 'POST' && req.url === '/api/stop') {
+    return readJson(req, body => {
+      if (!body) return json(res, 400, { error: 'bad json' });
+      const map = tickets(); const ids = resolveList(body.ids, map);
+      withLock(() => { for (const id of ids) {
+        append({ ts: new Date().toISOString(), type: 'status', id, status: 'stopped', agent: 'board' });
+        append({ ts: new Date().toISOString(), type: 'action', id, agent: 'board', text: '⛔ TicketStopped — stopped forever from the board' });
+      } });
+      json(res, 200, { stopped: ids });
+    });
+  }
+  // Reopen — un-stop (or un-close) selected tickets back to 'open'.
+  if (req.method === 'POST' && req.url === '/api/reopen') {
+    return readJson(req, body => {
+      if (!body) return json(res, 400, { error: 'bad json' });
+      const map = tickets(); const ids = resolveList(body.ids, map);
+      withLock(() => { for (const id of ids) {
+        append({ ts: new Date().toISOString(), type: 'status', id, status: 'open', agent: 'board' });
+        append({ ts: new Date().toISOString(), type: 'action', id, agent: 'board', text: '↩ reopened from the board' });
+      } });
+      json(res, 200, { reopened: ids });
+    });
+  }
+  // DTD — trigger a batched run-now sweep (POST) / read the latest verdicts + running state (GET).
+  if (req.method === 'POST' && req.url === '/api/dtd') {
+    if (execBlockedForRemote(req, res)) return;
+    return readJson(req, body => {
+      if (fs.existsSync(DTD_RUNNING)) return json(res, 200, { started: false, already: true });
+      const args = [DTD_RUN];
+      const explicit = body && Array.isArray(body.ids) && body.ids.length ? resolveList(body.ids, tickets()) : [];
+      if (explicit.length) args.push(...explicit); else args.push('--recent');
+      try { const child = spawn(process.execPath, args, { detached: true, stdio: 'ignore', cwd: __dirname }); child.unref(); }
+      catch (e) { return json(res, 500, { started: false, error: e.message }); }
+      json(res, 200, { started: true, ids: explicit.length ? explicit : 'recent' });
+    });
+  }
+  if (req.method === 'GET' && req.url === '/api/dtd') {
+    let verdicts = null, running = false, runningInfo = null;
+    try { verdicts = JSON.parse(fs.readFileSync(VERDICTS, 'utf8')); } catch {}
+    try { runningInfo = JSON.parse(fs.readFileSync(DTD_RUNNING, 'utf8')); running = true; } catch {}
+    return json(res, 200, { running, runningInfo, verdicts });
+  }
+
+  // shared nav-agent drop-in (grid-controls standard) — static assets
+  if (req.url === '/nav-agent/nav-agent.js' || req.url === '/nav-agent/nav-agent.css') {
+    const file = path.join(__dirname, req.url.replace(/^\//, ''));
+    const type = req.url.endsWith('.css') ? 'text/css' : 'application/javascript';
+    return fs.readFile(file, (e, buf) => {
+      if (e) { res.writeHead(404); return res.end('nav-agent asset missing'); }
+      res.writeHead(200, { 'Content-Type': type + '; charset=utf-8', 'Cache-Control': 'no-store' }); res.end(buf);
+    });
+  }
+  if (req.url === '/office' || req.url === '/office.html') {
+    return fs.readFile(OFFICE_HTML, (e, buf) => {
+      if (e) { res.writeHead(500); return res.end('office view missing'); }
+      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }); res.end(buf);
+    });
+  }
+  if (req.url === '/api/running') { return getRunning(d => { res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.end(JSON.stringify(d)); }); }
+  if (req.url === '/api/tickets') { res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); return res.end(JSON.stringify(withRanking([...tickets().values()]))); }
+  if (req.url === '/api/agents') { res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); return res.end(JSON.stringify(ticketAgents())); }
+  if (req.url === '/api/skills') { res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); return res.end(JSON.stringify(installedSkills())); }
+  if (req.url === '/api/messages') { res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); return res.end(JSON.stringify([...messages().values()])); }
+  if (req.url === '/kanban') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }); return res.end(page()); }
+  // default (/) = the adjustable-columns TABLE view (Steve's list-builds rule, 2026-08-10)
+  return fs.readFile(BOARD_HTML, (e, buf) => {
+    if (e) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }); return res.end(page()); } // fall back to kanban
+    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }); res.end(buf);
+  });
+}).listen(PORT, '127.0.0.1', () => {
+  console.log('ticket board on :' + PORT);
+  getRunning(() => {});                          // warm the pm2/sessions cache so first client fetch is instant
+  setInterval(() => getRunning(() => {}), 4500); // keep it warm ahead of the 5s TTL
+});
diff --git a/verification/tk11372/stages.cjs b/verification/tk11372/stages.cjs
new file mode 100644
index 00000000..9b3c403d
--- /dev/null
+++ b/verification/tk11372/stages.cjs
@@ -0,0 +1,105 @@
+// Actual HTTP with isolated ticket data and executable fault-injection probes.
+// Runtime timeouts are unchanged. No real PM2 command is executed.
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const http = require('node:http');
+const { fork } = require('node:child_process');
+const assert = require('node:assert/strict');
+const crypto = require('node:crypto');
+const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tk11372-stages-'));
+const results = [];
+const children = [];
+const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
+const hash = file => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
+const source = path.join(__dirname, '../../server.js');
+const sourceHash = hash(source);
+const auth = 'Basic ' + Buffer.from('fixture:fixture').toString('base64');
+const trace = fixture => fs.readFileSync(fixture.trace, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
+function alive(pid) { try { process.kill(pid, 0); return true; } catch { return false; } }
+function request(port, url, authorized = true) {
+  return new Promise(resolve => {
+    const start = Date.now();
+    const timer = setTimeout(() => req.destroy(new Error('deadline')), 1500);
+    const req = http.get({host:'127.0.0.1', port, path:url, headers:authorized ? {authorization:auth} : {}}, res => {
+      let body = ''; res.on('data', data => body += data);
+      res.on('end', () => { clearTimeout(timer); resolve({status:res.statusCode, body, ms:Date.now()-start}); });
+    });
+    req.on('error', error => {clearTimeout(timer);resolve({error:error.message, ms:Date.now()-start});});
+  });
+}
+async function until(fn, predicate, timeout = 40000) {
+  const end = Date.now() + timeout;
+  let value;
+  do {value = await fn(); if(predicate(value)) return value; await delay(200);} while(Date.now() < end);
+  throw new Error('Timed out: ' + JSON.stringify(value));
+}
+async function launch(label) {
+  const home = path.join(root, label); fs.mkdirSync(home);
+  for(const name of ['bin','.pm2','tickets']) fs.mkdirSync(path.join(home,name));
+  const mode = path.join(home,'mode'); fs.writeFileSync(mode,'healthy');
+  const traceFile = path.join(home,'trace.jsonl');fs.writeFileSync(traceFile,'');
+  fs.writeFileSync(path.join(home,'.pm2/rpc.sock'),'fixture');
+  const events = path.join(home,'tickets/events.jsonl');
+  fs.writeFileSync(events,JSON.stringify({type:'create',id:'TK-1-fixture',title:'fixture',agent:'fixture',ts:'2026-09-10T00:00:00Z'})+'\n');
+  fs.copyFileSync(path.join(__dirname,'../../lib.js'),path.join(home,'lib.js'));
+  fs.copyFileSync(path.join(__dirname,'server.candidate.cjs'),path.join(home,'server.cjs'));
+  const probe = `#!${process.execPath}
+const fs=require('fs'),path=require('path');
+const mode=fs.readFileSync(process.env.API_MODE,'utf8');
+const command=path.basename(process.argv[1])==='lsof'?'lsof':process.argv.includes('-Ao')?'sessions':'ps';
+fs.appendFileSync(process.env.API_TRACE,JSON.stringify({command,mode,pid:process.pid,at:Date.now()})+'\\n');
+if(mode==='hung-'+command) {process.on('SIGTERM',()=>{});setInterval(()=>{},1000);}
+else if(command==='lsof') {if(mode==='missing')process.exit(1);console.log('socket holder');}
+else if(command==='ps') console.log('1 PM2 v6.0: God Daemon ('+process.env.PM2_HOME+')');
+else console.log('claude interactive\\nclaude /skills/helper\\nother process\\nclaude second');
+`;
+  for(const command of ['lsof','ps']) {const file=path.join(home,'bin',command);fs.writeFileSync(file,probe);fs.chmodSync(file,0o755);}
+  const wrapper=path.join(home,'.claude/skills/keep-alive/proposals/TK-10970');fs.mkdirSync(wrapper,{recursive:true});
+  fs.writeFileSync(path.join(wrapper,'pm2-serialized.js'), `const fs=require('fs');const mode=fs.readFileSync(process.env.API_MODE,'utf8');fs.appendFileSync(process.env.API_TRACE,JSON.stringify({command:'jlist',mode,pid:process.pid,at:Date.now()})+'\\n');if(mode==='hung-jlist'){process.on('SIGTERM',()=>{});setInterval(()=>{},1000);}else if(mode==='bad-json')console.log('bad');else console.log(JSON.stringify([{name:mode,pm2_env:{status:'online'},monit:{}}]));`);
+  const preload=path.join(home,'preload.cjs');
+  fs.writeFileSync(preload,"const http=require('http');const listen=http.Server.prototype.listen;http.Server.prototype.listen=function(...args){this.once('listening',()=>process.send({port:this.address().port}));return listen.apply(this,args);};");
+  const child=fork(path.join(home,'server.cjs'),[],{execArgv:['--require',preload],env:{...process.env,HOME:home,PM2_HOME:path.join(home,'.pm2'),TICKET_DATA_DIR:path.join(home,'tickets'),TK_AUTH:'fixture:fixture',PORT:'0',PATH:path.join(home,'bin')+':'+process.env.PATH,API_MODE:mode,API_TRACE:traceFile},stdio:['ignore','pipe','pipe','ipc']});
+  children.push(child);
+  const output=fs.createWriteStream(path.join(home,'server.log'));child.stdout.pipe(output);child.stderr.pipe(output);
+  const port=await new Promise((resolve,reject)=>{const timer=setTimeout(()=>reject(new Error('listen timeout')),15000);child.once('message',m=>{clearTimeout(timer);resolve(m.port);});child.once('error',reject);});
+  return {child,port,mode,trace:traceFile,events,eventsHash:hash(events)};
+}
+async function stage(command) {
+  const fixture=await launch(command);
+  const get=()=>request(fixture.port,'/api/running');
+  const healthy=await until(get,r=>r.status===200 && JSON.parse(r.body).pm2[0]?.name==='healthy');
+  assert.equal(JSON.parse(healthy.body).sessions,2);
+  fs.writeFileSync(fixture.mode,'hung-'+command);
+  const started=await until(async()=>{await get();return trace(fixture).find(t=>t.command===command && t.mode==='hung-'+command);},Boolean);
+  const probes=await Promise.all([request(fixture.port,'/healthz'),request(fixture.port,'/api/tickets',false),request(fixture.port,'/api/tickets'),...Array.from({length:12},get)]);
+  assert.equal(probes[0].status,200);assert.equal(probes[1].status,401);assert.equal(JSON.parse(probes[2].body)[0].title,'fixture');
+  for(const response of probes.slice(3)) {assert.equal(response.status,200);assert.deepEqual(JSON.parse(response.body),JSON.parse(healthy.body));}
+  assert.equal(trace(fixture).filter(t=>t.command===command && t.mode==='hung-'+command).length,1);
+  const deadline = command==='jlist'?22000:4000;
+  await delay(Math.max(0, started.at+deadline+800-Date.now()));
+  const survivor=alive(started.pid);
+  if(survivor && process.env.EXPECT_SESSION_LEAK==='1' && command==='sessions') {
+    results.push({name:'original candidate session shell leaves hung child alive past timeout',verdict:'PASS',knownDefect:true,pid:started.pid,probes});
+    process.kill(started.pid,'SIGKILL');
+  } else assert.equal(survivor,false,command+' probe survived configured timeout');
+  const after=await get();
+  if(command!=='sessions')assert.deepEqual(JSON.parse(after.body),JSON.parse(healthy.body));
+  else assert.equal(JSON.parse(after.body).sessions,2);
+  fs.writeFileSync(fixture.mode,'recovered-'+command);
+  const recovered=await until(get,r=>r.status===200 && JSON.parse(r.body).pm2[0]?.name==='recovered-'+command);
+  assert.equal(JSON.parse(recovered.body).sessions,2);
+  assert.equal(hash(fixture.events),fixture.eventsHash);
+  results.push({name:command+' timeout responsiveness, singleflight, cached fallback, child termination and recovery',verdict:survivor?'FAIL':'PASS',deadline,probes,recovered,trace:trace(fixture),eventsUnchanged:true});
+  fixture.child.kill('SIGTERM');
+}
+(async()=>{
+  try {for(const command of (process.env.STAGES||'lsof,ps,jlist,sessions').split(','))await stage(command);}
+  finally {
+    for(const child of children)child.kill('SIGTERM');
+    for(const file of fs.readdirSync(root)){const t=path.join(root,file,'trace.jsonl');if(fs.existsSync(t))for(const row of fs.readFileSync(t,'utf8').split('\n').filter(Boolean).map(JSON.parse))if(alive(row.pid)){try{process.kill(row.pid,'SIGKILL');}catch{}}}
+    assert.equal(hash(source),sourceHash);
+    fs.writeFileSync(process.env.RESULT_FILE||path.join(__dirname,'stage-results.json'),JSON.stringify({timestamp:new Date().toISOString(),root,candidateSha256:hash(path.join(__dirname,'server.candidate.cjs')),liveSourceUnchanged:true,results},null,2)+'\n');
+  }
+  console.log(JSON.stringify({root,results:results.map(({name,verdict})=>({name,verdict}))},null,2));
+})().catch(error=>{console.error(error);process.exitCode=1;});

← 0b5e654b auto-data-snapshot: 2026-09-11T08:16:47 (1 data files) — ver  ·  back to Ticket System  ·  Record ordered cycle monitoring and concurrent parity reader 5d710f56 →