baton-issue-tracker 1.11.2 → 1.13.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 +10 -0
- package/source/commands/claim.js +1 -1
- package/source/commands/unclaim.js +104 -0
- package/source/commands/whoami.js +54 -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/package.json
CHANGED
package/source/cli.js
CHANGED
|
@@ -26,7 +26,9 @@ import { run as runPriority } from './commands/priority.js';
|
|
|
26
26
|
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
|
+
import { run as runWhoami } from './commands/whoami.js';
|
|
29
30
|
import { run as runSubmit } from './commands/submit.js';
|
|
31
|
+
import { run as runUnclaim } from './commands/unclaim.js';
|
|
30
32
|
import { run as runClaim } from './commands/claim.js';
|
|
31
33
|
|
|
32
34
|
import { authenticateContext } from './services/authService.js';
|
|
@@ -40,6 +42,7 @@ Commands:
|
|
|
40
42
|
init Initialize storage and seed issues from product specs
|
|
41
43
|
register Register a new AI agent or human user
|
|
42
44
|
agents List all registered agents and humans
|
|
45
|
+
whoami Show the currently authenticated agent or user
|
|
43
46
|
status Show issue counts and overall progress
|
|
44
47
|
view View all issue fields for a given issue ID
|
|
45
48
|
search Search issues by title and description (case insensitive)
|
|
@@ -47,6 +50,7 @@ Commands:
|
|
|
47
50
|
create Creates an issue with specified fields
|
|
48
51
|
approve Move an issue from in-review to closed
|
|
49
52
|
submit Submit finished work for human review
|
|
53
|
+
unclaim Release a claimed issue back to Open
|
|
50
54
|
claim Claim an issue as the authenticated agent
|
|
51
55
|
priority Set an issue's priority level
|
|
52
56
|
update Updates an issue's specified fields
|
|
@@ -62,6 +66,7 @@ Options:
|
|
|
62
66
|
register --name <name> Name of the agent or user
|
|
63
67
|
register --type <type> agent | human (default: agent)
|
|
64
68
|
agents [--json]
|
|
69
|
+
whoami [--json]
|
|
65
70
|
status --json Output as JSON (for AI agents)
|
|
66
71
|
view <id> [--json]
|
|
67
72
|
search <query> [--json]
|
|
@@ -77,6 +82,7 @@ Options:
|
|
|
77
82
|
create --json Output as JSON (for AI agents)
|
|
78
83
|
approve <id> [--json]
|
|
79
84
|
submit <id> [--json]
|
|
85
|
+
unclaim <id> [--json]
|
|
80
86
|
claim <id> [--json]
|
|
81
87
|
reject <id> --reason <text> Reject an issue with a given reason
|
|
82
88
|
priority <id> <level> [--json] low | medium | high
|
|
@@ -96,6 +102,7 @@ Examples:
|
|
|
96
102
|
baton init --force
|
|
97
103
|
baton register --name claude-dev --type agent
|
|
98
104
|
baton agents
|
|
105
|
+
baton whoami
|
|
99
106
|
baton status
|
|
100
107
|
baton view 29
|
|
101
108
|
baton search system
|
|
@@ -106,6 +113,7 @@ Examples:
|
|
|
106
113
|
baton create --title "Refactor auth" --description "Clean up JWT logic" --token-limit 4000
|
|
107
114
|
baton approve 5
|
|
108
115
|
baton submit 14
|
|
116
|
+
baton unclaim 14
|
|
109
117
|
baton claim 14
|
|
110
118
|
baton priority 5 high
|
|
111
119
|
baton priority 3 low
|
|
@@ -134,6 +142,7 @@ async function main() {
|
|
|
134
142
|
init: () => runInit(args),
|
|
135
143
|
register: () => runRegister(args),
|
|
136
144
|
agents: () => runAgents(args),
|
|
145
|
+
whoami: () => runWhoami(args),
|
|
137
146
|
status: () => runStatus(args),
|
|
138
147
|
view: () => runView(args),
|
|
139
148
|
search: () => runSearch(args),
|
|
@@ -147,6 +156,7 @@ async function main() {
|
|
|
147
156
|
delete: () => runDelete(args),
|
|
148
157
|
log: () => runLog(args),
|
|
149
158
|
submit: () => runSubmit(args),
|
|
159
|
+
unclaim: () => runUnclaim(args),
|
|
150
160
|
};
|
|
151
161
|
|
|
152
162
|
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
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// whoami.js
|
|
2
|
+
// Shows the currently authenticated agent or user.
|
|
3
|
+
// Usage: baton whoami [--json]
|
|
4
|
+
//
|
|
5
|
+
// Options:
|
|
6
|
+
// --json Output as JSON (for AI agents)
|
|
7
|
+
// -h, --help Show this help
|
|
8
|
+
//
|
|
9
|
+
// Examples:
|
|
10
|
+
// baton whoami
|
|
11
|
+
|
|
12
|
+
import { getCurrentActor } from '../services/authService.js';
|
|
13
|
+
import { hasFlag, renderOutput, renderError, wantsHelp } from '../util.js';
|
|
14
|
+
|
|
15
|
+
const VALID_FLAGS = ['--json', '-h', '--help'];
|
|
16
|
+
|
|
17
|
+
function serializeActor(actor) {
|
|
18
|
+
return {
|
|
19
|
+
id: actor.id,
|
|
20
|
+
name: actor.name,
|
|
21
|
+
type: actor.type,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function run(args = []) {
|
|
26
|
+
const isJson = hasFlag(args, '--json');
|
|
27
|
+
|
|
28
|
+
if (wantsHelp(args)) {
|
|
29
|
+
console.log(
|
|
30
|
+
`Usage: baton whoami [--json]\n\nOptions:\n --json Output as JSON (for AI agents)\n -h, --help Show this help\n\nExamples:\n baton whoami`,
|
|
31
|
+
);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const arg of args) {
|
|
36
|
+
if (arg.startsWith('-') && !VALID_FLAGS.includes(arg)) {
|
|
37
|
+
throw new Error(`Unknown flag provided: ${arg}.\nFlags: --json, -h, --help`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const actor = getCurrentActor();
|
|
42
|
+
if (!actor) {
|
|
43
|
+
renderError(isJson, 'No authenticated actor found. Please sign in with a registered agent.', 'UNAUTHORIZED');
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const envelope = { status: 'success', actor: serializeActor(actor) };
|
|
48
|
+
|
|
49
|
+
renderOutput(isJson, envelope, () => {
|
|
50
|
+
console.log(`Active Agent: "${actor.name}" (ID: ${actor.id}, Type: ${actor.type})`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
@@ -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
|
*/
|