[object Object]

← back to Ticket System

Clear blockers when tickets complete

180ff40fbe6536adc50f8c2201c34186bcfe3202 · 2026-09-02 17:39:52 -0700 · Steve Abrams

Files touched

Diff

commit 180ff40fbe6536adc50f8c2201c34186bcfe3202
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 2 17:39:52 2026 -0700

    Clear blockers when tickets complete
---
 lib.js                  | 8 +++++++-
 lib.test.js             | 9 +++++++++
 test/mcp-server.test.js | 2 +-
 3 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/lib.js b/lib.js
index 2febe64b..9f80763e 100644
--- a/lib.js
+++ b/lib.js
@@ -77,7 +77,13 @@ function tickets() {
       if (ev.correlation_id) t.last_correlation_id = ev.correlation_id;
       if (ev.type === 'comment') t.comments.push({ ts: ev.ts, agent: ev.agent || '', kind: ev.kind || 'comment', text: ev.text, correlation_id: ev.correlation_id || '' });
       else if (ev.type === 'action') t.actions.push({ ts: ev.ts, agent: ev.agent || '', text: ev.text, correlation_id: ev.correlation_id || '' });
-      else if (ev.type === 'status' && STATUSES.includes(ev.status)) { t.status = ev.status; t.status_since = ev.ts; }
+      else if (ev.type === 'status' && STATUSES.includes(ev.status)) {
+        t.status = ev.status;
+        t.status_since = ev.ts;
+        // A completed ticket cannot have an active current-state blocker. The
+        // blocker event remains in the append-only ledger for history.
+        if (ev.status === 'done') delete t.blocker;
+      }
       else if (ev.type === 'assign') t.assignee = ev.agent || '';
       else if (ev.type === 'blocker') t.blocker = cleanBlocker(ev.blocker);
       else if (ev.type === 'designation' && KINDS.includes(ev.kind)) {
diff --git a/lib.test.js b/lib.test.js
index f4cf23c4..bb7ff817 100644
--- a/lib.test.js
+++ b/lib.test.js
@@ -98,6 +98,15 @@ test('structured blocker metadata folds and validates required fields', () => {
   assert.throws(() => ticketBlocker(id, { type:'bad' }), /invalid blocker type/);
 });
 
+test('done status clears the materialized blocker without rewriting history', () => {
+  const id = 'TK-10033-completed-blocker';
+  append({ ts: iso(), type: 'create', id, title: 'Completed blocker', agent: 'a' });
+  ticketBlocker(id, { type:'technical_dependency', condition:'proof pending', next_action:'run proof', owner:'a', evidence_at:iso() });
+  append({ ts: iso(), type: 'status', id, status: 'done', agent: 'a' });
+  assert.equal(tickets().get(id).blocker, undefined);
+  assert.ok(readEventLines().map(JSON.parse).some(ev => ev.type === 'blocker' && ev.id === id));
+});
+
 // ── 3. comment vs action vs assign fold correctly; create body seeds a comment ─
 test('comment, action, and assign events fold into the right places', () => {
   const id = 'TK-10004-fold';
diff --git a/test/mcp-server.test.js b/test/mcp-server.test.js
index a34a12c2..c6e6b1e8 100644
--- a/test/mcp-server.test.js
+++ b/test/mcp-server.test.js
@@ -26,7 +26,7 @@ test('Codex ticket lifecycle persists to one canonical event ledger', () => {
   call('ticket_blocker',{ticket:id,type:'technical_dependency',condition:'scope missing',next_action:'grant scope',owner:'steve',evidence_at:new Date().toISOString(),steve_one_action:true});
   call('ticket_status',{ticket:id,status:'done'});
   const got = call('ticket_get',{ticket:id});
-  assert.equal(got.status,'done'); assert.equal(got.blocker.type,'technical_dependency'); assert.equal(got.actions.at(-1).text,'verified shared write'); assert.equal(got.actions.at(-1).correlation_id,logged.correlation_id);
+  assert.equal(got.status,'done'); assert.equal(got.blocker,undefined); assert.equal(got.actions.at(-1).text,'verified shared write'); assert.equal(got.actions.at(-1).correlation_id,logged.correlation_id);
   const lines=fs.readFileSync(path.join(tmp,'events.jsonl'),'utf8').trim().split('\n').map(JSON.parse);
   assert.ok(lines.every(e=>e.correlation_id));
 });

← ecf0b99a record TK-10993 safe hold cycle  ·  back to Ticket System  ·  Enforce terminal ticket blocker precedence c351d5ce →