baton-issue-tracker 1.6.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.
- package/drizzle/0002_breezy_wiccan.sql +10 -0
- package/drizzle/meta/0002_snapshot.json +229 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/source/cli.js +17 -6
- package/source/commands/next.js +3 -3
- package/source/commands/register.js +68 -0
- package/source/models/activityLog.js +2 -0
- package/source/models/agents.js +37 -0
- package/source/models/issue.js +2 -4
- package/source/models/schema.js +10 -3
- package/source/services/agentsService.js +42 -0
- package/source/services/authService.js +44 -0
- package/source/services/issuesService.js +33 -12
|
@@ -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
|
+
}
|
package/package.json
CHANGED
package/source/cli.js
CHANGED
|
@@ -26,6 +26,9 @@ import { run as runCreate } from './commands/create.js'
|
|
|
26
26
|
import { run as runUpdate } from './commands/update.js';
|
|
27
27
|
import { run as runPriority } from './commands/priority.js';
|
|
28
28
|
import { run as runLog } from './commands/log.js';
|
|
29
|
+
import { run as runRegister } from './commands/register.js';
|
|
30
|
+
|
|
31
|
+
import { authenticateContext } from './services/authService.js';
|
|
29
32
|
|
|
30
33
|
const HELP = `baton — AI agent issue tracker CLI
|
|
31
34
|
|
|
@@ -34,6 +37,7 @@ Usage:
|
|
|
34
37
|
|
|
35
38
|
Commands:
|
|
36
39
|
init Initialize storage and seed issues from product specs
|
|
40
|
+
register Register a new AI agent or human user
|
|
37
41
|
next Work on the highest-priority open issue
|
|
38
42
|
loop Run the agent autonomously for multiple steps
|
|
39
43
|
status Show issue counts and overall progress
|
|
@@ -52,6 +56,8 @@ Options:
|
|
|
52
56
|
init --json Output as JSON (for AI agents)
|
|
53
57
|
init <path> Same as --specs <path> (positional)
|
|
54
58
|
Default specs: docs/specs/project-requirements.md
|
|
59
|
+
register --name <name> Name of the agent or user
|
|
60
|
+
register --type <type> agent | human (default: agent)
|
|
55
61
|
loop --steps <n> Number of autonomous steps (alias: -n)
|
|
56
62
|
loop -n <n>
|
|
57
63
|
loop --json Output as JSON (for AI agents)
|
|
@@ -84,6 +90,7 @@ Examples:
|
|
|
84
90
|
baton init --specs ./my-specs.md
|
|
85
91
|
baton init ./my-specs.md
|
|
86
92
|
baton init --force
|
|
93
|
+
baton register --name claude-dev --type agent
|
|
87
94
|
baton next
|
|
88
95
|
baton loop --steps 5
|
|
89
96
|
baton status
|
|
@@ -109,8 +116,18 @@ Examples:
|
|
|
109
116
|
async function main() {
|
|
110
117
|
const [, , command, ...args] = process.argv;
|
|
111
118
|
|
|
119
|
+
if (!command || command === 'help' || wantsHelp(args) || command === '--help') {
|
|
120
|
+
console.log(HELP);
|
|
121
|
+
process.exit(command ? 0 : 1);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Authenticate the user and context before executing any command.
|
|
126
|
+
authenticateContext(command);
|
|
127
|
+
|
|
112
128
|
const handlers = {
|
|
113
129
|
init: () => runInit(args),
|
|
130
|
+
register: () => runRegister(args),
|
|
114
131
|
next: () => runNext(args),
|
|
115
132
|
loop: () => runLoop(args),
|
|
116
133
|
status: () => runStatus(args),
|
|
@@ -124,12 +141,6 @@ async function main() {
|
|
|
124
141
|
log: () => runLog(args),
|
|
125
142
|
};
|
|
126
143
|
|
|
127
|
-
if (!command || command === 'help' || command === '--help') {
|
|
128
|
-
console.log(HELP);
|
|
129
|
-
process.exit(command ? 0 : 1);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
144
|
const handler = handlers[command];
|
|
134
145
|
if (!handler) {
|
|
135
146
|
if (wantsHelp(args)) {
|
package/source/commands/next.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import {
|
|
7
7
|
isTrackerReady,
|
|
8
8
|
selectNextIssue,
|
|
9
|
-
|
|
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
|
|
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 =
|
|
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
|
+
}
|
package/source/models/issue.js
CHANGED
|
@@ -20,7 +20,7 @@ export class Issue {
|
|
|
20
20
|
tokenLimit = null,
|
|
21
21
|
description = null,
|
|
22
22
|
lastUpdated = new Date().toISOString(),
|
|
23
|
-
|
|
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.
|
|
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
|
-
|
package/source/models/schema.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
56
|
+
assigneeId: { flag: '--assignee-id', type: 'number'},
|
|
50
57
|
// Pagination options
|
|
51
58
|
limit: { flag: '--limit', type: 'number' },
|
|
52
|
-
offset:
|
|
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 /
|
|
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 `
|
|
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) ===
|
|
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
|
|
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
|
|
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
|
|