baton-issue-tracker 1.6.0 → 1.8.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.
@@ -0,0 +1,10 @@
1
+ CREATE TABLE `agents` (
2
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
+ `name` text NOT NULL,
4
+ `type` text NOT NULL
5
+ );
6
+ --> statement-breakpoint
7
+ CREATE UNIQUE INDEX `agents_name_unique` ON `agents` (`name`);--> statement-breakpoint
8
+ ALTER TABLE `activity` ADD `actor_id` integer REFERENCES agents(id);--> statement-breakpoint
9
+ ALTER TABLE `issues` ADD `assignee_id` integer REFERENCES agents(id);--> statement-breakpoint
10
+ ALTER TABLE `issues` DROP COLUMN `assignees`;
@@ -0,0 +1,229 @@
1
+ {
2
+ "version": "6",
3
+ "dialect": "sqlite",
4
+ "id": "d89cefa6-9c90-4882-8fab-f6322fd1f45e",
5
+ "prevId": "af5467aa-0757-4c91-8afb-c6f0202dd90c",
6
+ "tables": {
7
+ "activity": {
8
+ "name": "activity",
9
+ "columns": {
10
+ "log_id": {
11
+ "name": "log_id",
12
+ "type": "integer",
13
+ "primaryKey": true,
14
+ "notNull": true,
15
+ "autoincrement": true
16
+ },
17
+ "issue_id": {
18
+ "name": "issue_id",
19
+ "type": "integer",
20
+ "primaryKey": false,
21
+ "notNull": false,
22
+ "autoincrement": false
23
+ },
24
+ "actor_id": {
25
+ "name": "actor_id",
26
+ "type": "integer",
27
+ "primaryKey": false,
28
+ "notNull": false,
29
+ "autoincrement": false
30
+ },
31
+ "created_at": {
32
+ "name": "created_at",
33
+ "type": "text",
34
+ "primaryKey": false,
35
+ "notNull": true,
36
+ "autoincrement": false,
37
+ "default": "CURRENT_TIMESTAMP"
38
+ },
39
+ "action": {
40
+ "name": "action",
41
+ "type": "text",
42
+ "primaryKey": false,
43
+ "notNull": true,
44
+ "autoincrement": false
45
+ },
46
+ "details": {
47
+ "name": "details",
48
+ "type": "text",
49
+ "primaryKey": false,
50
+ "notNull": false,
51
+ "autoincrement": false
52
+ }
53
+ },
54
+ "indexes": {},
55
+ "foreignKeys": {
56
+ "activity_actor_id_agents_id_fk": {
57
+ "name": "activity_actor_id_agents_id_fk",
58
+ "tableFrom": "activity",
59
+ "tableTo": "agents",
60
+ "columnsFrom": [
61
+ "actor_id"
62
+ ],
63
+ "columnsTo": [
64
+ "id"
65
+ ],
66
+ "onDelete": "no action",
67
+ "onUpdate": "no action"
68
+ }
69
+ },
70
+ "compositePrimaryKeys": {},
71
+ "uniqueConstraints": {},
72
+ "checkConstraints": {
73
+ "activity_action_check": {
74
+ "name": "activity_action_check",
75
+ "value": "\"activity\".\"action\" IN ('state_change', 'priority_change', 'edit', 'read', 'creation', 'deletion', 'rejection')"
76
+ }
77
+ }
78
+ },
79
+ "agents": {
80
+ "name": "agents",
81
+ "columns": {
82
+ "id": {
83
+ "name": "id",
84
+ "type": "integer",
85
+ "primaryKey": true,
86
+ "notNull": true,
87
+ "autoincrement": true
88
+ },
89
+ "name": {
90
+ "name": "name",
91
+ "type": "text",
92
+ "primaryKey": false,
93
+ "notNull": true,
94
+ "autoincrement": false
95
+ },
96
+ "type": {
97
+ "name": "type",
98
+ "type": "text",
99
+ "primaryKey": false,
100
+ "notNull": true,
101
+ "autoincrement": false
102
+ }
103
+ },
104
+ "indexes": {
105
+ "agents_name_unique": {
106
+ "name": "agents_name_unique",
107
+ "columns": [
108
+ "name"
109
+ ],
110
+ "isUnique": true
111
+ }
112
+ },
113
+ "foreignKeys": {},
114
+ "compositePrimaryKeys": {},
115
+ "uniqueConstraints": {},
116
+ "checkConstraints": {}
117
+ },
118
+ "issues": {
119
+ "name": "issues",
120
+ "columns": {
121
+ "id": {
122
+ "name": "id",
123
+ "type": "integer",
124
+ "primaryKey": true,
125
+ "notNull": true,
126
+ "autoincrement": true
127
+ },
128
+ "created_at": {
129
+ "name": "created_at",
130
+ "type": "text",
131
+ "primaryKey": false,
132
+ "notNull": true,
133
+ "autoincrement": false,
134
+ "default": "CURRENT_TIMESTAMP"
135
+ },
136
+ "last_updated": {
137
+ "name": "last_updated",
138
+ "type": "text",
139
+ "primaryKey": false,
140
+ "notNull": true,
141
+ "autoincrement": false,
142
+ "default": "CURRENT_TIMESTAMP"
143
+ },
144
+ "attempt_num": {
145
+ "name": "attempt_num",
146
+ "type": "integer",
147
+ "primaryKey": false,
148
+ "notNull": true,
149
+ "autoincrement": false,
150
+ "default": 0
151
+ },
152
+ "title": {
153
+ "name": "title",
154
+ "type": "text",
155
+ "primaryKey": false,
156
+ "notNull": true,
157
+ "autoincrement": false,
158
+ "default": "'PENDING'"
159
+ },
160
+ "status": {
161
+ "name": "status",
162
+ "type": "text",
163
+ "primaryKey": false,
164
+ "notNull": true,
165
+ "autoincrement": false,
166
+ "default": "'Open'"
167
+ },
168
+ "priority": {
169
+ "name": "priority",
170
+ "type": "text",
171
+ "primaryKey": false,
172
+ "notNull": false,
173
+ "autoincrement": false,
174
+ "default": "'Low'"
175
+ },
176
+ "token_limit": {
177
+ "name": "token_limit",
178
+ "type": "integer",
179
+ "primaryKey": false,
180
+ "notNull": false,
181
+ "autoincrement": false
182
+ },
183
+ "description": {
184
+ "name": "description",
185
+ "type": "text",
186
+ "primaryKey": false,
187
+ "notNull": false,
188
+ "autoincrement": false
189
+ },
190
+ "assignee_id": {
191
+ "name": "assignee_id",
192
+ "type": "integer",
193
+ "primaryKey": false,
194
+ "notNull": false,
195
+ "autoincrement": false
196
+ }
197
+ },
198
+ "indexes": {},
199
+ "foreignKeys": {
200
+ "issues_assignee_id_agents_id_fk": {
201
+ "name": "issues_assignee_id_agents_id_fk",
202
+ "tableFrom": "issues",
203
+ "tableTo": "agents",
204
+ "columnsFrom": [
205
+ "assignee_id"
206
+ ],
207
+ "columnsTo": [
208
+ "id"
209
+ ],
210
+ "onDelete": "no action",
211
+ "onUpdate": "no action"
212
+ }
213
+ },
214
+ "compositePrimaryKeys": {},
215
+ "uniqueConstraints": {},
216
+ "checkConstraints": {}
217
+ }
218
+ },
219
+ "views": {},
220
+ "enums": {},
221
+ "_meta": {
222
+ "schemas": {},
223
+ "tables": {},
224
+ "columns": {}
225
+ },
226
+ "internal": {
227
+ "indexes": {}
228
+ }
229
+ }
@@ -15,6 +15,13 @@
15
15
  "when": 1780114722528,
