[object Object]

← back to Ticket System

front desk review fixes (TK-12235): tk claim/renew/release reject unknown flags + help intercept + claim --dry; renew/release require the lease holder (force+reason override, logged)

312dca6672a481c2c1a4397f99a2db12f1085ddf · 2026-09-25 10:18:58 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJmtLm2HzLnqEsye4iifpy

Files touched

Diff

commit 312dca6672a481c2c1a4397f99a2db12f1085ddf
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 25 10:18:58 2026 -0700

    front desk review fixes (TK-12235): tk claim/renew/release reject unknown flags + help intercept + claim --dry; renew/release require the lease holder (force+reason override, logged)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LJmtLm2HzLnqEsye4iifpy
---
 lib.js    | 12 ++++++++++--
 server.js |  4 ++--
 tk        | 27 ++++++++++++++++++++-------
 3 files changed, 32 insertions(+), 11 deletions(-)

diff --git a/lib.js b/lib.js
index 703fa545..777a1611 100644
--- a/lib.js
+++ b/lib.js
@@ -356,11 +356,15 @@ function claim({ machine = '', engine = '', agent = 'codex', projects, dry = fal
 }
 // Extend a live lease by LEASE_MS. Fails (throws) if lease_id doesn't match
 // the current live lease, or the lease already expired.
-function renew({ id, lease_id }) {
+function renew({ id, lease_id, agent, force }) {
   id = requireTicket(id);
   return withLock(() => {
     const l = leases().get(id);
     if (!l || l.lease_id !== lease_id) throw new Error('no matching live lease for ' + id);
+    if (agent && l.agent && agent !== l.agent) {
+      if (!force) throw new Error(`lease on ${id} is held by ${l.agent}, not ${agent} (pass force with a reason to override)`);
+      append({ ts: now(), type: 'comment', id, kind: 'comment', agent, text: `FORCED renew of lease held by ${l.agent}: ${force}` });
+    }
     const exp = Date.parse(l.lease_expires);
     if (!Number.isFinite(exp) || exp <= Date.now()) throw new Error('lease expired for ' + id);
     const tsNow = now();
@@ -371,11 +375,15 @@ function renew({ id, lease_id }) {
 }
 // End a lease (matching lease_id required, expired or not — so a stale local
 // lease can always be cleaned up), optionally also setting ticket status.
-function release({ id, lease_id, status }) {
+function release({ id, lease_id, status, agent, force }) {
   id = requireTicket(id);
   return withLock(() => {
     const l = leases().get(id);
     if (!l || l.lease_id !== lease_id) throw new Error('no matching live lease for ' + id);
+    if (agent && l.agent && agent !== l.agent) {
+      if (!force) throw new Error(`lease on ${id} is held by ${l.agent}, not ${agent} (pass force with a reason to override)`);
+      append({ ts: now(), type: 'comment', id, kind: 'comment', agent, text: `FORCED release of lease held by ${l.agent}: ${force}` });
+    }
     const tsNow = now();
     append({ ts: tsNow, type: 'lease_end', id, lease_id, agent: l.agent, reason: 'release' });
     if (status) {
diff --git a/server.js b/server.js
index b6551a4b..8a36017d 100644
--- a/server.js
+++ b/server.js
@@ -690,7 +690,7 @@ http.createServer((req, res) => {
       if (!body) return json(res, 400, { error: 'bad json' });
       const id = resolveId(body.id, cachedTickets());
       if (!id) return json(res, 400, { error: 'no such ticket ' + body.id });
-      try { const r = renewLease({ id, lease_id: body.lease_id }); json(res, 200, { renewed: r }); }
+      try { const r = renewLease({ id, lease_id: body.lease_id, agent: body.agent, force: body.force }); json(res, 200, { renewed: r }); }
       catch (e) { json(res, 409, { error: String(e.message || e) }); }
     });
   }
@@ -699,7 +699,7 @@ http.createServer((req, res) => {
       if (!body) return json(res, 400, { error: 'bad json' });
       const id = resolveId(body.id, cachedTickets());
       if (!id) return json(res, 400, { error: 'no such ticket ' + body.id });
-      try { const r = releaseLease({ id, lease_id: body.lease_id, status: body.status }); invalidateViews(); json(res, 200, { released: r }); }
+      try { const r = releaseLease({ id, lease_id: body.lease_id, status: body.status, agent: body.agent, force: body.force }); invalidateViews(); json(res, 200, { released: r }); }
       catch (e) { json(res, 409, { error: String(e.message || e) }); }
     });
   }
diff --git a/tk b/tk
index 17a6276d..855dfb35 100755
--- a/tk
+++ b/tk
@@ -200,25 +200,38 @@ if (cmd === 'new') {
   console.log(`${id} → ${agent} (doing)${force && lease ? ' (forced over lease)' : ''}`);
   titleWindow(id);
 } else if (cmd === 'claim') {
+  // TK-12235 review: `tk claim --help` performed a REAL claim (unknown flags were ignored).
+  // Help prints usage; --dry previews without writing; any other leftover arg is refused.
+  const CLAIM_USAGE = 'usage: tk claim [--engine X] [--machine Y] [--projects a,b] [--dry]';
+  if (argv.some(a => /^(--help|-h|--h)$/i.test(a))) { console.log(CLAIM_USAGE); process.exit(0); }
+  const dry = flagSet('--dry');
   const engine = opt('--engine') || '';
   const machine = opt('--machine') || os.hostname();
   const projectsRaw = opt('--projects');
   const projects = projectsRaw ? projectsRaw.split(',').map(s => s.trim()).filter(Boolean) : undefined;
+  if (argv.length) die(`tk claim: unknown argument(s): ${argv.join(' ')}\n${CLAIM_USAGE}`);
   let result;
-  try { result = claim({ machine, engine, agent, projects }); }
+  try { result = claim({ machine, engine, agent, projects, dry }); }
   catch (e) { die('tk claim failed: ' + e.message); }
   if (!result) { console.log('(no eligible ticket)'); process.exit(0); }
+  if (dry) { console.log(`[dry] would claim ${result.id}${result.title ? ' — ' + result.title : ''}`); process.exit(0); }
   console.log(`${result.id} → ${agent} (doing) lease_id=${result.lease_id} expires=${result.lease_expires}${result.title ? ' — ' + result.title : ''}`);
   titleWindow(result.id);
 } else if (cmd === 'renew') {
-  const id = norm(argv.shift()); const lease_id = opt('--lease-id') || opt('-l');
-  if (!lease_id) die('usage: tk renew TK-3 --lease-id L-xxx');
-  try { const r = renew({ id, lease_id }); console.log(`${id} lease renewed → expires=${r.lease_expires}`); }
+  const RENEW_USAGE = 'usage: tk renew TK-3 --lease-id L-xxx [--force "reason"]';
+  if (argv.some(a => /^(--help|-h|--h)$/i.test(a))) { console.log(RENEW_USAGE); process.exit(0); }
+  const id = norm(argv.shift()); const lease_id = opt('--lease-id') || opt('-l'); const force = opt('--force');
+  if (!lease_id) die(RENEW_USAGE);
+  if (argv.length) die(`tk renew: unknown argument(s): ${argv.join(' ')}\n${RENEW_USAGE}`);
+  try { const r = renew({ id, lease_id, agent, force }); console.log(`${id} lease renewed → expires=${r.lease_expires}`); }
   catch (e) { die('tk renew failed: ' + e.message); }
 } else if (cmd === 'release') {
-  const id = norm(argv.shift()); const lease_id = opt('--lease-id') || opt('-l'); const status = opt('--status');
-  if (!lease_id) die('usage: tk release TK-3 --lease-id L-xxx [--status open|blocked]');
-  try { release({ id, lease_id, status }); console.log(`${id} lease released${status ? ' → ' + status : ''}`); }
+  const RELEASE_USAGE = 'usage: tk release TK-3 --lease-id L-xxx [--status open|blocked] [--force "reason"]';
+  if (argv.some(a => /^(--help|-h|--h)$/i.test(a))) { console.log(RELEASE_USAGE); process.exit(0); }
+  const id = norm(argv.shift()); const lease_id = opt('--lease-id') || opt('-l'); const status = opt('--status'); const force = opt('--force');
+  if (!lease_id) die(RELEASE_USAGE);
+  if (argv.length) die(`tk release: unknown argument(s): ${argv.join(' ')}\n${RELEASE_USAGE}`);
+  try { release({ id, lease_id, status, agent, force }); console.log(`${id} lease released${status ? ' → ' + status : ''}`); }
   catch (e) { die('tk release failed: ' + e.message); }
 } else if (cmd === 'status') {
   const id = norm(argv.shift()); const s = (argv.shift() || '').toLowerCase();

← 5b093eee Front desk: atomic lease-based ticket claim (TK-12235)  ·  back to Ticket System  ·  auto-data-snapshot: 2026-09-25T10:22:25 (2 data files) — dat b4c361a8 →