baton-issue-tracker 1.13.2 → 1.13.4
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/README.md +80 -10
- package/package.json +1 -1
- package/source/cli.js +7 -1
- package/source/commands/create.js +28 -2
- package/source/commands/init.js +17 -2
- package/source/commands/list.js +24 -8
- package/source/commands/log.js +2 -1
- package/source/commands/unassign.js +95 -0
- package/source/commands/update.js +31 -1
- package/source/db/index.js +4 -2
- package/source/models/activityLog.js +2 -0
- package/source/models/schema.js +1 -1
- package/source/services/agentRestrictions.js +26 -0
- package/source/services/authService.js +22 -15
- package/source/services/issuesService.js +21 -2
- package/source/util.js +9 -7
package/README.md
CHANGED
|
@@ -1,14 +1,84 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./admin/branding/teamLogo.png" width="100">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
- [User Guide](docs/USER_GUIDE.md) — How to use Baton CLI.
|
|
5
|
-
- [Developer Guide](docs/CONTRIBUTING.md) — Setting up the development environment.
|
|
5
|
+
# Baton Issue Tracker (Baton)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
TBD
|
|
7
|
+
[](https://www.npmjs.com/package/baton-issue-tracker)
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
[Click Here](/admin/team.md)
|
|
9
|
+
**Baton** is a terminal-first issue tracker designed for the modern AI-augmented software engineering workflow. It provides a structured interface for human supervisors to manage, track, and approve tasks performed by autonomous AI agents.
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
In an environment where AI does much of the heavy lifting, Baton ensures accountability, tracks token budgets, and maintains a rigorous audit trail of every change made to your project's backlog.
|
|
12
|
+
|
|
13
|
+
**Baton** is developed and maintained by **Team Fantastic Four**.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quickstart
|
|
18
|
+
|
|
19
|
+
Get up and running with Baton in seconds.
|
|
20
|
+
|
|
21
|
+
### 1. Installation
|
|
22
|
+
Install Baton globally via NPM:
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g baton-issue-tracker
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 2. Initialization
|
|
28
|
+
Initialize a new tracker in your project root. This creates a local SQLite database at `.baton/baton.db`.
|
|
29
|
+
```bash
|
|
30
|
+
baton init
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 3. Check Identity
|
|
34
|
+
Verify your registration or add yourself to the project:
|
|
35
|
+
```bash
|
|
36
|
+
baton whoami
|
|
37
|
+
# If not registered:
|
|
38
|
+
baton register --name "your-name" --type human
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 4. Basic Workflow
|
|
42
|
+
* **Create a task:** `baton create --title "Fix login bug" --priority high`
|
|
43
|
+
* **Check status:** `baton status`
|
|
44
|
+
* **Claim next task (for agents):** `baton claim`
|
|
45
|
+
* **Submit work (for agents):** `baton submit <id>`
|
|
46
|
+
* **Approve work (for humans):** `baton approve <id>`
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
* **AI-Native API:** All commands support a `--json` flag for easy integration with AI agent loops.
|
|
53
|
+
* **Human-in-the-Loop:** A strict state machine ensuring agents can only resolve issues with human approval.
|
|
54
|
+
* **Full Activity History:** Track every edit, status change, and assignment with `baton log`.
|
|
55
|
+
* **Token Management:** Assign token limits to specific issues to control agent costs.
|
|
56
|
+
* **Zero-Config Storage:** Fully local SQLite storage—no external servers required.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Documentation Index
|
|
61
|
+
|
|
62
|
+
Baton is extensively documented to help both humans and agents get the most out of the system:
|
|
63
|
+
|
|
64
|
+
### **Guides**
|
|
65
|
+
* [**CLI Reference**](./docs/cli/README.md): Detailed documentation for every command and flag.
|
|
66
|
+
* [**Setup & Workflow**](./docs/cli/setup-commands.md): How to initialize and manage the agent work-loop.
|
|
67
|
+
* [**Issue Management**](./docs/cli/issue-commands.md): Deep dive into ticket CRUD and review processes.
|
|
68
|
+
* [**Contributing**](./CONTRIBUTING.md): Guidelines for developing and extending Baton.
|
|
69
|
+
|
|
70
|
+
### **Specifications**
|
|
71
|
+
* [**Product Requirements**](./docs/specs/project-requirements.md): The functional goals of the project.
|
|
72
|
+
* [**Data Model**](./docs/specs/issue-data-model.md): Detailed schema for issues and activity logs.
|
|
73
|
+
* [**ADRs (Architecture Decisions)**](./docs/adr/): Why we built Baton the way we did.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Team
|
|
78
|
+
Baton is developed and maintained by **Team Fantastic Four** (CSE 110, SP26).
|
|
79
|
+
Meet the team on our [**Team Page**](./admin/team.md).
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
package/package.json
CHANGED
package/source/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ import { run as runRegister } from './commands/register.js';
|
|
|
28
28
|
import { run as runAgents } from './commands/agents.js';
|
|
29
29
|
import { run as runWhoami } from './commands/whoami.js';
|
|
30
30
|
import { run as runSubmit } from './commands/submit.js';
|
|
31
|
+
import { run as runUnassign } from './commands/unassign.js';
|
|
31
32
|
import { run as runUnclaim } from './commands/unclaim.js';
|
|
32
33
|
import { run as runClaim } from './commands/claim.js';
|
|
33
34
|
|
|
@@ -56,6 +57,7 @@ Commands:
|
|
|
56
57
|
update Updates an issue's specified fields
|
|
57
58
|
delete Deletes an issue
|
|
58
59
|
log Show activity history for an issue
|
|
60
|
+
unassign Removes all assignees from issue
|
|
59
61
|
|
|
60
62
|
Options:
|
|
61
63
|
init --force Re-initialize an existing tracker database
|
|
@@ -94,6 +96,7 @@ Options:
|
|
|
94
96
|
update --json Output as JSON (for AI agents)
|
|
95
97
|
delete <id> [--yes]
|
|
96
98
|
log <id> [--json]
|
|
99
|
+
unassign <id> [--json] Output as JSON (for AI agents)
|
|
97
100
|
|
|
98
101
|
Examples:
|
|
99
102
|
baton init
|
|
@@ -120,6 +123,8 @@ Examples:
|
|
|
120
123
|
baton update 3 --title "Revised title"
|
|
121
124
|
baton update 7 --status closed --priority medium
|
|
122
125
|
baton log 5
|
|
126
|
+
baton unassign 12
|
|
127
|
+
baton unassign 11 --json
|
|
123
128
|
`;
|
|
124
129
|
|
|
125
130
|
/**
|
|
@@ -136,7 +141,7 @@ async function main() {
|
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
// Authenticate the user and context before executing any command.
|
|
139
|
-
authenticateContext(command);
|
|
144
|
+
authenticateContext(command, args);
|
|
140
145
|
|
|
141
146
|
const handlers = {
|
|
142
147
|
init: () => runInit(args),
|
|
@@ -156,6 +161,7 @@ async function main() {
|
|
|
156
161
|
delete: () => runDelete(args),
|
|
157
162
|
log: () => runLog(args),
|
|
158
163
|
submit: () => runSubmit(args),
|
|
164
|
+
unassign: () => runUnassign(args),
|
|
159
165
|
unclaim: () => runUnclaim(args),
|
|
160
166
|
};
|
|
161
167
|
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// --description <text> Issue description
|
|
15
15
|
// --priority <level> low | medium | high (default: low)
|
|
16
16
|
// --token-limit <n> Optional token budget for this issue
|
|
17
|
+
// --assignee <name> Agent assigned to this issue
|
|
17
18
|
// -h, --help Show this help
|
|
18
19
|
|
|
19
20
|
import { createIssue } from "../services/issuesService.js";
|
|
@@ -24,8 +25,9 @@ import { spawnSync } from "child_process";
|
|
|
24
25
|
import { writeFileSync, readFileSync, unlinkSync } from "fs";
|
|
25
26
|
import { tmpdir } from "os";
|
|
26
27
|
import { join } from "path";
|
|
28
|
+
import { listAgents, getAgentByName } from '../services/agentsService.js';
|
|
27
29
|
|
|
28
|
-
const ALLOWED_CREATE_FIELDS = ['title', 'priority', 'tokenLimit', 'description'];
|
|
30
|
+
const ALLOWED_CREATE_FIELDS = ['title', 'priority', 'tokenLimit', 'description', 'assigneeId'];
|
|
29
31
|
|
|
30
32
|
const VALID_FLAGS = new Set([
|
|
31
33
|
...ALLOWED_CREATE_FIELDS.map(key => issueSchema[key].flag),
|
|
@@ -157,6 +159,19 @@ async function runInteractiveMode() {
|
|
|
157
159
|
description = await openEditorForDescription();
|
|
158
160
|
}
|
|
159
161
|
|
|
162
|
+
// Assignee
|
|
163
|
+
const agents = listAgents();
|
|
164
|
+
const assigneeChoices = [
|
|
165
|
+
{ name: '(none)', value: null },
|
|
166
|
+
...agents.map(a => ({ name: `${a.name} (${a.type})`, value: a.id }))
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const assigneeId = await select({
|
|
170
|
+
message: 'Assign to:',
|
|
171
|
+
choices: assigneeChoices,
|
|
172
|
+
default: null,
|
|
173
|
+
});
|
|
174
|
+
|
|
160
175
|
// Preview & confirm
|
|
161
176
|
console.log("\n" + "-".repeat(48));
|
|
162
177
|
console.log(` Title : ${title}`);
|
|
@@ -181,7 +196,7 @@ async function runInteractiveMode() {
|
|
|
181
196
|
process.exit(0);
|
|
182
197
|
}
|
|
183
198
|
|
|
184
|
-
return { title, priority, tokenLimit, description };
|
|
199
|
+
return { title, priority, tokenLimit, description, assigneeId };
|
|
185
200
|
}
|
|
186
201
|
|
|
187
202
|
/**
|
|
@@ -217,6 +232,17 @@ export async function run(args) {
|
|
|
217
232
|
? await runInteractiveMode()
|
|
218
233
|
: parseArgs(args);
|
|
219
234
|
|
|
235
|
+
// Getting agent name from ID
|
|
236
|
+
if (options.assigneeId) {
|
|
237
|
+
const target = getAgentByName(options.assigneeId);
|
|
238
|
+
if (!target) {
|
|
239
|
+
const agents = listAgents();
|
|
240
|
+
const names = agents.map(agent => `${agent.name} (${agent.type})`).join(', ');
|
|
241
|
+
throw new Error(`Agent/User "${options.assigneeId}" is not registered.\nRegistered agents: ${names}`);
|
|
242
|
+
}
|
|
243
|
+
options.assigneeId = target.id;
|
|
244
|
+
}
|
|
245
|
+
|
|
220
246
|
const issue = await createIssue(options);
|
|
221
247
|
const envelope = { status: "success", issue: serializeIssue(issue) };
|
|
222
248
|
|
package/source/commands/init.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// baton init --specs ./path/to/my-specs.md
|
|
12
12
|
// baton init --specs C:\full\path\to\specs.md
|
|
13
13
|
|
|
14
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { initDB } from '../db/index.js';
|
|
17
17
|
import { Priority } from '../models/issue.js';
|
|
@@ -156,6 +156,21 @@ export async function run(args = []) {
|
|
|
156
156
|
clearAllIssues();
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
const templatePath = join('docs', 'BATON_AGENT_RULES.md');
|
|
160
|
+
|
|
161
|
+
let rulesContent = '';
|
|
162
|
+
if (existsSync(templatePath)) {
|
|
163
|
+
rulesContent = readFileSync(templatePath, 'utf8');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const outputPath = join(process.cwd(), 'BATON_AGENT_RULES.md');
|
|
167
|
+
if (existsSync(outputPath) && !flags.force) {
|
|
168
|
+
console.error('Error: BATON_AGENT_RULES.md already exists. Use --force to overwrite.');
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
writeFileSync(outputPath, rulesContent, 'utf8');
|
|
173
|
+
|
|
159
174
|
const resolvedSpecsPath = resolvePath(flags.specs, DEFAULT_SPECS_PATH);
|
|
160
175
|
const createdIssues = generateIssuesFromSpecs(flags.specs);
|
|
161
176
|
const envelope = {
|
|
@@ -180,4 +195,4 @@ export async function run(args = []) {
|
|
|
180
195
|
});
|
|
181
196
|
|
|
182
197
|
return 0;
|
|
183
|
-
}
|
|
198
|
+
}
|
package/source/commands/list.js
CHANGED
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
// --priority <p> Filter by priority: low | medium | high
|
|
9
9
|
// --limit <n> Max results (default: 50)
|
|
10
10
|
// --offset <n> Skip first n results (default: 0)
|
|
11
|
+
// --assignee <name> Filter by assignee name
|
|
11
12
|
// --json Output as JSON (for AI agents)
|
|
12
13
|
// -h, --help Show this help
|
|
13
14
|
|
|
14
15
|
import { listIssues } from '../services/issuesService.js';
|
|
16
|
+
import { issueSchema } from '../models/schema.js';
|
|
17
|
+
import { listAgents, getAgentByName } from '../services/agentsService.js';
|
|
18
|
+
|
|
15
19
|
import {
|
|
16
|
-
getFlagValue,
|
|
17
|
-
getNumericFlag,
|
|
18
20
|
hasFlag,
|
|
19
21
|
parseArgs,
|
|
20
22
|
printIssueTable,
|
|
@@ -23,6 +25,13 @@ import {
|
|
|
23
25
|
serializeIssue,
|
|
24
26
|
} from '../util.js';
|
|
25
27
|
|
|
28
|
+
const ALLOWED_LIST_FIELDS = ['status', 'priority', 'limit', 'offset', 'assigneeId'];
|
|
29
|
+
|
|
30
|
+
const VALID_FLAGS = new Set([
|
|
31
|
+
...ALLOWED_LIST_FIELDS.map(key => issueSchema[key].flag),
|
|
32
|
+
'--json',
|
|
33
|
+
]);
|
|
34
|
+
|
|
26
35
|
/**
|
|
27
36
|
* Lists issues matching the filters and pagination settings
|
|
28
37
|
* @param {string[]} args - The command line arguments
|
|
@@ -31,19 +40,26 @@ import {
|
|
|
31
40
|
|
|
32
41
|
export async function run(args) {
|
|
33
42
|
const isJson = hasFlag(args, '--json');
|
|
34
|
-
const validFlags = ['--status', '--priority', '--limit', '--offset', '--json'];
|
|
35
|
-
// Check if user misspelled a flag
|
|
36
43
|
for (const arg of args) {
|
|
37
|
-
if (arg.startsWith('--')) {
|
|
38
|
-
|
|
39
|
-
throw new Error(`Unknown flag provided: ${arg}. \nFlags: --status <s>, --priority <p>, --limit <n>, --offset <n>, --json`);
|
|
40
|
-
}
|
|
44
|
+
if (arg.startsWith('--') && !VALID_FLAGS.has(arg)) {
|
|
45
|
+
throw new Error(`Unknown flag provided: ${arg}.\nFlags: ${[...VALID_FLAGS].join(', ')}`);
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
try {
|
|
45
50
|
const options = parseArgs(args);
|
|
46
51
|
|
|
52
|
+
// Getting agent name from ID
|
|
53
|
+
if (options.assigneeId) {
|
|
54
|
+
const target = getAgentByName(options.assigneeId);
|
|
55
|
+
if (!target) {
|
|
56
|
+
const agents = listAgents();
|
|
57
|
+
const names = agents.map(agent => `${agent.name} (${agent.type})`).join(', ');
|
|
58
|
+
throw new Error(`Agent/User "${options.assigneeId}" is not registered.\nRegistered agents: ${names}`);
|
|
59
|
+
}
|
|
60
|
+
options.assigneeId = target.id;
|
|
61
|
+
}
|
|
62
|
+
|
|
47
63
|
const result = await listIssues(options);
|
|
48
64
|
const issues = result.map(serializeIssue);
|
|
49
65
|
const envelope = { status: 'success', count: issues.length, issues };
|
package/source/commands/log.js
CHANGED
|
@@ -22,8 +22,9 @@ function serializeLogEntry(entry) {
|
|
|
22
22
|
return {
|
|
23
23
|
log_id: entry.logId,
|
|
24
24
|
issue_id: entry.issueId,
|
|
25
|
+
actor_id: entry.actorId,
|
|
25
26
|
action: entry.action,
|
|
26
|
-
details: entry.details
|
|
27
|
+
details: entry.details,
|
|
27
28
|
created_at: entry.createdAt,
|
|
28
29
|
};
|
|
29
30
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// unassign.js
|
|
2
|
+
// AI was consulted to generate the majority of the contents of this file.
|
|
3
|
+
// However, a human has reviewed and editted the generated code.
|
|
4
|
+
// Removes the current assignee from an issue.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// baton unassign <id> [--json]
|
|
8
|
+
//
|
|
9
|
+
// Examples:
|
|
10
|
+
// baton unassign 12
|
|
11
|
+
// baton unassign 12 --json
|
|
12
|
+
|
|
13
|
+
import { getIssue, unassignIssue } from "../services/issuesService.js";
|
|
14
|
+
import { getCurrentActor } from "../services/context.js";
|
|
15
|
+
import { AgentType } from "../models/agents.js";
|
|
16
|
+
import { hasFlag, renderOutput, renderError, serializeIssue } from "../util.js";
|
|
17
|
+
|
|
18
|
+
const VALID_FLAGS = new Set(["--json"]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Removes the current assignee from an issue.
|
|
22
|
+
* @param {string[]} args - The command-line arguments
|
|
23
|
+
* @returns {Promise<number>} The exit code: 0 is success, 1 is error
|
|
24
|
+
*/
|
|
25
|
+
export async function run(args) {
|
|
26
|
+
const isJson = hasFlag(args, "--json");
|
|
27
|
+
|
|
28
|
+
// Validate flags
|
|
29
|
+
const providedFlags = args.filter((a) => a.startsWith("--"));
|
|
30
|
+
for (const flag of providedFlags) {
|
|
31
|
+
if (!VALID_FLAGS.has(flag)) {
|
|
32
|
+
throw new Error(`Unknown flag: ${flag}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const idArg = args.find((a) => !a.startsWith("-"));
|
|
37
|
+
const issueId = Number(idArg);
|
|
38
|
+
|
|
39
|
+
if (!Number.isInteger(issueId) || issueId <= 0) {
|
|
40
|
+
renderError(
|
|
41
|
+
isJson,
|
|
42
|
+
`Invalid ID "${idArg}". ID must be a positive integer.`,
|
|
43
|
+
"INVALID_ID",
|
|
44
|
+
);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const actor = getCurrentActor();
|
|
49
|
+
|
|
50
|
+
if (!actor || actor.type !== AgentType.HUMAN) {
|
|
51
|
+
renderError(isJson, "Only human users can unassign issues.", "FORBIDDEN");
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let issue;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
issue = getIssue(issueId);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.message.includes("not found")) {
|
|
61
|
+
renderError(isJson, error.message, "NOT_FOUND");
|
|
62
|
+
} else {
|
|
63
|
+
renderError(isJson, error.message);
|
|
64
|
+
}
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!issue.assigneeId) {
|
|
69
|
+
renderError(
|
|
70
|
+
isJson,
|
|
71
|
+
`Issue #${issueId} is not assigned.`,
|
|
72
|
+
"INVALID_STATE"
|
|
73
|
+
);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Use unassignIssue from issuesService.js
|
|
79
|
+
const updatedIssue = unassignIssue(issueId);
|
|
80
|
+
|
|
81
|
+
const envelope = {
|
|
82
|
+
status: "success",
|
|
83
|
+
issue: serializeIssue(updatedIssue),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
renderOutput(isJson, envelope, () => {
|
|
87
|
+
console.log(`Success: Issue #${issueId} is now unassigned.`);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return 0;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
renderError(isJson, error.message);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -25,8 +25,9 @@ import { spawnSync } from "child_process";
|
|
|
25
25
|
import { writeFileSync, readFileSync, unlinkSync } from "fs";
|
|
26
26
|
import { tmpdir } from "os";
|
|
27
27
|
import { join } from "path";
|
|
28
|
+
import { listAgents, getAgentByName } from '../services/agentsService.js';
|
|
28
29
|
|
|
29
|
-
const ALLOWED_UPDATE_FIELDS = ['title', 'status', 'priority', 'tokenLimit', 'description'];
|
|
30
|
+
const ALLOWED_UPDATE_FIELDS = ['title', 'status', 'priority', 'tokenLimit', 'description', 'assigneeId'];
|
|
30
31
|
|
|
31
32
|
const VALID_FLAGS = new Set([
|
|
32
33
|
...ALLOWED_UPDATE_FIELDS.map(key => issueSchema[key].flag),
|
|
@@ -161,6 +162,24 @@ async function runInteractiveMode(issue) {
|
|
|
161
162
|
if (edited !== null) results.description = edited;
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
// Assignee
|
|
166
|
+
const wantsAssignee = await confirm({
|
|
167
|
+
message: "Edit assignee?",
|
|
168
|
+
default: false,
|
|
169
|
+
});
|
|
170
|
+
if (wantsAssignee) {
|
|
171
|
+
const agents = listAgents();
|
|
172
|
+
const assigneeChoices = [
|
|
173
|
+
{ name: '(none)', value: null },
|
|
174
|
+
...agents.map(agent => ({ name: `${agent.name} (${agent.type})`, value: agent.id }))
|
|
175
|
+
];
|
|
176
|
+
results.assigneeId = await select({
|
|
177
|
+
message: 'Assign to:',
|
|
178
|
+
choices: assigneeChoices,
|
|
179
|
+
default: issue.assigneeId ?? null
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
164
183
|
// Diff: loop over results and collect only what changed.
|
|
165
184
|
// Adding a new prompt above is all that is needed -- nothing to update here.
|
|
166
185
|
const pending = Object.fromEntries(
|
|
@@ -236,6 +255,17 @@ export async function run(args) {
|
|
|
236
255
|
? await runInteractiveMode(oldIssue)
|
|
237
256
|
: parseArgs(cmdArgs.slice(1));
|
|
238
257
|
|
|
258
|
+
// Getting agent name from ID
|
|
259
|
+
if (options.assigneeId) {
|
|
260
|
+
const target = getAgentByName(options.assigneeId);
|
|
261
|
+
if (!target) {
|
|
262
|
+
const agents = listAgents();
|
|
263
|
+
const names = agents.map(agent => `${agent.name} (${agent.type})`).join(', ');
|
|
264
|
+
throw new Error(`Agent/User "${options.assigneeId}" is not registered.\nRegistered agents: ${names}`);
|
|
265
|
+
}
|
|
266
|
+
options.assigneeId = target.id;
|
|
267
|
+
}
|
|
268
|
+
|
|
239
269
|
const newIssue = await updateIssue(id, oldIssue, options);
|
|
240
270
|
const envelope = { status: "success", issue: serializeIssue(newIssue) };
|
|
241
271
|
|
package/source/db/index.js
CHANGED
|
@@ -18,7 +18,8 @@ if (!fs.existsSync(dataDir)) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// Initialize better-sqlite3 connection
|
|
21
|
-
|
|
21
|
+
// Using let here so we can reassign it in tests with an in-memory database instance.
|
|
22
|
+
let sqliteConnection = new Database(dbPath);
|
|
22
23
|
|
|
23
24
|
// Initialize Drizzle ORM, passing in the schema for relational queries
|
|
24
25
|
// Using let here so we can reassign it in tests with an in-memory database instance.
|
|
@@ -71,6 +72,7 @@ export function getSQLiteDB() {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
// function for the test suite mocking
|
|
74
|
-
export function setTestDB(testDbInstance) {
|
|
75
|
+
export function setTestDB(testDbInstance, testSqliteConnection) {
|
|
75
76
|
db = testDbInstance;
|
|
77
|
+
if (testSqliteConnection) sqliteConnection = testSqliteConnection;
|
|
76
78
|
}
|
|
@@ -14,12 +14,14 @@ export class ActivityLog {
|
|
|
14
14
|
issueId,
|
|
15
15
|
actorId = null,
|
|
16
16
|
action,
|
|
17
|
+
details = null,
|
|
17
18
|
createdAt = new Date().toISOString(),
|
|
18
19
|
} = {}) {
|
|
19
20
|
this.logId = logId;
|
|
20
21
|
this.issueId = issueId;
|
|
21
22
|
this.actorId = actorId;
|
|
22
23
|
this.action = action;
|
|
24
|
+
this.details = details,
|
|
23
25
|
this.createdAt = createdAt;
|
|
24
26
|
}
|
|
25
27
|
}
|
package/source/models/schema.js
CHANGED
|
@@ -53,7 +53,7 @@ export const issueSchema = {
|
|
|
53
53
|
tokenLimit: { flag: '--token-limit', type: 'number' },
|
|
54
54
|
priority: { flag: '--priority', type: 'enum', values: Object.values(Priority) },
|
|
55
55
|
description:{ flag: '--description', type: 'string' },
|
|
56
|
-
assigneeId: { flag: '--assignee
|
|
56
|
+
assigneeId: { flag: '--assignee', type: 'string'},
|
|
57
57
|
// Pagination options
|
|
58
58
|
limit: { flag: '--limit', type: 'number' },
|
|
59
59
|
offset: { flag: '--offset', type: 'number' }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// agentRestrictions.js
|
|
2
|
+
|
|
3
|
+
const AGENT_RESTRICTED_COMMANDS = new Set(['init', 'approve', 'reject', 'delete', 'priority']);
|
|
4
|
+
const AGENT_RESTRICTED_FLAGS = new Set(['--status', '--token-limit', '--assignee']);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validates whether an actor has permission to execute a given command and arguments.
|
|
8
|
+
* @param {string} command - The primary CLI command name.
|
|
9
|
+
* @param {string[]} [args=[]] - The command line arguments.
|
|
10
|
+
* @throws {Error} If the agent entered a restricted command or flag
|
|
11
|
+
*/
|
|
12
|
+
export function authorizeAction(command, args = []) {
|
|
13
|
+
if (AGENT_RESTRICTED_COMMANDS.has(command)) {
|
|
14
|
+
throw new Error(`Command "${command}" is restricted to human users only.`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (command === 'update') {
|
|
18
|
+
const usedRestricted = args.filter(arg => AGENT_RESTRICTED_FLAGS.has(arg));
|
|
19
|
+
if (usedRestricted.length > 0) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Agents cannot update parameters: ${usedRestricted.join(', ')}. ` +
|
|
22
|
+
`Use 'baton claim', 'baton submit', or 'baton unclaim' for status changes.`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
2
|
import { getAgentByName } from './agentsService.js';
|
|
3
3
|
import { setCurrentActor, getCurrentActor } from './context.js';
|
|
4
|
+
import { isTrackerReady } from './issuesService.js';
|
|
5
|
+
import { authorizeAction } from './agentRestrictions.js';
|
|
4
6
|
|
|
5
7
|
export { getCurrentActor };
|
|
6
8
|
|
|
@@ -10,30 +12,35 @@ export { getCurrentActor };
|
|
|
10
12
|
* If the agent/user is registered, it sets them as the active actor for this session.
|
|
11
13
|
* If not, it terminates the process with a helpful error message.
|
|
12
14
|
* @param {string} command - The command being executed
|
|
15
|
+
* @param {string[]} [args=[]] - The command line arguments.
|
|
13
16
|
*/
|
|
14
|
-
export function authenticateContext(command) {
|
|
15
|
-
const exemptCommands = ['
|
|
17
|
+
export function authenticateContext(command, args = []) {
|
|
18
|
+
const exemptCommands = ['register', 'help'];
|
|
16
19
|
if (exemptCommands.includes(command)) {
|
|
17
20
|
return;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
if (!isTrackerReady()) {
|
|
24
|
+
if (command === 'init') {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
console.error("Error: Tracker is not initialized. Please run 'baton init' first.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
const activeName = process.env.BATON_AGENT || os.userInfo().username;
|
|
32
|
+
const agent = getAgentByName(activeName);
|
|
33
|
+
|
|
34
|
+
if (!agent) {
|
|
26
35
|
console.error(`Error: Agent/User "${activeName}" is not registered in this Baton tracker.`);
|
|
27
36
|
console.error(`Please run 'baton register --name "${activeName}" --type "human"' (or "agent") first.`);
|
|
28
37
|
process.exit(1);
|
|
29
|
-
|
|
38
|
+
}
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
console.error("Error: Tracker is not initialized. Please run 'baton init' first.");
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
throw error;
|
|
40
|
+
// Check if agent entered a restricted command
|
|
41
|
+
if (agent.type === 'agent') {
|
|
42
|
+
authorizeAction(command, args);
|
|
38
43
|
}
|
|
44
|
+
|
|
45
|
+
setCurrentActor(agent);
|
|
39
46
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// AI was consulted to guide implementation of part of the file.
|
|
1
2
|
import { getDB } from '../db/index.js';
|
|
2
3
|
import { eq, and, or, like, sql } from "drizzle-orm";
|
|
3
4
|
import {issuesTable, activityTable} from "../models/schema.js";
|
|
@@ -257,6 +258,24 @@ export function updateIssue(id, oldIssue, { title, description, tokenLimit, stat
|
|
|
257
258
|
return getIssue(id);
|
|
258
259
|
}
|
|
259
260
|
|
|
261
|
+
/**
|
|
262
|
+
*
|
|
263
|
+
* Sets assigneeId to null
|
|
264
|
+
* Logs an edit event
|
|
265
|
+
* @param {number} issueId - ID of the issue to be editted
|
|
266
|
+
* @returns {Issue}
|
|
267
|
+
*/
|
|
268
|
+
export function unassignIssue(issueId){
|
|
269
|
+
const db = getDB();
|
|
270
|
+
|
|
271
|
+
// Check that issue exists
|
|
272
|
+
findById(db, issueId);
|
|
273
|
+
|
|
274
|
+
db.update(issuesTable).set({ status: Status.OPEN, assigneeId: null }).where(eq(issuesTable.id, issueId)).run();
|
|
275
|
+
logActivity(db, issueId, Action.EDIT, `Success: Issue #${issueId} is now unassigned.`);
|
|
276
|
+
return getIssue(issueId);
|
|
277
|
+
}
|
|
278
|
+
|
|
260
279
|
/**
|
|
261
280
|
* Change the status of an issue from in-review to closed
|
|
262
281
|
* Logs a closed event.
|
|
@@ -384,7 +403,7 @@ export function deleteIssue(id) {
|
|
|
384
403
|
*/
|
|
385
404
|
export function getActivityLog(issueId) {
|
|
386
405
|
const db = getDB();
|
|
387
|
-
return db.select().from(activityTable).where(eq(activityTable.issueId, issueId)).all();
|
|
406
|
+
return db.select().from(activityTable).where(eq(activityTable.issueId, issueId)).all().map(rowToLog);
|
|
388
407
|
}
|
|
389
408
|
|
|
390
409
|
/**
|
|
@@ -394,7 +413,7 @@ export function getActivityLog(issueId) {
|
|
|
394
413
|
*/
|
|
395
414
|
export function getRecentActivity({ limit = 20 } = {}) {
|
|
396
415
|
const db = getDB();
|
|
397
|
-
return db.select().from(activityTable).orderBy(sql`${activityTable.logId} DESC`).limit(limit).all();
|
|
416
|
+
return db.select().from(activityTable).orderBy(sql`${activityTable.logId} DESC`).limit(limit).all().map(rowToLog);
|
|
398
417
|
}
|
|
399
418
|
// =============================================================================
|
|
400
419
|
// Tracker operations (CLI: init / next / status / claim)
|
package/source/util.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { isAbsolute, resolve } from 'node:path';
|
|
7
7
|
import { issueSchema } from '../source/models/schema.js';
|
|
8
|
+
import { getAgentById } from '../source/services/agentsService.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Formats a timestamp as `HH:MM:SS YYYY-MM-DD`.
|
|
@@ -269,7 +270,7 @@ export const WIDTHS = {
|
|
|
269
270
|
title: 20,
|
|
270
271
|
status: 15,
|
|
271
272
|
priority: 10,
|
|
272
|
-
|
|
273
|
+
assignee: 10,
|
|
273
274
|
description: 50
|
|
274
275
|
};
|
|
275
276
|
|
|
@@ -283,7 +284,7 @@ export function printTableHeader() {
|
|
|
283
284
|
"TITLE".padEnd(WIDTHS.title) + " │ " +
|
|
284
285
|
"STATUS".padEnd(WIDTHS.status) + " │ " +
|
|
285
286
|
"PRIORITY".padEnd(WIDTHS.priority) + " │ " +
|
|
286
|
-
|
|
287
|
+
"ASSIGNEE".padEnd(WIDTHS.assignee) + " │ " +
|
|
287
288
|
"DESCRIPTION".padEnd(WIDTHS.description)
|
|
288
289
|
);
|
|
289
290
|
console.log(
|
|
@@ -291,7 +292,7 @@ export function printTableHeader() {
|
|
|
291
292
|
"─".repeat(WIDTHS.title) + "─┼─" +
|
|
292
293
|
"─".repeat(WIDTHS.status) + "─┼─" +
|
|
293
294
|
"─".repeat(WIDTHS.priority) + "─┼─" +
|
|
294
|
-
|
|
295
|
+
"─".repeat(WIDTHS.assignee) + "─┼─" +
|
|
295
296
|
"─".repeat(WIDTHS.description)
|
|
296
297
|
);
|
|
297
298
|
}
|
|
@@ -324,9 +325,10 @@ export function printIssueTable(issue) {
|
|
|
324
325
|
const priorityVal = truncate(issue.priority, WIDTHS.priority).padEnd(WIDTHS.priority);
|
|
325
326
|
const descVal = truncate(issue.description, WIDTHS.description).padEnd(WIDTHS.description);
|
|
326
327
|
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
328
|
+
// Getting Agent name from id
|
|
329
|
+
const assigneeId = issue.assigneeId ?? issue.assignee_id ?? null;
|
|
330
|
+
const assignee = assigneeId ? getAgentById(assigneeId) : null;
|
|
331
|
+
const assigneeVal = truncate(assignee?.name ?? null, WIDTHS.assignee).padEnd(WIDTHS.assignee);
|
|
330
332
|
|
|
331
|
-
console.log(`${idVal} │ ${titleVal} │ ${statusVal} │ ${priorityVal} │ ${descVal}`);
|
|
333
|
+
console.log(`${idVal} │ ${titleVal} │ ${statusVal} │ ${priorityVal} │ ${assigneeVal} │ ${descVal}`);
|
|
332
334
|
}
|