baton-issue-tracker 1.5.0 → 1.7.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.
@@ -3,6 +3,12 @@ import { sql } from "drizzle-orm";
3
3
  import { Status, Priority } from "./issue.js";
4
4
  import { Action } from "./activityLog.js";
5
5
 
6
+ export const agentsTable = sqliteTable("agents", {
7
+ id: int().primaryKey({ autoIncrement: true }),
8
+ name: text().notNull().unique(),
9
+ type: text({ enum: ['agent', 'human'] }).notNull(),
10
+ });
11
+
6
12
  export const issuesTable = sqliteTable("issues", {
7
13
  id: int().primaryKey({ autoIncrement: true }),
8
14
  createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
@@ -13,7 +19,7 @@ export const issuesTable = sqliteTable("issues", {
13
19
  priority: text({ enum: Object.values(Priority) }).default(Priority.LOW),
14
20
  tokenLimit: int("token_limit"),
15
21
  description: text(),
16
- assignees: text({mode: "json"}).default(sql`'[]'`),
22
+ assigneeId: int("assignee_id").references(() => agentsTable.id),
17
23
  });
18
24
 
19
25
  export const activityTable = sqliteTable(
@@ -21,6 +27,7 @@ export const activityTable = sqliteTable(
21
27
  {
22
28
  logId: int("log_id").primaryKey({ autoIncrement: true }),
23
29
  issueId: int("issue_id"),
30
+ actorId: int("actor_id").references(() => agentsTable.id),
24
31
  createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
25
32
  action: text({ enum: Object.values(Action) }).notNull(),
26
33
  details: text(),
@@ -46,8 +53,8 @@ export const issueSchema = {
46
53
  tokenLimit: { flag: '--token-limit', type: 'number' },
47
54
  priority: { flag: '--priority', type: 'enum', values: Object.values(Priority) },
48
55
  description:{ flag: '--description', type: 'string' },
49
- //assignees: { flag: '--assignees', type: 'json'},
56
+ assigneeId: { flag: '--assignee-id', type: 'number'},
50
57
  // Pagination options
51
58
  limit: { flag: '--limit', type: 'number' },
52
- offset: { flag: '--offset', type: 'number' }
59
+ offset: { flag: '--offset', type: 'number' }
53
60
  };
@@ -0,0 +1,42 @@
1
+ import { getDB } from '../db/index.js';
2
+ import { eq } from "drizzle-orm";
3
+ import { agentsTable } from "../models/schema.js";
4
+ import { Agent } from '../models/agents.js';
5
+
6
+ /**
7
+ * Registers a new agent in the database.
8
+ * @param {string} name
9
+ * @param {string} type
10
+ * @returns {Agent|null}
11
+ */
12
+ export function registerAgent(name, type) {
13
+ const db = getDB();
14
+ const result = db.insert(agentsTable)
15
+ .values({ name: name, type: type })
16
+ .returning({ id: agentsTable.id })
17
+ .get();
18
+
19
+ const row = db.select().from(agentsTable).where(eq(agentsTable.id, result.id)).get();
20
+
21
+ if (row) {
22
+ return new Agent(row);
23
+ } else {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Retrieves an agent by their unique name.
30
+ * @param {string} name
31
+ * @returns {Agent|null}
32
+ */
33
+ export function getAgentByName(name) {
34
+ const db = getDB();
35
+ const row = db.select().from(agentsTable).where(eq(agentsTable.name, name)).get();
36
+
37
+ if (row) {
38
+ return new Agent(row);
39
+ } else {
40
+ return null;
41
+ }
42
+ }
@@ -0,0 +1,44 @@
1
+ import os from 'os';
2
+ import { getAgentByName } from './agentsService.js';
3
+ import { setActiveActor } from './issuesService.js';
4
+
5
+ /**
6
+ * Authenticates the current execution context.
7
+ * Looks for BATON_AGENT in the environment, falling back to the OS username.
8
+ * If the agent/user is registered, it sets them as the active actor for this session.
9
+ * If not, it terminates the process with a helpful error message.
10
+ * @param {string} command - The command being executed
11
+ */
12
+ export function authenticateContext(command) {
13
+ // Commands that don't require an active agent signed in
14
+ const exemptCommands = ['init', 'register', 'help'];
15
+ if (exemptCommands.includes(command)) {
16
+ return;
17
+ }
18
+
19
+ // Identify who is running the CLI
20
+ const activeName = process.env.BATON_AGENT || os.userInfo().username;
21
+
22
+ try {
23
+ // Look them up in the database
24
+ const agent = getAgentByName(activeName);
25
+
26
+ if (!agent) {
27
+ console.error(`Error: Agent/User "${activeName}" is not registered in this Baton tracker.`);
28
+ console.error(`Please run 'baton register --name "${activeName}" --type "human"' (or "agent") first.`);
29
+ process.exit(1);
30
+ }
31
+
32
+ // Set the global state for the remainder of this command's lifecycle
33
+ setActiveActor(agent.id);
34
+ } catch (error) {
35
+ // Gracefully handle the scenario where the database hasn't been initialized yet
36
+ if (error.message && error.message.includes('no such table: agents')) {
37
+ console.error("Error: Tracker is not initialized. Please run 'baton init' first.");
38
+ process.exit(1);
39
+ }
40
+
41
+ // Rethrow if it's a completely different error
42
+ throw error;
43
+ }
44
+ }
@@ -8,6 +8,17 @@ import {
8
8
  } from "../models/issue.js";
9
9
  import { ActivityLog, Action } from '../models/activityLog.js';
10
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
+ }
21
+
11
22
  /**
12
23
  * Internal helper to log actions.
13
24
  * @private
@@ -18,7 +29,7 @@ import { ActivityLog, Action } from '../models/activityLog.js';
18
29
  */
19
30
  function logActivity(db, issueId, action, details = null) {
20
31
  db.insert(activityTable)
21
- .values({ issueId, action, details })
32
+ .values({ issueId, action, details, actorId: currentActorId })
22
33
  .run();
23
34
  }
24
35
 
@@ -65,6 +76,7 @@ function findById(db, id) {
65
76
  * @property {string} [priority] - The priority level.
66
77
  * @property {number} [tokenLimit] - The maximum token limit for the issue.
67
78
  * @property {string} [description] - The detailed description of the issue.
79
+ * @property {number} [assigneeId] - The assigned agent ID.
68
80
  */
69
81
 
70
82
  /**
@@ -78,6 +90,7 @@ export function createIssue({
78
90
  priority,
79
91
  tokenLimit,
80
92
  description,
93
+ assigneeId,
81
94
  } = {}) {
82
95
 
83
96
  const db = getDB();
@@ -87,6 +100,7 @@ export function createIssue({
87
100
  priority: priority ?? Priority.LOW,
88
101
  tokenLimit: tokenLimit ?? null,
89
102
  description: description ?? null,
103
+ assigneeId: assigneeId ?? null,
90
104
  })
91
105
  .returning({ id: issuesTable.id })
92
106
  .get();
@@ -116,6 +130,7 @@ export function getIssue(id) {
116
130
  * @property {string} [priority] - Filter issues by their priority level.
117
131
  * @property {number} [limit] - Maximum number of issues to return.
118
132
  * @property {number} [offset] - Pagination offset.
133
+ * @property {number} [assigneeId] - Filter by assigned agent.
119
134
  */
120
135
 
121
136
  /**
@@ -123,7 +138,7 @@ export function getIssue(id) {
123
138
  * @param {ListIssuesOptions} options - Filtering and pagination options.
124
139
  * @returns {Issue[]}
125
140
  */
126
- export async function listIssues({ status, priority, limit, offset } = {}) {
141
+ export async function listIssues({ status, priority, limit, offset, assigneeId } = {}) {
127
142
  const db = getDB();
128
143
  const filters = [];
129
144
 
@@ -136,6 +151,10 @@ export async function listIssues({ status, priority, limit, offset } = {}) {
136
151
  filters.push(sql`${issuesTable.priority} COLLATE NOCASE = ${priority}`);
137
152
  }
138
153
 
154
+ if (assigneeId !== undefined) {
155
+ filters.push(eq(issuesTable.assigneeId, assigneeId));
156
+ }
157
+
139
158
  // set defaults if limit or offset is NULL
140
159
  const limitVal = limit ?? 50;
141
160
  const offsetVal = offset ?? 0;
@@ -180,6 +199,7 @@ export function searchIssues(query) {
180
199
  * @property {number} [tokenLimit] - The updated token limit.
181
200
  * @property {string} [status] - The updated status.
182
201
  * @property {string} [priority] - The updated priority.
202
+ * @property {number} [assigneeId] - The updated assignee ID.
183
203
  */
184
204
 
185
205
  /**
@@ -190,13 +210,14 @@ export function searchIssues(query) {
190
210
  * @param {UpdateIssueFields} fields - The fields to update.
191
211
  * @returns {Issue}
192
212
  */
193
- export function updateIssue(id, oldIssue, { title, description, tokenLimit, status, priority } = {}) {
213
+ export function updateIssue(id, oldIssue, { title, description, tokenLimit, status, priority, assigneeId } = {}) {
194
214
  const db = getDB();
195
215
  const updates = {};
196
216
 
197
217
  if (title !== undefined) updates.title = title;
198
218
  if (description !== undefined) updates.description = description;
199
219
  if (tokenLimit !== undefined) updates.tokenLimit = tokenLimit;
220
+ if (assigneeId !== undefined) updates.assigneeId = assigneeId;
200
221
 
201
222
  // Normalize status argument
202
223
  if (status !== undefined) {
@@ -350,12 +371,12 @@ export function getRecentActivity({ limit = 20 } = {}) {
350
371
  return db.select().from(activityTable).orderBy(sql`${activityTable.logId} DESC`).limit(limit).all();
351
372
  }
352
373
  // =============================================================================
353
- // Tracker operations (CLI: init / next / status / loop)
374
+ // Tracker operations (CLI: init / next / status / claim)
354
375
  // To be edited later if needed.
355
376
  // =============================================================================
356
377
 
357
378
  /**
358
- * True when both `issues` and `activity` tables exist (matches initDB schema).
379
+ * True when both `issues`, `activity`, and `agents` tables exist.
359
380
  * @returns {boolean}
360
381
  */
361
382
  export function isTrackerReady() {
@@ -365,9 +386,9 @@ export function isTrackerReady() {
365
386
  const row = db.get(sql`
366
387
  SELECT COUNT(*) AS table_count
367
388
  FROM sqlite_master
368
- WHERE type = 'table' AND name IN ('issues', 'activity')
389
+ WHERE type = 'table' AND name IN ('issues', 'activity', 'agents')
369
390
  `);
370
- return (row?.table_count ?? 0) === 2;
391
+ return (row?.table_count ?? 0) === 3;
371
392
  } catch (error) {
372
393
  return false;
373
394
  }
@@ -425,12 +446,11 @@ export function selectNextIssue() {
425
446
  }
426
447
 
427
448
  /**
428
- * Mark an issue in-progress, increment attempts, and log activity (atomic).
429
- * Uses existing `findById` / `logActivity` from above; does not modify lines 1–187.
449
+ * Mark an issue in-progress, assign it to the current actor, increment attempts, and log activity.
430
450
  * @param {number} issueId
431
451
  * @returns {object}
432
452
  */
433
- export function workOnIssue(issueId) {
453
+ export function claimIssue(issueId) {
434
454
  const db = getDB();
435
455
  const issue = findById(db, issueId);
436
456
 
@@ -444,6 +464,7 @@ export function workOnIssue(issueId) {
444
464
  tx.update(issuesTable)
445
465
  .set({
446
466
  status: Status.IN_PROGRESS,
467
+ assigneeId: currentActorId,
447
468
  attemptNum: sql`${issuesTable.attemptNum} + 1`
448
469
  })
449
470
  .where(eq(issuesTable.id, issueId))
@@ -453,14 +474,14 @@ export function workOnIssue(issueId) {
453
474
  tx,
454
475
  issueId,
455
476
  Action.STATE_CHANGE,
456
- `Status changed from ${issue.status} to ${Status.IN_PROGRESS}`,
477
+ `Status changed from ${issue.status} to ${Status.IN_PROGRESS}`
457
478
  );
458
479
 
459
480
  logActivity(
460
481
  tx,
461
482
  issueId,
462
483
  Action.EDIT,
463
- `Agent attempt #${issue.attemptNum + 1} on issue #${issueId}`,
484
+ `Agent attempt #${issue.attemptNum + 1} on issue #${issueId}`
464
485
  );
465
486
  });
466
487