baton-issue-tracker 1.9.0 → 1.11.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.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "A CLI issue tracker for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
package/source/cli.js CHANGED
@@ -29,6 +29,8 @@ import { run as runDelete } from './commands/delete.js';
29
29
  import { run as runPriority } from './commands/priority.js';
30
30
  import { run as runLog } from './commands/log.js';
31
31
  import { run as runRegister } from './commands/register.js';
32
+ import { run as runAgents } from './commands/agents.js';
33
+ import { run as runSubmit } from './commands/submit.js';
32
34
 
33
35
  import { authenticateContext } from './services/authService.js';
34
36
 
@@ -40,6 +42,7 @@ Usage:
40
42
  Commands:
41
43
  init Initialize storage and seed issues from product specs
42
44
  register Register a new AI agent or human user
45
+ agents List all registered agents and humans
43
46
  next Work on the highest-priority open issue
44
47
  loop Run the agent autonomously for multiple steps
45
48
  status Show issue counts and overall progress
@@ -48,6 +51,7 @@ Commands:
48
51
  list Lists issues filtered by status and priority
49
52
  create Creates an issue with specified fields
50
53
  approve Move an issue from in-review to closed
54
+ submit Submit finished work for human review
51
55
  priority Set an issue's priority level
52
56
  update Updates an issue's specified fields
53
57
  delete Deletes an issue
@@ -61,6 +65,7 @@ Options:
61
65
  Default specs: docs/specs/project-requirements.md
62
66
  register --name <name> Name of the agent or user
63
67
  register --type <type> agent | human (default: agent)
68
+ agents [--json]
64
69
  loop --steps <n> Number of autonomous steps (alias: -n)
65
70
  loop -n <n>
66
71
  loop --json Output as JSON (for AI agents)
@@ -79,6 +84,7 @@ Options:
79
84
  create --token-limit <n> Optional token budget for this issue
80
85
  create --json Output as JSON (for AI agents)
81
86
  approve <id> [--json]
87
+ submit <id> [--json]
82
88
  reject <id> --reason <text> Reject an issue with a given reason
83
89
  priority <id> <level> [--json] low | medium | high
84
90
  update --title <text> New title
@@ -96,6 +102,7 @@ Examples:
96
102
  baton init ./my-specs.md
97
103
  baton init --force
98
104
  baton register --name claude-dev --type agent
105
+ baton agents
99
106
  baton next
100
107
  baton loop --steps 5
101
108
  baton status
@@ -107,6 +114,7 @@ Examples:
107
114
  baton create --title "Fix login bug" --priority high
108
115
  baton create --title "Refactor auth" --description "Clean up JWT logic" --token-limit 4000
109
116
  baton approve 5
117
+ baton submit 14
110
118
  baton priority 5 high
111
119
  baton priority 3 low
112
120
  baton update 3 --title "Revised title"
