baton-issue-tracker 1.11.2 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baton-issue-tracker",
3
- "version": "1.11.2",
3
+ "version": "1.13.0",
4
4
  "description": "A CLI issue tracker for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
package/source/cli.js CHANGED
@@ -26,7 +26,9 @@ import { run as runPriority } from './commands/priority.js';
26
26
  import { run as runLog } from './commands/log.js';
27
27
  import { run as runRegister } from './commands/register.js';
28
28
  import { run as runAgents } from './commands/agents.js';
29
+ import { run as runWhoami } from './commands/whoami.js';
29
30
  import { run as runSubmit } from './commands/submit.js';
31
+ import { run as runUnclaim } from './commands/unclaim.js';
30
32
  import { run as runClaim } from './commands/claim.js';
31
33
 
32
34
  import { authenticateContext } from './services/authService.js';
@@ -40,6 +42,7 @@ Commands:
40
42
  init Initialize storage and seed issues from product specs
41
43
  register Register a new AI agent or human user
42
44
  agents List all registered agents and humans
45
+ whoami Show the currently authenticated agent or user
43
46
  status Show issue counts and overall progress
44
47
  view View all issue fields for a given issue ID
45
48
  search Search issues by title and description (case insensitive)
@@ -47,6 +50,7 @@ Commands:
47
50
  create Creates an issue with specified fields
48
51
  approve Move an issue from in-review to closed
49
52
  submit Submit finished work for human review
53
+ unclaim Release a claimed issue back to Open
50
54
  claim Claim an issue as the authenticated agent
51
55
  priority Set an issue's priority level
52
56
  update Updates an issue's specified fields
@@ -62,6 +66,7 @@ Options:
62
66
  register --name <name> Name of the agent or user
63
67
  register --type <type> agent | human (default: agent)
64
68
  agents [--json]
69
+ whoami [--json]
65
70
  status --json Output as JSON (for AI agents)
66
71
  view <id> [--json]
67
72
  search <query> [--json]
@@ -77,6 +82,7 @@ Options:
77
82
  create --json Output as JSON (for AI agents)
78
83
  approve <id> [--json]
79
84
  submit <id> [--json]
85
+ unclaim <id> [--json]
80
86
  claim <id> [--json]
81
87
  reject <id> --reason <text> Reject an issue with a given reason
82
88
  priority <id> <level> [--json] low | medium | high
@@ -96,6 +102,7 @@ Examples:
96
102
  baton init --force
97
103
  baton register --name claude-dev --type agent
98
104
  baton agents
105
+ baton whoami
99
106
  baton status
100
107
  baton view 29
101
108
  baton search system
@@ -106,6 +113,7 @@ Examples:
106
113
  baton create --title "Refactor auth" --description "Clean up JWT logic" --token-limit 4000
107
114
  baton approve 5
108
115
  baton submit 14
116
+ baton unclaim 14
109
117
  baton claim 14
110
118
  baton priority 5 high
111
119
  baton priority 3 low
