baton-issue-tracker 1.11.2 → 1.12.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.12.0",
4
4
  "description": "A CLI issue tracker for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
package/source/cli.js CHANGED
@@ -27,6 +27,7 @@ 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
29
  import { run as runSubmit } from './commands/submit.js';
30
+ import { run as runUnclaim } from './commands/unclaim.js';
30
31
  import { run as runClaim } from './commands/claim.js';
31
32
 
32
33
  import { authenticateContext } from './services/authService.js';
@@ -47,6 +48,7 @@ Commands:
47
48
  create Creates an issue with specified fields
48
49
  approve Move an issue from in-review to closed
49
50
  submit Submit finished work for human review
51
+ unclaim Release a claimed issue back to Open
50
52
  claim Claim an issue as the authenticated agent
51
53
  priority Set an issue's priority level
52
54
  update Updates an issue's specified fields
@@ -77,6 +79,7 @@ Options:
77
79
  create --json Output as JSON (for AI agents)
78
80
  approve <id> [--json]
79
81
  submit <id> [--json]
82
+ unclaim <id> [--json]
80
83
  claim <id> [--json]
81
84
  reject <id> --reason <text> Reject an issue with a given reason
82
85
  priority <id> <level> [--json] low | medium | high
@@ -106,6 +109,7 @@ Examples:
106
109
  baton create --title "Refactor auth" --description "Clean up JWT logic" --token-limit 4000
107
110
  baton approve 5
108
111
  baton submit 14
112
+ baton unclaim 14
109
113
  baton claim 14
110
114
  baton priority 5 high
111
115
  baton priority 3 low
@@ -147,6 +151,7 @@ async function main() {
147
151
  delete: () => runDelete(args),
148
152
  log: () => runLog(args),
149
153
  submit: () => runSubmit(args),
154
+ unclaim: () => runUnclaim(args),
150
155
  };
151
156
 
152
157
  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
+ }
@@ -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
  */