[object Object]

← back to Ticket System

ticket ids: 5-digit zero-padded + title slug (TK-00025-style); resolveId accepts bare number, TK-n, or full id; legacy ids resolve

a54785050f068509e8f60076692cb2d64f862056 · 2026-07-26 15:34:39 -0700 · Steve Abrams

Files touched

Diff

commit a54785050f068509e8f60076692cb2d64f862056
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jul 26 15:34:39 2026 -0700

    ticket ids: 5-digit zero-padded + title slug (TK-00025-style); resolveId accepts bare number, TK-n, or full id; legacy ids resolve
---
 lib.js | 32 ++++++++++++++++++++++++++++----
 tk     | 10 +++++++---
 2 files changed, 35 insertions(+), 7 deletions(-)

diff --git a/lib.js b/lib.js
index 9367cd4..e10e777 100644
--- a/lib.js
+++ b/lib.js
@@ -53,10 +53,34 @@ function tickets() {
   return map;
 }
 
-function nextId() {
+// Ticket ids are 5-digit zero-padded + a slug of the title (Steve, 2026-07-26):
+//   TK-00025-ga4-attribution-prep
+// The numeric part is the tracking key; legacy short ids (TK-3, TK-17) remain
+// valid in the log and resolvable by number.
+function slugify(title) {
+  return String(title || '').toLowerCase()
+    .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40).replace(/-+$/, '');
+}
+function idNum(id) { return parseInt(String(id).replace(/^TK-/i, ''), 10); } // parseInt stops at the slug dash
+function nextId(title) {
   let max = 0;
-  for (const ev of readEvents()) if (ev.type === 'create') { const n = parseInt(String(ev.id).replace('TK-', ''), 10); if (n > max) max = n; }
-  return 'TK-' + (max + 1);
+  for (const ev of readEvents()) if (ev.type === 'create') { const n = idNum(ev.id); if (n > max) max = n; }
+  const num = String(max + 1).padStart(5, '0');
+  const slug = slugify(title);
+  return 'TK-' + num + (slug ? '-' + slug : '');
+}
+// Resolve any reference — full id, TK-24, 24, 00024, or 00024-partial-slug —
+// to the canonical stored id (or null if nothing matches).
+function resolveId(ref, map) {
+  if (!ref) return null;
+  const m = map || tickets();
+  let r = String(ref).toUpperCase();
+  if (!r.startsWith('TK-')) r = 'TK-' + r;
+  for (const id of m.keys()) if (id.toUpperCase() === r) return id;
+  const n = idNum(r);
+  if (!Number.isFinite(n)) return null;
+  for (const id of m.keys()) if (idNum(id) === n) return id;
+  return null;
 }
 
-module.exports = { withLock, append, tickets, nextId, STATUSES, EVENTS };
+module.exports = { withLock, append, tickets, nextId, resolveId, idNum, slugify, STATUSES, EVENTS };
diff --git a/tk b/tk
index 89e3015..8d87d53 100755
--- a/tk
+++ b/tk
@@ -1,5 +1,7 @@
 #!/opt/homebrew/bin/node
 // tk — fleet ticket CLI. Every agent action ties to a ticket; comments/notes are the exchange channel.
+// Ids are TK-<5-digit>-<name-slug> (e.g. TK-00025-ga4-attribution-prep); reference
+// tickets by any form — 25, TK-25, 00025, or the full id. Legacy TK-3-style ids resolve too.
 // Usage:
 //   tk new "title" [-p project] [-a agent] [-b "opening comment"]
 //   tk comment TK-3 "text" [-a agent]         # discussion comment
@@ -14,7 +16,7 @@
 //   tk list [--status s] [--project p] [--agent a] [--all]
 //   tk show TK-3
 // Agent identity defaults to $TK_AGENT, else "claude@<tty-or-pid>".
-const { withLock, append, tickets, nextId, STATUSES } = require(require('path').join(__dirname, 'lib.js'));
+const { withLock, append, tickets, nextId, resolveId, STATUSES } = require(require('path').join(__dirname, 'lib.js'));
 
 const argv = process.argv.slice(2);
 const cmd = argv.shift();
@@ -24,14 +26,16 @@ const agentOpt = opt('-a') || opt('--agent-id');
 const agent = agentOpt || process.env.TK_AGENT || ('claude@' + (process.env.TERM_SESSION_ID || process.pid));
 const ts = new Date().toISOString();
 const die = m => { console.error(m); process.exit(1); };
-const norm = id => { if (!id) die('missing ticket id'); id = id.toUpperCase(); return id.startsWith('TK-') ? id : 'TK-' + id; };
+// Resolve any reference (24, TK-24, 00024, or the full 5-digit+slug id) to the
+// canonical stored id — legacy short ids and new TK-00024-slug ids both work.
+const norm = ref => { if (!ref) die('missing ticket id'); const id = resolveId(ref); if (!id) die('no such ticket ' + ref); return id; };
 const fmt = t => `${t.id} [${t.status}] (${t.assignee || 'unassigned'})${t.project ? ' {' + t.project + '}' : ''} ${t.title}`;
 
 if (cmd === 'new') {
   const project = opt('-p') || opt('--project') || '';
   const body = opt('-b') || opt('--body') || '';
   const title = argv.join(' ').trim(); if (!title) die('usage: tk new "title" [-p project] [-a agent]');
-  const ev = withLock(() => append({ ts, type: 'create', id: nextId(), title, project, agent, body }));
+  const ev = withLock(() => append({ ts, type: 'create', id: nextId(title), title, project, agent, body }));
   console.log(ev.id);
 } else if (cmd === 'comment' || cmd === 'note' || cmd === 'win' || cmd === 'challenge' || cmd === 'cody') {
   const id = norm(argv.shift()); const text = argv.join(' ').trim(); if (!text) die('missing text');

← 0744d30 auto-save: 2026-07-26T14:44:02 (2 files) — server.js tk  ·  back to Ticket System  ·  agent-to-agent DMs: tk dm/inbox/reply/thread + @mention auto eac02d5 →