16
16
  "tag": "0001_salty_luke_cage",
17
17
  "breakpoints": true
18
+ },
19
+ {
20
+ "idx": 2,
21
+ "version": "6",
22
+ "when": 1780293597234,
23
+ "tag": "0002_breezy_wiccan",
24
+ "breakpoints": true
18
25
  }
19
26
  ]
20
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baton-issue-tracker",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "A CLI issue tracker for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
package/source/cli.js CHANGED
@@ -24,8 +24,12 @@ import { run as runSearch } from './commands/search.js';
24
24
  import { run as runList } from './commands/list.js';
25
25
  import { run as runCreate } from './commands/create.js'
26
26
  import { run as runUpdate } from './commands/update.js';
27
+ import { run as runDelete } from './commands/delete.js';
27
28
  import { run as runPriority } from './commands/priority.js';
28
29
  import { run as runLog } from './commands/log.js';
30
+ import { run as runRegister } from './commands/register.js';
31
+
32
+ import { authenticateContext } from './services/authService.js';
29
33
 
30
34
  const HELP = `baton — AI agent issue tracker CLI
31
35
 
@@ -34,6 +38,7 @@ Usage:
34
38
 
35
39
  Commands:
36
40
  init Initialize storage and seed issues from product specs
41
+ register Register a new AI agent or human user
37
42
  next Work on the highest-priority open issue
38
43
  loop Run the agent autonomously for multiple steps
39
44
  status Show issue counts and overall progress
@@ -44,6 +49,7 @@ Commands:
44
49
  approve Move an issue from in-review to closed
45
50
  priority Set an issue's priority level
46
51
  update Updates an issue's specified fields
52
+ delete Deletes an issue
47
53
  log Show activity history for an issue
48
54
 
49
55
  Options:
@@ -52,6 +58,8 @@ Options:
52
58
  init --json Output as JSON (for AI agents)
53
59
  init <path> Same as --specs <path> (positional)
54
60
  Default specs: docs/specs/project-requirements.md
61
+ register --name <name> Name of the agent or user
62
+ register --type <type> agent | human (default: agent)
55
63
  loop --steps <n> Number of autonomous steps (alias: -n)
56
64
  loop -n <n>
57
65
  loop --json Output as JSON (for AI agents)
@@ -77,6 +85,7 @@ Options:
77
85
  update --status <s> open | in-progress | closed
78
86
  update --priority <level> low | medium | high
79
87
  update --json Output as JSON (for AI agents)
88
+ delete <id> [--yes]
80
89
  log <id> [--json]
81
90
 
82
91
  Examples:
@@ -84,6 +93,7 @@ Examples:
84
93
  baton init --specs ./my-specs.md
85
94
  baton init ./my-specs.md
86
95
  baton init --force
96
+ baton register --name claude-dev --type agent
87
97
  baton next
88
98
  baton loop --steps 5
89
99
  baton status
@@ -109,8 +119,18 @@ Examples:
109
119
  async function main() {
110
120
  const [, , command, ...args] = process.argv;
111
121
 
122
+ if (!command || command === 'help' || wantsHelp(args) || command === '--help') {
123
+ console.log(HELP);
124
+ process.exit(command ? 0 : 1);
125
+ return;
126
+ }
127
+
128
+ // Authenticate the user and context before executing any command.
129
+ authenticateContext(command);
130
+
112
131
  const handlers = {
113
132
  init: () => runInit(args),
133
+ register: () => runRegister(args),
114
134
  next: () => runNext(args),
115
135
  loop: () => runLoop(args),
116
136
  status: () => runStatus(args),
@@ -121,15 +141,10 @@ async function main() {
121
141
  priority: () => runPriority(args),
122
142
  create: () => runCreate(args),
123
143
  update: () => runUpdate(args),
144
+ delete: () => runDelete(args),
124
145
  log: () => runLog(args),
125
146
  };
126
147
 
127
- if (!command || command === 'help' || command === '--help') {
128
- console.log(HELP);
129
- process.exit(command ? 0 : 1);
130
- return;
131
- }
132
-
133
148
  const handler = handlers[command];
134
149
  if (!handler) {
135
150
  if (wantsHelp(args)) {
@@ -0,0 +1,99 @@
1
+ // delete.js
2
+ // supports deleting an issue.
3
+ //
4
+ // AI was used to modify this file to support JSON outputs.
5
+ //
6
+ // Usage:
7
+ // baton delete <id> [--yes] [--json]
8
+ //
9
+ // Options:
10
+ // --yes Skip confirmation prompt
11
+ // --json Output in JSON format
12
+ // -h, --help Show this help
13
+ //
14
+ // Examples:
15
+ // baton delete 4
16
+ // baton delete 4 --yes
17
+
18
+ import { deleteIssue, getIssue } from '../services/issuesService.js';
19
+ import { hasFlag, wantsHelp, renderOutput, renderError } from '../util.js';
20
+ import { confirm } from '@inquirer/prompts';
21
+
22
+ const USAGE = "Usage: baton delete <id> [options]\n\nOptions:\n --yes Skip confirmation prompt\n --json Output in JSON format\n -h, --help Show this help";
23
+
24
+ /**
25
+ * Deletes an issue for a specified ID.
26
+ *
27
+ * @param {string[]} args - The command line arguments
28
+ * @returns {Promise<number>} The exit code: 0 is success, 1 is error.
29
+ */
30
+ export async function run(args) {
31
+ const isJson = hasFlag(args, '--json');
32
+
33
+ // (0) Help check
34
+ if (wantsHelp(args)) {
35
+ console.log(USAGE);
36
+ return 0;
37
+ }
38
+
39
+ // (1) Parse arguments
40
+ const idArgs = args.filter(arg => !arg.startsWith('-'));
41
+ if (idArgs.length === 0) {
42
+ renderError(isJson, `No ID provided.\n${USAGE}`, 'MISSING_ID');
43
+ return 1;
44
+ }
45
+
46
+ const id = Number(idArgs[0]);
47
+ if (!Number.isInteger(id)) {
48
+ renderError(isJson, `Invalid ID "${idArgs[0]}". ID must be an integer.`, 'INVALID_ID');
49
+ return 1;
50
+ }
51
+
52
+ const isYes = hasFlag(args, "--yes");
53
+
54
+ try {
55
+ // (2) Check if issue exists before confirming
56
+ try {
57
+ await getIssue(id);
58
+ } catch (error) {
59
+ if (error.message.includes("not found")) {
60
+ renderError(isJson, error.message, 'NOT_FOUND');
61
+ } else {
62
+ renderError(isJson, error.message);
63
+ }
64
+ return 1;
65
+ }
66
+
67
+ // (3) Confirmation prompt
68
+ let confirmed = isYes;
69
+ if (!confirmed && !isJson) { // Only prompt if not JSON and not --yes
70
+ try {
71
+ confirmed = await confirm({ message: `Are you sure you want to delete issue #${id}?`, default: false });
72
+ } catch {
73
+ return 1;
74
+ }
75
+ } else if (!confirmed && isJson) {
76
+ // In JSON mode, do not prompt. If --yes is missing, we fail or assume no.
77
+ renderError(isJson, "Confirmation required. Use --yes to confirm deletion in JSON mode.", 'CONFIRMATION_REQUIRED');
78
+ return 1;
79
+ }
80
+
81
+ if (!confirmed) {
82
+ console.log("Deletion cancelled.");
83
+ return 0;
84
+ }
85
+
86
+ // (4) Execution
87
+ await deleteIssue(id);
88
+
89
+ const envelope = { status: 'success', id: id, message: `Issue #${id} deleted successfully.` };
90
+ renderOutput(isJson, envelope, () => {
91
+ console.log(`Issue #${id} deleted successfully.`);
92
+ });
93
+
94
+ return 0;
95
+ } catch (error) {
96
+ renderError(isJson, error.message);
97
+ return 1;
98
+ }
99
+ }
@@ -6,7 +6,7 @@
6
6
  import {
7
7
  isTrackerReady,
8
8
  selectNextIssue,
9
- workOnIssue,
9
+ claimIssue,
10
10
  } from '../services/issuesService.js';