@@ -134,6 +142,7 @@ async function main() {
134
142
  init: () => runInit(args),
135
143
  register: () => runRegister(args),
136
144
  agents: () => runAgents(args),
145
+ whoami: () => runWhoami(args),
137
146
  status: () => runStatus(args),
138
147
  view: () => runView(args),
139
148
  search: () => runSearch(args),
@@ -147,6 +156,7 @@ async function main() {
147
156
  delete: () => runDelete(args),
148
157
  log: () => runLog(args),
149
158
  submit: () => runSubmit(args),
159
+ unclaim: () => runUnclaim(args),
150
160
  };
151
161
 
152
162
  const handler = handlers[command];
@@ -3,7 +3,7 @@
3
3
  // Usage: baton claim <id> [--json]
4
4
 
5
5
  import { isTrackerReady, claimIssue } from '../services/issuesService.js';
6
- import { getCurrentActor } from '../services/authService.js';
6
+ import { getCurrentActor } from '../services/context.js';
7
7
  import {
8
8
  hasFlag,
9
9
  renderOutput,
@@ -0,0 +1,104 @@
1
+ // unclaim.js
2
+ // Allows an agent to release an issue they have previously claimed, returning it to Open.
3
+ // The command verifies the authenticated actor is of type 'agent' and is the current assignee.
4
+ // Usage: baton unclaim <id> [--json]
5
+ //
6
+ // Options:
7
+ // --json Output as JSON (for AI agents)
8
+ // -h, --help Show this help
9
+ //
10
+ // Examples:
11
+ // baton unclaim 14
12
+
13
+ import { getIssue, unclaimIssue } from '../services/issuesService.js';
14
+ import { getCurrentActor } from '../services/context.js';
15
+ import { AgentType } from '../models/agents.js';
16
+ import {
17
+ hasFlag,
18
+ renderOutput,
19
+ renderError,
20
+ serializeIssue,
21
+ wantsHelp,
22
+ } from '../util.js';
23
+
24
+ const HELP = `baton unclaim — Release a claimed issue back to Open
25
+
26
+ Usage:
27
+ baton unclaim <id> [--json]
28
+
29
+ Options:
30
+ --json Output as JSON (for AI agents)
31
+ -h, --help Show this help
32
+
33
+ Examples:
34
+ baton unclaim 14
35
+ > Success: Issue #14 released by agent "claude-dev" and returned to Open.
36
+ `;
37
+
38
+ /**
39
+ * Releases an issue back to Open status, clearing the assignee.
40
+ * @param {string[]} args - The command line arguments
41
+ * @returns {Promise<number>} The exit code: 0 is success, 1 is error
42
+ */
43
+ export async function run(args) {
44
+ if (wantsHelp(args)) {
45
+ console.log(HELP);
46
+ return 0;
47
+ }
48
+
49
+ const isJson = hasFlag(args, '--json');
50
+
51
+ const idArgs = args.filter((arg) => !arg.startsWith('-'));
52
+ if (idArgs.length === 0) {
53
+ renderError(isJson, 'Missing issue ID.\nUsage: baton unclaim <id>', 'MISSING_ID');
54
+ return 1;
55
+ }
56
+
57
+ const id = Number(idArgs[0]);
58
+ if (!Number.isInteger(id) || id <= 0) {
59
+ renderError(isJson, `Invalid ID "${idArgs[0]}". ID must be a positive integer.`, 'INVALID_ID');
60
+ return 1;
61
+ }
62
+
63
+ const actor = getCurrentActor();
64
+
65
+ if (!actor || actor.type !== AgentType.AGENT) {
66
+ renderError(isJson, 'Only agents can unclaim issues.', 'FORBIDDEN');
67
+ return 1;
68
+ }
69
+
70
+ let issue;
71
+ try {
72
+ issue = getIssue(id);
73
+ } catch (error) {
74
+ if (error.message.includes('not found')) {
75
+ renderError(isJson, error.message, 'NOT_FOUND');
76
+ } else {
77
+ renderError(isJson, error.message);
78
+ }
79
+ return 1;
80
+ }
81
+
82
+ if (issue.assigneeId !== actor.id) {
83
+ renderError(
84
+ isJson,
85
+ `Issue #${id} is not assigned to you. Only the current assignee can unclaim an issue.`,
86
+ 'FORBIDDEN',
87
+ );
88
+ return 1;
89
+ }
90
+
91
+ try {
92
+ const updatedIssue = unclaimIssue(id);
93
+ const envelope = { status: 'success', issue: serializeIssue(updatedIssue) };
94
+
95
+ renderOutput(isJson, envelope, () => {
96
+ console.log(`Success: Issue #${id} released by agent "${actor.name}" and returned to Open.`);
97
+ });
98
+
99
+ return 0;
100
+ } catch (error) {
101
+ renderError(isJson, error.message);
102
+ return 1;
103
+ }
104
+ }
@@ -0,0 +1,54 @@
1
+ // whoami.js
2
+ // Shows the currently authenticated agent or user.
3
+ // Usage: baton whoami [--json]
4
+ //
5
+ // Options:
6
+ // --json Output as JSON (for AI agents)
7
+ // -h, --help Show this help
8
+ //
9
+ // Examples:
10
+ // baton whoami
11
+
12
+ import { getCurrentActor } from '../services/authService.js';
13
+ import { hasFlag, renderOutput, renderError, wantsHelp } from '../util.js';
14
+
15
+ const VALID_FLAGS = ['--json', '-h', '--help'];
16
+
17
+ function serializeActor(actor) {
18
+ return {
19
+ id: actor.id,
20
+ name: actor.name,
21
+ type: actor.type,
22
+ };
23
+ }
24
+
25
+ export async function run(args = []) {
26
+ const isJson = hasFlag(args, '--json');
27
+
28
+ if (wantsHelp(args)) {
29
+ console.log(
30
+ `Usage: baton whoami [--json]\n\nOptions:\n --json Output as JSON (for AI agents)\n -h, --help Show this help\n\nExamples:\n baton whoami`,
31
+ );
32
+ return 0;
33
+ }
34
+
35
+ for (const arg of args) {
36
+ if (arg.startsWith('-') && !VALID_FLAGS.includes(arg)) {
37
+ throw new Error(`Unknown flag provided: ${arg}.\nFlags: --json, -h, --help`);
38
+ }
39
+ }
40
+
41
+ const actor = getCurrentActor();
42
+ if (!actor) {
43
+ renderError(isJson, 'No authenticated actor found. Please sign in with a registered agent.', 'UNAUTHORIZED');
44
+ return 1;
45
+ }
46
+
47
+ const envelope = { status: 'success', actor: serializeActor(actor) };
48
+
49
+ renderOutput(isJson, envelope, () => {
50
+ console.log(`Active Agent: "${actor.name}" (ID: ${actor.id}, Type: ${actor.type})`);
51
+ });
52
+
53
+ return 0;
54
+ }
@@ -41,6 +41,22 @@ export function getAgentByName(name) {
41
41
  }
42
42
  }
43
43
 
44
+ /**
45
+ * Retrieves an agent by their unique ID.
46
+ * @param {number} id
47
+ * @returns {Agent|null}
48
+ */
49
+ export function getAgentById(id) {
50
+ const db = getDB();
51
+ const row = db.select().from(agentsTable).where(eq(agentsTable.id, id)).get();
52
+
53
+ if (row) {
54
+ return new Agent(row);
55
+ } else {
56
+ return null;
57
+ }
58
+ }
59
+
44
60
  /**
45
61
  * Lists all registered agents and humans, ordered by id.
46
62
  * @returns {Agent[]}
@@ -1,8 +1,8 @@
1
1
  import os from 'os';
2
2
  import { getAgentByName } from './agentsService.js';
3
- import { setActiveActor } from './issuesService.js';
3
+ import { setCurrentActor, getCurrentActor } from './context.js';
4
4
 
5
- let currentActor = null;
5
+ export { getCurrentActor };
6
6
 
7
7
  /**
8
8
  * Authenticates the current execution context.
@@ -12,17 +12,14 @@ let currentActor = null;
12
12
  * @param {string} command - The command being executed
13
13
  */
14
14
  export function authenticateContext(command) {
15
- // Commands that don't require an active agent signed in
16
15
  const exemptCommands = ['init', 'register', 'help'];
17
16
  if (exemptCommands.includes(command)) {
18
17
  return;
19
18
  }
20
19
 
21
- // Identify who is running the CLI
22
20
  const activeName = process.env.BATON_AGENT || os.userInfo().username;
23
-
21
+
24
22
  try {
25
- // Look them up in the database
26
23
  const agent = getAgentByName(activeName);
27
24
 
28
25
  if (!agent) {
@@ -31,25 +28,12 @@ export function authenticateContext(command) {
31
28
  process.exit(1);
32
29
  }
33
30
 
34
- // Set the global state for the remainder of this command's lifecycle
35
- currentActor = agent;
36
- setActiveActor(agent.id);
31
+ setCurrentActor(agent);
37
32
  } catch (error) {
38
- // Gracefully handle the scenario where the database hasn't been initialized yet
39
33
  if (error.message && error.message.includes('no such table: agents')) {
40
34
  console.error("Error: Tracker is not initialized. Please run 'baton init' first.");
41
35
  process.exit(1);
42
36
  }
43
-
44
- // Rethrow if it's a completely different error
45
37
  throw error;
46
38
  }
47
- }
48
-
49
- /**
50
- * Returns the authenticated actor object for this session.
51
- * @returns {object|null}
52
- */
53
- export function getCurrentActor() {
54
- return currentActor;
55
39
  }
@@ -0,0 +1,25 @@
1
+ let currentActor = null;
2
+
3
+ /**
4
+ * Sets the active actor for this session.
5
+ * @param {object|null} actor - The authenticated agent/user object, or null to clear.
6
+ */
7
+ export function setCurrentActor(actor) {
8
+ currentActor = actor;
9
+ }
10
+
11
+ /**
12
+ * Returns the full authenticated actor object for this session.
13
+ * @returns {object|null}
14
+ */
15
+ export function getCurrentActor() {
16
+ return currentActor;
17
+ }
18
+
19
+ /**
20
+ * Returns the ID of the authenticated actor, or null if not set.
21
+ * @returns {number|null}
22
+ */
23
+ export function getCurrentActorId() {
24
+ return currentActor?.id ?? null;
25
+ }
@@ -7,17 +7,7 @@ import {
7
7
  Priority,
8
8
  } from "../models/issue.js";
9
9
  import { ActivityLog, Action } from '../models/activityLog.js';
10
-
11
- // Internal variable to hold the active agent's ID for logging purposes. Set via setActiveActor() during authentication.
12
- let currentActorId = null;
13
-
14
- /**
15
- * Sets the active actor ID for logging purposes.
16
- * @param {number} id - The ID of the active actor.
17
- */
18
- export function setActiveActor(id) {
19
- currentActorId = id;
20
- }
10
+ import { getCurrentActorId } from './context.js';
21
11
 
22
12
  /**
23
13
  * Internal helper to log actions.
@@ -29,18 +19,10 @@ export function setActiveActor(id) {
29
19
  */
30
20
  function logActivity(db, issueId, action, details = null) {
31
21
  db.insert(activityTable)
32
- .values({ issueId, action, details, actorId: currentActorId })
22
+ .values({ issueId, action, details, actorId: getCurrentActorId() })
33
23
  .run();
34
24
  }
35
25
 
36
- /**
37
- * Returns the active actor ID set during authentication.
38
- * @returns {number|null}
39
- */
40
- export function getActiveActor() {
41
- return currentActorId;
42
- }
43
-
44
26
  /**
45
27
  * Convert a raw database row to an Issue instance.
46
28
  * @private
@@ -491,10 +473,10 @@ export function claimIssue(issueId) {
491
473
  logActivity(tx, issueId, Action.READ, `Agent accessed issue #${issueId}`);
492
474
 
493
475
  tx.update(issuesTable)
494
- .set({
476
+ .set({
495
477
  status: Status.IN_PROGRESS,
496
- assigneeId: currentActorId,
497
- attemptNum: sql`${issuesTable.attemptNum} + 1`
478
+ assigneeId: getCurrentActorId(),
479
+ attemptNum: sql`${issuesTable.attemptNum} + 1`
498
480
  })
499
481
  .where(eq(issuesTable.id, issueId))
500
482
  .run();
@@ -517,6 +499,36 @@ export function claimIssue(issueId) {
517
499
  return findById(db, issueId);
518
500
  }
519
501
 
502
+ /**
503
+ * Release an issue back to Open status, clearing the assignee.
504
+ * Logs a STATE_CHANGE activity entry.
505
+ * @param {number} issueId
506
+ * @returns {Issue}
507
+ * @throws {Error} If issueId is invalid or issue is not found
508
+ */
509
+ export function unclaimIssue(issueId) {
510
+ if (!Number.isInteger(issueId)) {
511
+ throw new Error('issueId must be an integer');
512
+ }
513
+
514
+ const db = getDB();
515
+ findById(db, issueId);
516
+
517
+ db.update(issuesTable)
518
+ .set({ status: Status.OPEN, assigneeId: null })
519
+ .where(eq(issuesTable.id, issueId))
520
+ .run();
521
+
522
+ logActivity(
523
+ db,
524
+ issueId,
525
+ Action.STATE_CHANGE,
526
+ `Issue #${issueId} was released and returned to Open.`,
527
+ );
528
+
529
+ return getIssue(issueId);
530
+ }
531
+
520
532
  /**
521
533
  * Remove all issues (`baton init --force`). Activity rows are kept for audit.
522
534
  */