@@ -133,6 +141,7 @@ async function main() {
133
141
  const handlers = {
134
142
  init: () => runInit(args),
135
143
  register: () => runRegister(args),
144
+ agents: () => runAgents(args),
136
145
  next: () => runNext(args),
137
146
  loop: () => runLoop(args),
138
147
  status: () => runStatus(args),
@@ -146,6 +155,7 @@ async function main() {
146
155
  update: () => runUpdate(args),
147
156
  delete: () => runDelete(args),
148
157
  log: () => runLog(args),
158
+ submit: () => runSubmit(args),
149
159
  };
150
160
 
151
161
  const handler = handlers[command];
@@ -0,0 +1,78 @@
1
+ // agents.js
2
+ // Lists all registered agents and humans in the tracker.
3
+ // Usage: baton agents [--json]
4
+ //
5
+ // Options:
6
+ // --json Output as JSON (for AI agents)
7
+ // -h, --help Show this help
8
+ //
9
+ // Examples:
10
+ // baton agents
11
+
12
+ import { listAgents } from '../services/agentsService.js';
13
+ import { hasFlag, renderOutput } from '../util.js';
14
+
15
+ const VALID_FLAGS = ['--json'];
16
+
17
+ const COL = { id: 4, name: 14, type: 5 };
18
+
19
+ /**
20
+ * @param {Agent} agent
21
+ * @returns {{ id: number, name: string, type: string }}
22
+ */
23
+ function serializeAgent(agent) {
24
+ return {
25
+ id: agent.id,
26
+ name: agent.name,
27
+ type: agent.type,
28
+ };
29
+ }
30
+
31
+ /**
32
+ * @param {Agent[]} agents
33
+ */
34
+ function printAgentsTable(agents) {
35
+ console.log(
36
+ `${'ID'.padEnd(COL.id)} | ${'Name'.padEnd(COL.name)} | Type`,
37
+ );
38
+ for (const agent of agents) {
39
+ console.log(
40
+ `${String(agent.id).padEnd(COL.id)} | ${agent.name.padEnd(COL.name)} | ${agent.type}`,
41
+ );
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Lists all registered agents and humans.
47
+ * @param {string[]} args - The command line arguments
48
+ * @returns {Promise<number>} The exit code: 0 is success, 1 is error
49
+ */
50
+ export async function run(args = []) {
51
+ const isJson = hasFlag(args, '--json');
52
+
53
+ for (const arg of args) {
54
+ if (arg.startsWith('--') && !VALID_FLAGS.includes(arg)) {
55
+ throw new Error(`Unknown flag provided: ${arg}.\nFlags: --json`);
56
+ }
57
+ }
58
+
59
+ try {
60
+ const agents = listAgents();
61
+ const records = agents.map(serializeAgent);
62
+ const envelope = { status: 'success', count: records.length, agents: records };
63
+
64
+ renderOutput(isJson, envelope, (data) => {
65
+ if (data.count === 0) {
66
+ console.log('No registered agents or humans found.');
67
+ return;
68
+ }
69
+ printAgentsTable(agents);
70
+ });
71
+
72
+ return 0;
73
+ } catch (error) {
74
+ console.error('Error: Failed to list agents.');
75
+ console.error(error.message);
76
+ return 1;
77
+ }
78
+ }
@@ -0,0 +1,76 @@
1
+ // submit.js
2
+ // AI was consulted to generate the contents of the file. It was then reviewed and edited by a human.
3
+ // Allows an agent to submit "finished" work for human review.
4
+ // The issue must be in the in-progress status to be submitted for review.
5
+ // Usage: baton submit <id> [--json]
6
+ //
7
+ // Options:
8
+ // --json Output as JSON (for AI agents)
9
+ //
10
+ // Examples:
11
+ // baton submit 14
12
+
13
+ import { getIssue, submitForReview } from '../services/issuesService.js';
14
+ import { Status } from '../models/issue.js';
15
+ import {
16
+ hasFlag,
17
+ renderOutput,
18
+ renderError,
19
+ serializeIssue,
20
+ } from '../util.js';
21
+
22
+ /**
23
+ * Submits an in-progress issue for human review.
24
+ * @param {string[]} args - The command line arguments
25
+ * @returns {Promise<number>} The exit code: 0 is success, 1 is error
26
+ */
27
+ export async function run(args) {
28
+ const isJson = hasFlag(args, '--json');
29
+
30
+ const idArgs = args.filter((arg) => arg !== '--json');
31
+ if (idArgs.length === 0) {
32
+ renderError(isJson, 'Missing issue ID.\nUsage: baton submit <id>', 'MISSING_ID');
33
+ return 1;
34
+ }
35
+
36
+ const id = Number(idArgs[0]);
37
+ if (!Number.isInteger(id)) {
38
+ renderError(isJson, `Invalid ID "${idArgs[0]}". ID must be an integer.`, 'INVALID_ID');
39
+ return 1;
40
+ }
41
+
42
+ let issue;
43
+ try {
44
+ issue = getIssue(id);
45
+ } catch (error) {
46
+ if (error.message.includes('not found')) {
47
+ renderError(isJson, error.message, 'NOT_FOUND');
48
+ } else {
49
+ renderError(isJson, error.message);
50
+ }
51
+ return 1;
52
+ }
53
+
54
+ if (issue.status !== Status.IN_PROGRESS) {
55
+ renderError(
56
+ isJson,
57
+ `Issue #${id} is currently "${issue.status}". Only issues in "${Status.IN_PROGRESS}" can be submitted for review.`,
58
+ 'INVALID_STATE',
59
+ );
60
+ return 1;
61
+ }
62
+
63
+ try {
64
+ const updatedIssue = submitForReview(id);
65
+ const envelope = { status: 'success', issue: serializeIssue(updatedIssue) };
66
+
67
+ renderOutput(isJson, envelope, () => {
68
+ console.log(`Success: Issue #${id} submitted for review.`);
69
+ });
70
+
71
+ return 0;
72
+ } catch (error) {
73
+ renderError(isJson, error.message);
74
+ return 1;
75
+ }
76
+ }
@@ -39,4 +39,14 @@ export function getAgentByName(name) {
39
39
  } else {
40
40
  return null;
41
41
  }
42
+ }
43
+
44
+ /**
45
+ * Lists all registered agents and humans, ordered by id.
46
+ * @returns {Agent[]}
47
+ */
48
+ export function listAgents() {
49
+ const db = getDB();
50
+ const rows = db.select().from(agentsTable).orderBy(agentsTable.id).all();
51
+ return rows.map((row) => new Agent(row));
42
52
  }
@@ -33,6 +33,14 @@ function logActivity(db, issueId, action, details = null) {
33
33
  .run();
34
34
  }
35
35
 
36
+ /**
37
+ * Returns the active actor ID set during authentication.
38
+ * @returns {number|null}
39
+ */
40
+ export function getActiveActor() {
41
+ return currentActorId;
42
+ }
43
+
36
44
  /**
37
45
  * Convert a raw database row to an Issue instance.
38
46
  * @private
@@ -280,16 +288,37 @@ export function rejectIssue(id, reason) {
280
288
  }
281
289
 
282
290
  /**
283
- * Change the status of an issue to in-review.
284
- * Logs a state_change_event.
285
- * @param {number} id
291
+ * Change the status of an issue from in-progress to in-review.
292
+ * Logs a state_change event attributed to the current actor.
293
+ * @param {number} issueId
286
294
  * @returns {Issue}
295
+ * @throws {Error} If issueId is invalid, issue is not found, or status is not in-progress
287
296
  */
288
- export function submitForReview(id) {
297
+ export function submitForReview(issueId) {
298
+ if (!Number.isInteger(issueId)) {
299
+ throw new Error('issueId must be an integer');
300
+ }
301
+
289
302
  const db = getDB();
290
- db.update(issuesTable).set({ status: Status.IN_REVIEW }).where(eq(issuesTable.id, id)).run();
291
- logActivity(db, id, Action.STATE_CHANGE, `Issue #${id} was submitted for review.`);
292
- return getIssue(id);
303
+ const existing = findById(db, issueId);
304
+
305
+ if (existing.status !== Status.IN_PROGRESS) {
306
+ throw new Error(
307
+ `Issue #${issueId} is currently "${existing.status}". Only issues in "${Status.IN_PROGRESS}" can be submitted for review.`,
308
+ );
309
+ }
310
+
311
+ db.update(issuesTable)
312
+ .set({ status: Status.IN_REVIEW })
313
+ .where(eq(issuesTable.id, issueId))
314
+ .run();
315
+ logActivity(
316
+ db,
317
+ issueId,
318
+ Action.STATE_CHANGE,
319
+ `Issue #${issueId} was submitted for review.`,
320
+ );
321
+ return getIssue(issueId);
293
322
  }
294
323
 
295
324
  /**