11
11
  import { formatTimestamp, hasFlag, renderOutput, reportTrackerNotReady, serializeIssue } from '../util.js';
12
12
 
@@ -14,7 +14,7 @@ import { formatTimestamp, hasFlag, renderOutput, reportTrackerNotReady, serializ
14
14
  * Moves the AI agent to work on the next issue.
15
15
  * Checks if the tracker is ready and if there are any open issues.
16
16
  * Prompts user to initialize the tracker if it is not ready.
17
- * Stats are updated on the issue through workOnIssue function from init.js.
17
+ * Stats are updated on the issue through claimIssue function from init.js.
18
18
  * @returns {Promise<number>} The exit code: 0 is success, 1 is error.
19
19
  */
20
20
  export async function run(args = []) {
@@ -33,7 +33,7 @@ export async function run(args = []) {
33
33
  return 0;
34
34
  }
35
35
 
36
- const updated = workOnIssue(issue.id);
36
+ const updated = claimIssue(issue.id);
37
37
  const envelope = { status: 'success', issue: serializeIssue(updated) };
38
38
 
39
39
  renderOutput(isJson, envelope, () => {
@@ -0,0 +1,68 @@
1
+ // register.js
2
+ // AI was consulted for some portions of this file.
3
+ // register command which allows the user to register a new AI agent or human
4
+ // Usage: baton register --name <name> [--type <type>] [--json]
5
+ //
6
+ // Options:
7
+ // --name <n> Name of the agent or human (required)
8
+ // --type <t> Type: agent | human (default: agent)
9
+ // --json Output as JSON (for AI agents)
10
+ // -h, --help Show this help
11
+
12
+ import { registerAgent } from '../services/agentsService.js';
13
+ import {
14
+ getFlagValue,
15
+ hasFlag,
16
+ renderOutput,
17
+ } from '../util.js';
18
+
19
+ /**
20
+ * Registers a new agent or human user
21
+ * @param {string[]} args - The command line arguments
22
+ * @returns {Promise<number>} The exit code: 0 is success, 1 is error
23
+ */
24
+ export async function run(args) {
25
+ const isJson = hasFlag(args, '--json');
26
+ const validFlags = ['--name', '--type', '--json'];
27
+
28
+ // Check if user misspelled a flag
29
+ for (const arg of args) {
30
+ if (arg.startsWith('--')) {
31
+ if (!validFlags.includes(arg)) {
32
+ throw new Error(`Unknown flag provided: ${arg}. \nFlags: --name <n>, --type <t>, --json`);
33
+ }
34
+ }
35
+ }
36
+
37
+ try {
38
+ const name = getFlagValue(args, '--name');
39
+ const type = getFlagValue(args, '--type') || 'agent';
40
+
41
+ if (!name) {
42
+ throw new Error("Missing required flag: --name <name>");
43
+ }
44
+
45
+ if (type !== 'agent' && type !== 'human') {
46
+ throw new Error(`Invalid type "${type}". Must be 'agent' or 'human'.`);
47
+ }
48
+
49
+ const agent = registerAgent(name, type);
50
+ const envelope = { status: 'success', agent };
51
+
52
+ renderOutput(isJson, envelope, (data) => {
53
+ console.log(`\nSuccessfully registered ${data.agent.type}: "${data.agent.name}" (ID: ${data.agent.id})\n`);
54
+ });
55
+
56
+ return 0;
57
+ } catch (error) {
58
+ if (error.message.includes('UNIQUE constraint failed')) {
59
+ console.error(`Error: An agent or user with the name "${getFlagValue(args, '--name')}" is already registered.`);
60
+ } else if (error.message.includes('Missing required') || error.message.includes('Invalid type')) {
61
+ console.error(`Usage Error: ${error.message}`);
62
+ } else {
63
+ console.error("Error: Failed to register agent.");
64
+ console.error(error.message);
65
+ }
66
+ return 1;
67
+ }
68
+ }
@@ -12,11 +12,13 @@ export class ActivityLog {
12
12
  constructor({
13
13
  logId = null,
14
14
  issueId,
15
+ actorId = null,
15
16
  action,
16
17
  createdAt = new Date().toISOString(),
17
18
  } = {}) {
18
19
  this.logId = logId;
19
20
  this.issueId = issueId;
21
+ this.actorId = actorId;
20
22
  this.action = action;
21
23
  this.createdAt = createdAt;
22
24
  }
@@ -0,0 +1,37 @@
1
+ export const AgentType = Object.freeze({
2
+ AGENT: "agent",
3
+ HUMAN: "human"
4
+ });
5
+
6
+ export class Agent {
7
+ constructor({
8
+ // User fields
9
+ name = "Unnamed",
10
+ type = AgentType.AGENT,
11
+ // Auto-generated fields
12
+ id = 0,
13
+ createdAt = new Date().toISOString(),
14
+ } = {}) {
15
+ this.name = name;
16
+ this.type = type;
17
+ this.id = id;
18
+ }
19
+
20
+ /**
21
+ * Validates the agent fields based on project business rules.
22
+ * @returns {{isValid: boolean, errors: string[]}} boolean and an array of errors
23
+ */
24
+ validate() {
25
+ const errors = [];
26
+
27
+ if (!this.name || typeof this.name !== "string" || this.name.trim() === "") {
28
+ errors.push("Name cannot be empty");
29
+ }
30
+
31
+ if (!Object.values(AgentType).includes(this.type)) {
32
+ errors.push(`Invalid type "${this.type}". Must be one of: ${Object.values(AgentType).join(", ")}`);
33
+ }
34
+
35
+ return { isValid: errors.length === 0, errors: errors };
36
+ }
37
+ }
@@ -20,7 +20,7 @@ export class Issue {
20
20
  tokenLimit = null,
21
21
  description = null,
22
22
  lastUpdated = new Date().toISOString(),
23
- assignees = null,
23
+ assigneeId = null,
24
24
  // Auto-generated fields
25
25
  id = 0,
26
26
  createdAt = new Date().toISOString(),
@@ -32,7 +32,7 @@ export class Issue {
32
32
  this.tokenLimit = tokenLimit;
33
33
  this.description = description;
34
34
  this.lastUpdated = lastUpdated;
35
- this.assignees = assignees;
35
+ this.assigneeId = assigneeId;
36
36
  this.id = id;
37
37
  this.createdAt = createdAt;
38
38
  this.attemptNum = attemptNum;
@@ -68,5 +68,3 @@ export class Issue {
68
68
  return {isValid: errors.length == 0, errors: errors}
69
69
  }
70
70
  }
71
-
72
-
@@ -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