baton-issue-tracker 1.11.1 → 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 +1 -1
- package/source/cli.js +5 -0
- package/source/commands/claim.js +1 -1
- package/source/commands/unclaim.js +104 -0
- package/source/services/agentsService.js +16 -0
- package/source/services/authService.js +4 -20
- package/source/services/context.js +25 -0
- package/source/services/issuesService.js +35 -23
- package/source/util.js +24 -13
package/package.json
CHANGED
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];
|
package/source/commands/claim.js
CHANGED
|
@@ -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/
|
|
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 {
|
|
3
|
+
import { setCurrentActor, getCurrentActor } from './context.js';
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
*/
|
package/source/util.js
CHANGED
|
@@ -199,25 +199,36 @@ function toJsonEnum(value) {
|
|
|
199
199
|
return value?.toLowerCase().replace(/-/g, '_') ?? null;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Converts a camelCase string to snake_case.
|
|
204
|
+
* @param {string} str
|
|
205
|
+
* @returns {string}
|
|
206
|
+
*/
|
|
207
|
+
function camelToSnake(str) {
|
|
208
|
+
return str.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
202
211
|
/**
|
|
203
212
|
* Normalizes a DB row or Issue instance into a stable JSON-friendly shape.
|
|
204
|
-
*
|
|
213
|
+
* Dynamically built from issueSchema so the output always matches the schema.
|
|
205
214
|
* @param {object} issue
|
|
206
215
|
* @returns {object}
|
|
207
216
|
*/
|
|
208
217
|
export function serializeIssue(issue) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
218
|
+
const skip = new Set(['limit', 'offset']);
|
|
219
|
+
const result = {};
|
|
220
|
+
|
|
221
|
+
for (const [key, config] of Object.entries(issueSchema)) {
|
|
222
|
+
if (skip.has(key)) continue;
|
|
223
|
+
const snakeKey = camelToSnake(key);
|
|
224
|
+
const value = issue[key] ?? issue[snakeKey] ?? null;
|
|
225
|
+
result[snakeKey] = config.type === 'enum' ? toJsonEnum(value) : value;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
result.created_at = issue.createdAt ?? issue.created_at ?? null;
|
|
229
|
+
result.last_updated = issue.lastUpdated ?? issue.last_updated ?? null;
|
|
230
|
+
|
|
231
|
+
return result;
|
|
221
232
|
}
|
|
222
233
|
|
|
223
234
|
/**
|