aitasks 1.2.2 → 1.3.1
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 +44 -5
- package/dist/index.js +203 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,6 +72,7 @@ export AITASKS_JSON=true
|
|
|
72
72
|
| Command | Description |
|
|
73
73
|
|---|---|
|
|
74
74
|
| `aitasks init` | Initialize a task database in the current project |
|
|
75
|
+
| `aitasks init --with-review` | Initialize with review enforcement (agents cannot mark done without a passing review) |
|
|
75
76
|
| `aitasks onboard` | Print or inject agent protocol instructions into CLAUDE.md / AGENTS.md |
|
|
76
77
|
|
|
77
78
|
### Task Discovery
|
|
@@ -79,7 +80,7 @@ export AITASKS_JSON=true
|
|
|
79
80
|
| Command | Description |
|
|
80
81
|
|---|---|
|
|
81
82
|
| `aitasks list` | List all tasks, sorted by priority |
|
|
82
|
-
| `aitasks list --status ready` | Filter by status (`ready`, `in_progress`, `
|
|
83
|
+
| `aitasks list --status ready` | Filter by status (`ready`, `in_progress`, `blocked`, `review`, `done`) |
|
|
83
84
|
| `aitasks next` | Show the highest-priority unblocked ready task |
|
|
84
85
|
| `aitasks next --claim --agent <id>` | Auto-claim and start the best task |
|
|
85
86
|
| `aitasks show <id>` | Full detail on a specific task (includes time tracking) |
|
|
@@ -96,9 +97,9 @@ export AITASKS_JSON=true
|
|
|
96
97
|
| `aitasks start <id...> --agent <id>` | Begin active work on task(s) |
|
|
97
98
|
| `aitasks note <id> <text>` | Add an implementation note |
|
|
98
99
|
| `aitasks check <id> <n> --evidence <text>` | Verify acceptance criterion n |
|
|
99
|
-
| `aitasks done <id...> --agent <id>` | Mark task(s) complete (all criteria must be verified) |
|
|
100
|
-
| `aitasks review <id...> --agent <id>` | Submit for
|
|
101
|
-
| `aitasks reject <id> --reason <text>` | Reject
|
|
100
|
+
| `aitasks done <id...> --agent <id>` | Mark task(s) complete (all criteria must be verified; must be in `review` status if enforcement is on) |
|
|
101
|
+
| `aitasks review <id...> --agent <id>` | Submit task(s) for review (moves to `review` status) |
|
|
102
|
+
| `aitasks reject <id> --reason <text>` | Reject a task in review, send it back to `in_progress` with feedback |
|
|
102
103
|
| `aitasks unclaim <id> --agent <id>` | Release a task back to the pool |
|
|
103
104
|
| `aitasks undo <id>` | Undo the last action on a task |
|
|
104
105
|
| `aitasks delete <id...>` | Delete task(s) - does not require claiming first |
|
|
@@ -151,12 +152,50 @@ When you run `aitasks init`, it automatically injects a full agent protocol into
|
|
|
151
152
|
You can also inject/view it manually:
|
|
152
153
|
|
|
153
154
|
```sh
|
|
154
|
-
aitasks onboard # print to stdout
|
|
155
|
+
aitasks onboard # print to stdout (reflects current review_required setting)
|
|
155
156
|
aitasks onboard --append # append to detected agent file
|
|
156
157
|
aitasks onboard --file MY.md # append to a specific file
|
|
157
158
|
aitasks onboard --json # output as JSON string
|
|
158
159
|
```
|
|
159
160
|
|
|
161
|
+
The injected instructions automatically adapt to the project's review enforcement setting — if `--with-review` is enabled, agents receive the full review workflow (with `aitasks review`, sub-agent approval, and `aitasks reject`) instead of the standard completion flow.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Review Enforcement
|
|
166
|
+
|
|
167
|
+
Enable a mandatory review gate so agents cannot mark tasks done without an explicit approval step:
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
# Enable at init time
|
|
171
|
+
aitasks init --with-review
|
|
172
|
+
|
|
173
|
+
# Enable on an existing project (also replaces the agent instructions file with review-aware version)
|
|
174
|
+
aitasks init --with-review # safe to re-run; updates DB setting and rewrites agent file
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**How it works:**
|
|
178
|
+
|
|
179
|
+
1. Agent completes work and checks all acceptance criteria
|
|
180
|
+
2. Agent submits for review — task moves to `review` status:
|
|
181
|
+
```sh
|
|
182
|
+
aitasks review TASK-001 --agent $AITASKS_AGENT_ID
|
|
183
|
+
```
|
|
184
|
+
3. A separate review sub-agent inspects the implementation and either:
|
|
185
|
+
- **Approves** — moves task to done:
|
|
186
|
+
```sh
|
|
187
|
+
aitasks done TASK-001 --agent review-agent
|
|
188
|
+
```
|
|
189
|
+
- **Rejects** — sends it back to `in_progress` with feedback:
|
|
190
|
+
```sh
|
|
191
|
+
aitasks reject TASK-001 --reason "Missing error handling for 404 case" --agent review-agent
|
|
192
|
+
```
|
|
193
|
+
4. If rejected, the agent addresses feedback, re-verifies criteria, and repeats from step 2.
|
|
194
|
+
|
|
195
|
+
**Enforcement:** When review is required, `aitasks done` and `aitasks update --status done` both block if the task is not already in `review` status. The gate cannot be bypassed.
|
|
196
|
+
|
|
197
|
+
**Board:** Tasks in `review` status appear in the **IN PROGRESS** section with a `◈` magenta indicator so they're visually distinct from actively worked tasks.
|
|
198
|
+
|
|
160
199
|
---
|
|
161
200
|
|
|
162
201
|
## Data Storage
|
package/dist/index.js
CHANGED
|
@@ -1890,7 +1890,7 @@ var require_commander = __commonJS((exports) => {
|
|
|
1890
1890
|
var require_package = __commonJS((exports, module) => {
|
|
1891
1891
|
module.exports = {
|
|
1892
1892
|
name: "aitasks",
|
|
1893
|
-
version: "1.
|
|
1893
|
+
version: "1.3.1",
|
|
1894
1894
|
description: "CLI task management tool built for AI agents",
|
|
1895
1895
|
type: "module",
|
|
1896
1896
|
bin: {
|
|
@@ -29071,7 +29071,16 @@ import { join as join3 } from "path";
|
|
|
29071
29071
|
import { existsSync as existsSync3 } from "fs";
|
|
29072
29072
|
|
|
29073
29073
|
// src/db/schema.ts
|
|
29074
|
-
var SCHEMA_VERSION =
|
|
29074
|
+
var SCHEMA_VERSION = 2;
|
|
29075
|
+
function runMigrations(db) {
|
|
29076
|
+
const row = db.query(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
29077
|
+
const current = row ? parseInt(row.value, 10) : 1;
|
|
29078
|
+
if (current < 2) {
|
|
29079
|
+
db.exec(`UPDATE tasks SET status = 'review' WHERE status = 'needs_review'`);
|
|
29080
|
+
db.exec(`INSERT OR IGNORE INTO meta (key, value) VALUES ('review_required', 'false')`);
|
|
29081
|
+
db.exec(`UPDATE meta SET value = '2' WHERE key = 'schema_version'`);
|
|
29082
|
+
}
|
|
29083
|
+
}
|
|
29075
29084
|
function initializeSchema(db) {
|
|
29076
29085
|
db.exec(`
|
|
29077
29086
|
CREATE TABLE IF NOT EXISTS meta (
|
|
@@ -29126,6 +29135,7 @@ function initializeSchema(db) {
|
|
|
29126
29135
|
insert.run("last_task_number", "0");
|
|
29127
29136
|
insert.run("schema_version", String(SCHEMA_VERSION));
|
|
29128
29137
|
insert.run("initialized_at", String(Date.now()));
|
|
29138
|
+
insert.run("review_required", "false");
|
|
29129
29139
|
}
|
|
29130
29140
|
|
|
29131
29141
|
// src/db/backup.ts
|
|
@@ -29240,10 +29250,20 @@ function getDb() {
|
|
|
29240
29250
|
const db = new Database(dbPath);
|
|
29241
29251
|
applyPragmas(db);
|
|
29242
29252
|
checkIntegrity(db, taskieDir);
|
|
29253
|
+
runMigrations(db);
|
|
29243
29254
|
registerCleanup();
|
|
29244
29255
|
_db = db;
|
|
29245
29256
|
return db;
|
|
29246
29257
|
}
|
|
29258
|
+
function getReviewRequired() {
|
|
29259
|
+
const db = getDb();
|
|
29260
|
+
const row = db.query(`SELECT value FROM meta WHERE key = 'review_required'`).get();
|
|
29261
|
+
return row?.value === "true";
|
|
29262
|
+
}
|
|
29263
|
+
function setReviewRequired(enabled) {
|
|
29264
|
+
const db = getDb();
|
|
29265
|
+
db.run(`INSERT OR REPLACE INTO meta (key, value) VALUES ('review_required', ?)`, [enabled ? "true" : "false"]);
|
|
29266
|
+
}
|
|
29247
29267
|
function createFreshDb(taskieDir) {
|
|
29248
29268
|
const dbPath = join3(taskieDir, "db.sqlite");
|
|
29249
29269
|
const db = new Database(dbPath);
|
|
@@ -29265,7 +29285,8 @@ import { join as join4 } from "path";
|
|
|
29265
29285
|
// src/utils/instructions.ts
|
|
29266
29286
|
var INSTRUCTIONS_START_MARKER = "<!-- aitasks:instructions -->";
|
|
29267
29287
|
var INSTRUCTIONS_END_MARKER = "<!-- aitasks:instructions:end -->";
|
|
29268
|
-
function getAgentInstructions(version) {
|
|
29288
|
+
function getAgentInstructions(version, opts = {}) {
|
|
29289
|
+
const reviewRequired = opts.reviewRequired ?? false;
|
|
29269
29290
|
return `${INSTRUCTIONS_START_MARKER}
|
|
29270
29291
|
|
|
29271
29292
|
## AITasks \u2014 Agent Task Protocol (v${version})
|
|
@@ -29372,7 +29393,7 @@ aitasks deps TASK-001 # Shows what this task is blocked by and what it blocks
|
|
|
29372
29393
|
|
|
29373
29394
|
---
|
|
29374
29395
|
|
|
29375
|
-
### Completing a Task
|
|
29396
|
+
### Completing a Task${reviewRequired ? " \u26A0\uFE0F REVIEW REQUIRED" : ""}
|
|
29376
29397
|
|
|
29377
29398
|
You MUST verify every acceptance criterion before marking done.
|
|
29378
29399
|
|
|
@@ -29388,15 +29409,30 @@ You MUST verify every acceptance criterion before marking done.
|
|
|
29388
29409
|
aitasks check TASK-001 2 --evidence "integration test suite passes: 12/12 green"
|
|
29389
29410
|
\`\`\`
|
|
29390
29411
|
|
|
29391
|
-
3.
|
|
29412
|
+
${reviewRequired ? `3. Submit for review \u2014 you CANNOT mark done directly:
|
|
29413
|
+
\`\`\`bash
|
|
29414
|
+
aitasks review TASK-001 --agent $AITASKS_AGENT_ID
|
|
29415
|
+
\`\`\`
|
|
29416
|
+
|
|
29417
|
+
4. Spawn a review sub-agent to inspect the implementation and either:
|
|
29418
|
+
- **Approve** (moves task to done):
|
|
29419
|
+
\`\`\`bash
|
|
29420
|
+
aitasks done TASK-001 --agent <review-agent-id>
|
|
29421
|
+
\`\`\`
|
|
29422
|
+
- **Reject** (sends task back to in_progress with feedback):
|
|
29423
|
+
\`\`\`bash
|
|
29424
|
+
aitasks reject TASK-001 --reason "<specific feedback>" --agent <review-agent-id>
|
|
29425
|
+
\`\`\`
|
|
29426
|
+
|
|
29427
|
+
5. If rejected: address the feedback, re-check criteria, and repeat from step 3.` : `3. Mark done (will FAIL if any criterion is unchecked):
|
|
29392
29428
|
\`\`\`bash
|
|
29393
29429
|
aitasks done TASK-001 --agent $AITASKS_AGENT_ID
|
|
29394
29430
|
\`\`\`
|
|
29395
29431
|
|
|
29396
|
-
If
|
|
29432
|
+
If review is needed:
|
|
29397
29433
|
\`\`\`bash
|
|
29398
29434
|
aitasks review TASK-001 --agent $AITASKS_AGENT_ID
|
|
29399
|
-
|
|
29435
|
+
\`\`\``}
|
|
29400
29436
|
|
|
29401
29437
|
---
|
|
29402
29438
|
|
|
@@ -29434,7 +29470,8 @@ aitasks unclaim TASK-001 --agent $AITASKS_AGENT_ID --reason "Blocked on missing
|
|
|
29434
29470
|
4. Add implementation notes continuously, not just at the end.
|
|
29435
29471
|
5. If a task needs splitting, create subtasks BEFORE marking parent done.
|
|
29436
29472
|
6. Your evidence strings must be concrete and verifiable \u2014 not vague affirmations.
|
|
29437
|
-
7. Always provide --desc and at least one --ac when creating a task. Both are required
|
|
29473
|
+
7. Always provide --desc and at least one --ac when creating a task. Both are required.${reviewRequired ? `
|
|
29474
|
+
8. NEVER move a task to done directly. Always submit for review first with \`aitasks review\`, then spawn a review sub-agent to approve or reject. Tasks cannot be marked done without a passing review.` : ""}
|
|
29438
29475
|
|
|
29439
29476
|
---
|
|
29440
29477
|
|
|
@@ -29481,13 +29518,17 @@ function findExistingAgentFile(projectRoot) {
|
|
|
29481
29518
|
}
|
|
29482
29519
|
return null;
|
|
29483
29520
|
}
|
|
29484
|
-
function injectOrCreateAgentFile(projectRoot, version) {
|
|
29485
|
-
const instructions = getAgentInstructions(version);
|
|
29521
|
+
function injectOrCreateAgentFile(projectRoot, version, reviewRequired = false, force = false) {
|
|
29522
|
+
const instructions = getAgentInstructions(version, { reviewRequired });
|
|
29486
29523
|
const existing = findExistingAgentFile(projectRoot);
|
|
29487
29524
|
if (existing) {
|
|
29488
29525
|
const content = readFileSync2(existing, "utf8");
|
|
29489
29526
|
if (instructionsAlreadyPresent(content)) {
|
|
29490
|
-
|
|
29527
|
+
if (!force)
|
|
29528
|
+
return { filePath: existing, action: "skipped" };
|
|
29529
|
+
const replaced = replaceInstructionsBlock(content, instructions);
|
|
29530
|
+
writeFileSync2(existing, replaced, "utf8");
|
|
29531
|
+
return { filePath: existing, action: "appended" };
|
|
29491
29532
|
}
|
|
29492
29533
|
const separator = content.endsWith(`
|
|
29493
29534
|
`) ? `
|
|
@@ -29503,8 +29544,25 @@ function injectOrCreateAgentFile(projectRoot, version) {
|
|
|
29503
29544
|
`, "utf8");
|
|
29504
29545
|
return { filePath: newPath, action: "created" };
|
|
29505
29546
|
}
|
|
29506
|
-
function
|
|
29507
|
-
const
|
|
29547
|
+
function replaceInstructionsBlock(content, newInstructions) {
|
|
29548
|
+
const start = content.indexOf(INSTRUCTIONS_START_MARKER);
|
|
29549
|
+
const end = content.indexOf(INSTRUCTIONS_END_MARKER);
|
|
29550
|
+
if (start === -1 || end === -1)
|
|
29551
|
+
return content;
|
|
29552
|
+
const before = content.slice(0, start).replace(/\n+$/, "");
|
|
29553
|
+
const after = content.slice(end + INSTRUCTIONS_END_MARKER.length).replace(/^\n+/, "");
|
|
29554
|
+
const sep = before.length > 0 ? `
|
|
29555
|
+
|
|
29556
|
+
` : "";
|
|
29557
|
+
const tail = after.length > 0 ? `
|
|
29558
|
+
|
|
29559
|
+
` + after : `
|
|
29560
|
+
`;
|
|
29561
|
+
return before + sep + newInstructions + `
|
|
29562
|
+
` + tail;
|
|
29563
|
+
}
|
|
29564
|
+
function appendToSpecificFile(filePath, version, reviewRequired = false) {
|
|
29565
|
+
const instructions = getAgentInstructions(version, { reviewRequired });
|
|
29508
29566
|
if (!existsSync4(filePath)) {
|
|
29509
29567
|
writeFileSync2(filePath, instructions + `
|
|
29510
29568
|
`, "utf8");
|
|
@@ -29525,27 +29583,39 @@ function appendToSpecificFile(filePath, version) {
|
|
|
29525
29583
|
}
|
|
29526
29584
|
|
|
29527
29585
|
// src/commands/init.ts
|
|
29528
|
-
var initCommand = new Command("init").description("Initialize AITasks in the current project").option("--skip-agent-file", "Skip injecting agent instructions into CLAUDE.md/AGENTS.md/GEMINI.md").action(async (opts) => {
|
|
29586
|
+
var initCommand = new Command("init").description("Initialize AITasks in the current project").option("--skip-agent-file", "Skip injecting agent instructions into CLAUDE.md/AGENTS.md/GEMINI.md").option("--with-review", "Enforce review gate: agents cannot mark tasks done without a passing review").action(async (opts) => {
|
|
29529
29587
|
const root = findProjectRoot();
|
|
29530
29588
|
const taskieDir = join5(root, ".aitasks");
|
|
29531
29589
|
if (existsSync5(join5(taskieDir, "db.sqlite"))) {
|
|
29532
29590
|
console.log(source_default.yellow(" AITasks is already initialized in this project."));
|
|
29533
29591
|
console.log(source_default.dim(` DB: ${join5(taskieDir, "db.sqlite")}`));
|
|
29592
|
+
if (opts.withReview) {
|
|
29593
|
+
setReviewRequired(true);
|
|
29594
|
+
console.log(source_default.green(" \u2713") + " Review enforcement enabled.");
|
|
29595
|
+
}
|
|
29534
29596
|
if (!opts.skipAgentFile) {
|
|
29535
|
-
const result = injectOrCreateAgentFile(root, getVersion());
|
|
29597
|
+
const result = injectOrCreateAgentFile(root, getVersion(), !!opts.withReview, !!opts.withReview);
|
|
29536
29598
|
printAgentFileResult(result);
|
|
29537
29599
|
}
|
|
29538
29600
|
return;
|
|
29539
29601
|
}
|
|
29540
29602
|
mkdirSync(taskieDir, { recursive: true });
|
|
29541
29603
|
createFreshDb(taskieDir);
|
|
29604
|
+
if (opts.withReview) {
|
|
29605
|
+
setReviewRequired(true);
|
|
29606
|
+
}
|
|
29542
29607
|
console.log("");
|
|
29543
29608
|
console.log(source_default.green(" \u2713") + source_default.bold(" AITasks initialized"));
|
|
29544
29609
|
console.log(source_default.dim(` Project root : ${root}`));
|
|
29545
29610
|
console.log(source_default.dim(` Database : ${join5(taskieDir, "db.sqlite")}`));
|
|
29611
|
+
if (opts.withReview) {
|
|
29612
|
+
console.log(source_default.magenta(" \u25C8") + source_default.bold(" Review enforcement enabled"));
|
|
29613
|
+
console.log(source_default.dim(" Agents must submit tasks for review before marking done."));
|
|
29614
|
+
console.log(source_default.dim(" Use: aitasks review <id> \u2192 review sub-agent \u2192 aitasks done <id>"));
|
|
29615
|
+
}
|
|
29546
29616
|
console.log("");
|
|
29547
29617
|
if (!opts.skipAgentFile) {
|
|
29548
|
-
const result = injectOrCreateAgentFile(root, getVersion());
|
|
29618
|
+
const result = injectOrCreateAgentFile(root, getVersion(), !!opts.withReview);
|
|
29549
29619
|
printAgentFileResult(result);
|
|
29550
29620
|
}
|
|
29551
29621
|
console.log(source_default.dim(" Run `aitasks create` to add your first task."));
|
|
@@ -30156,7 +30226,7 @@ var PRIORITY_ORDER = `CASE priority
|
|
|
30156
30226
|
WHEN 'low' THEN 3 END`;
|
|
30157
30227
|
var STATUS_ORDER = `CASE status
|
|
30158
30228
|
WHEN 'in_progress' THEN 0
|
|
30159
|
-
WHEN '
|
|
30229
|
+
WHEN 'review' THEN 1
|
|
30160
30230
|
WHEN 'blocked' THEN 2
|
|
30161
30231
|
WHEN 'ready' THEN 3
|
|
30162
30232
|
WHEN 'backlog' THEN 4
|
|
@@ -30357,6 +30427,17 @@ function completeTask(taskId, agentId) {
|
|
|
30357
30427
|
if (unchecked.length > 0) {
|
|
30358
30428
|
return { task: null, error: "Not all acceptance criteria are verified", unchecked };
|
|
30359
30429
|
}
|
|
30430
|
+
if (getReviewRequired() && task.status !== "review") {
|
|
30431
|
+
return {
|
|
30432
|
+
task: null,
|
|
30433
|
+
error: `Review required: submit this task for review first.
|
|
30434
|
+
` + ` 1. aitasks review <taskId> --agent $AITASKS_AGENT_ID
|
|
30435
|
+
` + ` 2. Spawn a review sub-agent to inspect the work
|
|
30436
|
+
` + ` 3. Review agent approves: aitasks done <taskId> --agent <review-agent-id>
|
|
30437
|
+
` + ` Review agent rejects: aitasks reject <taskId> --reason "<feedback>"
|
|
30438
|
+
` + " Tasks cannot be moved to Done without a passing review."
|
|
30439
|
+
};
|
|
30440
|
+
}
|
|
30360
30441
|
const updated = updateTask(taskId, { status: "done", completed_at: Date.now() });
|
|
30361
30442
|
logEvent({ task_id: taskId, agent_id: agentId, event_type: "completed", payload: {} });
|
|
30362
30443
|
const pendingRows = db.query(`SELECT id, blocked_by FROM tasks WHERE status != 'done'`).all();
|
|
@@ -30442,7 +30523,7 @@ function reviewTask(taskId, agentId) {
|
|
|
30442
30523
|
if (task.status !== "in_progress") {
|
|
30443
30524
|
return { task: null, error: "Task must be in_progress to request review" };
|
|
30444
30525
|
}
|
|
30445
|
-
const updated = updateTask(taskId, { status: "
|
|
30526
|
+
const updated = updateTask(taskId, { status: "review" });
|
|
30446
30527
|
logEvent({ task_id: taskId, agent_id: agentId, event_type: "review_requested", payload: {} });
|
|
30447
30528
|
return { task: updated };
|
|
30448
30529
|
}
|
|
@@ -30450,8 +30531,8 @@ function rejectTask(taskId, reason, agentId) {
|
|
|
30450
30531
|
const task = getTask(taskId);
|
|
30451
30532
|
if (!task)
|
|
30452
30533
|
return { task: null, error: "Task not found" };
|
|
30453
|
-
if (task.status !== "
|
|
30454
|
-
return { task: null, error: "Task must be in
|
|
30534
|
+
if (task.status !== "review") {
|
|
30535
|
+
return { task: null, error: "Task must be in review status to reject" };
|
|
30455
30536
|
}
|
|
30456
30537
|
updateTask(taskId, { status: "in_progress" });
|
|
30457
30538
|
addImplementationNote(taskId, `REVIEW REJECTED: ${reason}`, agentId ?? "human");
|
|
@@ -38533,7 +38614,7 @@ var STATUS_ICON = {
|
|
|
38533
38614
|
ready: "\u25CE",
|
|
38534
38615
|
in_progress: "\u25B6",
|
|
38535
38616
|
blocked: "\u2298",
|
|
38536
|
-
|
|
38617
|
+
review: "\u25C8",
|
|
38537
38618
|
done: "\u2713"
|
|
38538
38619
|
};
|
|
38539
38620
|
|
|
@@ -40114,7 +40195,7 @@ var STATUS_COLORS = {
|
|
|
40114
40195
|
ready: "blue",
|
|
40115
40196
|
in_progress: "yellow",
|
|
40116
40197
|
blocked: "#FF5C5C",
|
|
40117
|
-
|
|
40198
|
+
review: "magenta",
|
|
40118
40199
|
done: "green"
|
|
40119
40200
|
};
|
|
40120
40201
|
var PRIORITY_COLORS = {
|
|
@@ -40526,7 +40607,7 @@ var STATUS_COLORS2 = {
|
|
|
40526
40607
|
ready: "blue",
|
|
40527
40608
|
in_progress: "yellow",
|
|
40528
40609
|
blocked: "#FF5C5C",
|
|
40529
|
-
|
|
40610
|
+
review: "magenta",
|
|
40530
40611
|
done: "green"
|
|
40531
40612
|
};
|
|
40532
40613
|
var PRIORITY_COLORS2 = {
|
|
@@ -41214,8 +41295,9 @@ var reviewCommand = new Command("review").description("Request human review for
|
|
|
41214
41295
|
if (!json) {
|
|
41215
41296
|
console.log("");
|
|
41216
41297
|
console.log(source_default.magenta(" \u25C8") + ` ${source_default.bold(task.id)} submitted for review`);
|
|
41217
|
-
console.log(source_default.dim(`
|
|
41218
|
-
console.log(source_default.dim(`
|
|
41298
|
+
console.log(source_default.dim(` Spawn a review sub-agent to inspect the implementation.`));
|
|
41299
|
+
console.log(source_default.dim(` Approve: aitasks done ${task.id} --agent <review-agent-id>`));
|
|
41300
|
+
console.log(source_default.dim(` Reject: aitasks reject ${task.id} --reason "<feedback>"`));
|
|
41219
41301
|
}
|
|
41220
41302
|
}
|
|
41221
41303
|
if (json) {
|
|
@@ -41364,6 +41446,9 @@ var updateCommand = new Command("update").description("Update task fields").argu
|
|
|
41364
41446
|
if (Object.keys(changes).length === 0) {
|
|
41365
41447
|
exitError("No changes specified. Use --help to see available options.", json);
|
|
41366
41448
|
}
|
|
41449
|
+
if (opts.status === "done" && getReviewRequired() && task.status !== "review") {
|
|
41450
|
+
exitError("Review required: use `aitasks done` which enforces the review gate.\n" + " Submit for review first: aitasks review <taskId> --agent $AITASKS_AGENT_ID", json);
|
|
41451
|
+
}
|
|
41367
41452
|
const updated = updateTask(id, changes);
|
|
41368
41453
|
if (json)
|
|
41369
41454
|
return jsonOut(true, updated);
|
|
@@ -41387,7 +41472,7 @@ var STATUS_COLORS3 = {
|
|
|
41387
41472
|
ready: "blue",
|
|
41388
41473
|
in_progress: "yellow",
|
|
41389
41474
|
blocked: "#FF5C5C",
|
|
41390
|
-
|
|
41475
|
+
review: "magenta",
|
|
41391
41476
|
done: "green"
|
|
41392
41477
|
};
|
|
41393
41478
|
var PRIORITY_COLORS3 = {
|
|
@@ -41398,11 +41483,11 @@ var PRIORITY_COLORS3 = {
|
|
|
41398
41483
|
};
|
|
41399
41484
|
function buildTree(tasks) {
|
|
41400
41485
|
const sorted = [...tasks].sort((a2, b2) => a2.id.localeCompare(b2.id));
|
|
41401
|
-
const inProgressIds = new Set(sorted.filter((t) => t.status === "in_progress").map((t) => t.id));
|
|
41486
|
+
const inProgressIds = new Set(sorted.filter((t) => t.status === "in_progress" || t.status === "review").map((t) => t.id));
|
|
41402
41487
|
const doneIds = new Set(sorted.filter((t) => t.status === "done").map((t) => t.id));
|
|
41403
41488
|
const shownIds = new Set;
|
|
41404
41489
|
const items = [];
|
|
41405
|
-
const ipCount = sorted.filter((t) => t.status === "in_progress").length;
|
|
41490
|
+
const ipCount = sorted.filter((t) => t.status === "in_progress" || t.status === "review").length;
|
|
41406
41491
|
const doneCount = sorted.filter((t) => t.status === "done").length;
|
|
41407
41492
|
function push(task, indent, isLastSibling, section, showHeader) {
|
|
41408
41493
|
items.push({
|
|
@@ -41416,13 +41501,13 @@ function buildTree(tasks) {
|
|
|
41416
41501
|
shownIds.add(task.id);
|
|
41417
41502
|
}
|
|
41418
41503
|
let firstIp = true;
|
|
41419
|
-
for (const task of sorted.filter((t) => t.status === "in_progress" && !t.parent_id)) {
|
|
41504
|
+
for (const task of sorted.filter((t) => (t.status === "in_progress" || t.status === "review") && !t.parent_id)) {
|
|
41420
41505
|
push(task, 0, false, "in_progress", firstIp);
|
|
41421
41506
|
firstIp = false;
|
|
41422
41507
|
const subs = sorted.filter((t) => t.parent_id === task.id);
|
|
41423
41508
|
subs.forEach((sub, i) => push(sub, 1, i === subs.length - 1, "in_progress", false));
|
|
41424
41509
|
}
|
|
41425
|
-
for (const task of sorted.filter((t) => t.status === "in_progress" && !!t.parent_id)) {
|
|
41510
|
+
for (const task of sorted.filter((t) => (t.status === "in_progress" || t.status === "review") && !!t.parent_id)) {
|
|
41426
41511
|
if (!shownIds.has(task.id)) {
|
|
41427
41512
|
push(task, 0, false, "in_progress", firstIp);
|
|
41428
41513
|
firstIp = false;
|
|
@@ -41546,7 +41631,7 @@ var wrapAnsiLine = (line, maxW) => {
|
|
|
41546
41631
|
result2.push(current);
|
|
41547
41632
|
return result2;
|
|
41548
41633
|
};
|
|
41549
|
-
var RightPane = ({ task, width, height, scrollOffset }) => {
|
|
41634
|
+
var RightPane = ({ task, width, height, scrollOffset, metricsRef }) => {
|
|
41550
41635
|
const { lines, contentW } = import_react29.useMemo(() => {
|
|
41551
41636
|
if (!task)
|
|
41552
41637
|
return { lines: [], contentW: 20 };
|
|
@@ -41616,6 +41701,8 @@ var RightPane = ({ task, width, height, scrollOffset }) => {
|
|
|
41616
41701
|
const visibleH = Math.max(3, height - 2);
|
|
41617
41702
|
const maxOffset = Math.max(0, totalLines - visibleH);
|
|
41618
41703
|
const offset = Math.min(scrollOffset, maxOffset);
|
|
41704
|
+
if (metricsRef)
|
|
41705
|
+
metricsRef.current = { maxOffset, visibleH };
|
|
41619
41706
|
const visibleLines = lines.slice(offset, offset + visibleH);
|
|
41620
41707
|
const scrollbar = import_react29.useMemo(() => {
|
|
41621
41708
|
if (totalLines <= visibleH)
|
|
@@ -41664,7 +41751,7 @@ var RightPane = ({ task, width, height, scrollOffset }) => {
|
|
|
41664
41751
|
}, i, true, undefined, this))
|
|
41665
41752
|
}, undefined, false, undefined, this);
|
|
41666
41753
|
};
|
|
41667
|
-
var PICKER_STATUSES = ["backlog", "ready", "in_progress", "blocked", "
|
|
41754
|
+
var PICKER_STATUSES = ["backlog", "ready", "in_progress", "blocked", "review", "done"];
|
|
41668
41755
|
var StatusPicker = ({ task }) => /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
41669
41756
|
flexDirection: "column",
|
|
41670
41757
|
paddingLeft: 2,
|
|
@@ -41858,6 +41945,8 @@ var TreeBoardComponent = ({ getTasks }) => {
|
|
|
41858
41945
|
return;
|
|
41859
41946
|
}
|
|
41860
41947
|
if (input && !key.ctrl && !key.meta) {
|
|
41948
|
+
if (Date.now() < suppressUntilRef.current)
|
|
41949
|
+
return;
|
|
41861
41950
|
setSearchQuery((q2) => q2 + input);
|
|
41862
41951
|
return;
|
|
41863
41952
|
}
|
|
@@ -41873,7 +41962,7 @@ var TreeBoardComponent = ({ getTasks }) => {
|
|
|
41873
41962
|
"2": "ready",
|
|
41874
41963
|
"3": "in_progress",
|
|
41875
41964
|
"4": "blocked",
|
|
41876
|
-
"5": "
|
|
41965
|
+
"5": "review",
|
|
41877
41966
|
"6": "done"
|
|
41878
41967
|
};
|
|
41879
41968
|
const newStatus = statusMap[input];
|
|
@@ -41910,14 +41999,40 @@ var TreeBoardComponent = ({ getTasks }) => {
|
|
|
41910
41999
|
leftWidthRef.current = leftWidth;
|
|
41911
42000
|
const scrollOffsetRef = import_react29.useRef(scrollOffset);
|
|
41912
42001
|
scrollOffsetRef.current = scrollOffset;
|
|
42002
|
+
const colsRef = import_react29.useRef(cols);
|
|
42003
|
+
colsRef.current = cols;
|
|
42004
|
+
const rowsRef = import_react29.useRef(rows);
|
|
42005
|
+
rowsRef.current = rows;
|
|
42006
|
+
const leftScrollOffsetRef = import_react29.useRef(leftScrollOffset);
|
|
42007
|
+
leftScrollOffsetRef.current = leftScrollOffset;
|
|
42008
|
+
const leftMaxOffsetRef = import_react29.useRef(0);
|
|
42009
|
+
const rightMetricsRef = import_react29.useRef({ maxOffset: 0, visibleH: 0 });
|
|
42010
|
+
const dragRef = import_react29.useRef(null);
|
|
42011
|
+
const visibleLeftRowsRef = import_react29.useRef([]);
|
|
42012
|
+
const suppressUntilRef = import_react29.useRef(0);
|
|
42013
|
+
const searchActiveRef = import_react29.useRef(false);
|
|
41913
42014
|
import_react29.useEffect(() => {
|
|
41914
|
-
process.stdout.write("\x1B[?
|
|
42015
|
+
process.stdout.write("\x1B[?1002h\x1B[?1006h");
|
|
42016
|
+
const jumpToRow = (row, pane) => {
|
|
42017
|
+
const contentStart = 4;
|
|
42018
|
+
const visH = Math.max(1, rowsRef.current - 4);
|
|
42019
|
+
const relRow = Math.max(0, Math.min(row - contentStart, visH - 1));
|
|
42020
|
+
const ratio = visH > 1 ? relRow / (visH - 1) : 0;
|
|
42021
|
+
if (pane === "left") {
|
|
42022
|
+
setLeftScrollOffset(Math.round(ratio * leftMaxOffsetRef.current));
|
|
42023
|
+
} else {
|
|
42024
|
+
setScrollOffset(Math.round(ratio * rightMetricsRef.current.maxOffset));
|
|
42025
|
+
}
|
|
42026
|
+
};
|
|
41915
42027
|
const onData = (buf) => {
|
|
41916
42028
|
const str = buf.toString("latin1");
|
|
41917
|
-
const sgr = str.match(/\x1b\[<(\d+);(\d+);(\d+)
|
|
42029
|
+
const sgr = str.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
|
|
41918
42030
|
if (sgr) {
|
|
41919
42031
|
const btn = parseInt(sgr[1], 10);
|
|
41920
42032
|
const col = parseInt(sgr[2], 10) - 1;
|
|
42033
|
+
const row = parseInt(sgr[3], 10) - 1;
|
|
42034
|
+
const isRelease = sgr[4] === "m";
|
|
42035
|
+
suppressUntilRef.current = Date.now() + 150;
|
|
41921
42036
|
if (btn === 64 || btn === 65) {
|
|
41922
42037
|
if (col < leftWidthRef.current) {
|
|
41923
42038
|
if (btn === 64)
|
|
@@ -41930,6 +42045,34 @@ var TreeBoardComponent = ({ getTasks }) => {
|
|
|
41930
42045
|
if (btn === 65)
|
|
41931
42046
|
setScrollOffset((o) => o + 8);
|
|
41932
42047
|
}
|
|
42048
|
+
return;
|
|
42049
|
+
}
|
|
42050
|
+
if (isRelease) {
|
|
42051
|
+
dragRef.current = null;
|
|
42052
|
+
return;
|
|
42053
|
+
}
|
|
42054
|
+
if (btn === 0 || btn === 32) {
|
|
42055
|
+
const leftSbCol = leftWidthRef.current - 2;
|
|
42056
|
+
const rightSbCol = colsRef.current - 3;
|
|
42057
|
+
if (btn === 0) {
|
|
42058
|
+
if (col === leftSbCol)
|
|
42059
|
+
dragRef.current = "left";
|
|
42060
|
+
else if (col >= rightSbCol)
|
|
42061
|
+
dragRef.current = "right";
|
|
42062
|
+
else {
|
|
42063
|
+
dragRef.current = null;
|
|
42064
|
+
if (col < leftWidthRef.current - 1) {
|
|
42065
|
+
const visRowIdx = row - (searchActiveRef.current ? 4 : 3);
|
|
42066
|
+
const clicked = visibleLeftRowsRef.current[visRowIdx];
|
|
42067
|
+
if (clicked?.kind === "item")
|
|
42068
|
+
setSelectedIdx(clicked.itemIdx);
|
|
42069
|
+
}
|
|
42070
|
+
}
|
|
42071
|
+
}
|
|
42072
|
+
if (dragRef.current === "left")
|
|
42073
|
+
jumpToRow(row, "left");
|
|
42074
|
+
if (dragRef.current === "right")
|
|
42075
|
+
jumpToRow(row, "right");
|
|
41933
42076
|
}
|
|
41934
42077
|
return;
|
|
41935
42078
|
}
|
|
@@ -41951,9 +42094,9 @@ var TreeBoardComponent = ({ getTasks }) => {
|
|
|
41951
42094
|
}
|
|
41952
42095
|
}
|
|
41953
42096
|
};
|
|
41954
|
-
process.stdin.
|
|
42097
|
+
process.stdin.prependListener("data", onData);
|
|
41955
42098
|
return () => {
|
|
41956
|
-
process.stdout.write("\x1B[?1006l\x1B[?
|
|
42099
|
+
process.stdout.write("\x1B[?1006l\x1B[?1002l");
|
|
41957
42100
|
process.stdin.off("data", onData);
|
|
41958
42101
|
};
|
|
41959
42102
|
}, []);
|
|
@@ -41962,9 +42105,12 @@ var TreeBoardComponent = ({ getTasks }) => {
|
|
|
41962
42105
|
const leftVisibleH = Math.max(1, rows - 4);
|
|
41963
42106
|
const leftTotalRows = leftRows.length;
|
|
41964
42107
|
const leftMaxOffset = Math.max(0, leftTotalRows - leftVisibleH);
|
|
42108
|
+
leftMaxOffsetRef.current = leftMaxOffset;
|
|
42109
|
+
searchActiveRef.current = mode === "search" || !!searchQuery;
|
|
41965
42110
|
const leftOffset = Math.min(leftScrollOffset, leftMaxOffset);
|
|
41966
42111
|
const rawLeftRows = leftRows.slice(leftOffset, leftOffset + leftVisibleH);
|
|
41967
42112
|
const visibleLeftRows = rawLeftRows[0]?.kind === "date-sep" ? rawLeftRows.slice(1) : rawLeftRows;
|
|
42113
|
+
visibleLeftRowsRef.current = visibleLeftRows;
|
|
41968
42114
|
const leftScrollbar = (() => {
|
|
41969
42115
|
const bar = Array(leftVisibleH).fill(" ");
|
|
41970
42116
|
if (leftTotalRows > leftVisibleH) {
|
|
@@ -42194,7 +42340,8 @@ var TreeBoardComponent = ({ getTasks }) => {
|
|
|
42194
42340
|
task: selectedTask,
|
|
42195
42341
|
width: rightInner,
|
|
42196
42342
|
height: rightHeight,
|
|
42197
|
-
scrollOffset
|
|
42343
|
+
scrollOffset,
|
|
42344
|
+
metricsRef: rightMetricsRef
|
|
42198
42345
|
}, undefined, false, undefined, this)
|
|
42199
42346
|
}, undefined, false, undefined, this)
|
|
42200
42347
|
]
|
|
@@ -42212,10 +42359,10 @@ var STATUS_LABELS = {
|
|
|
42212
42359
|
ready: "READY",
|
|
42213
42360
|
in_progress: "IN PROGRESS",
|
|
42214
42361
|
blocked: "BLOCKED",
|
|
42215
|
-
|
|
42362
|
+
review: "REVIEW",
|
|
42216
42363
|
done: "DONE"
|
|
42217
42364
|
};
|
|
42218
|
-
var ALL_STATUSES = ["in_progress", "
|
|
42365
|
+
var ALL_STATUSES = ["in_progress", "review", "ready", "blocked", "backlog", "done"];
|
|
42219
42366
|
var StaticCard = ({ task }) => {
|
|
42220
42367
|
const priColor = PRIORITY_COLORS3[task.priority] ?? "gray";
|
|
42221
42368
|
return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
@@ -42482,13 +42629,14 @@ var exportCommand = new Command("export").description("Export all task data").op
|
|
|
42482
42629
|
// src/commands/onboard.ts
|
|
42483
42630
|
var VERSION = "1.0.0";
|
|
42484
42631
|
var onboardCommand = new Command("onboard").description("Print or inject agent protocol instructions").option("--append", "Auto-detect and append to CLAUDE.md/AGENTS.md/GEMINI.md").option("--file <path>", "Append to a specific file").option("--json", "Output the instructions as a JSON string").action((opts) => {
|
|
42485
|
-
const
|
|
42632
|
+
const reviewRequired = isInitialized() ? getReviewRequired() : false;
|
|
42633
|
+
const instructions = getAgentInstructions(VERSION, { reviewRequired });
|
|
42486
42634
|
if (opts.json) {
|
|
42487
42635
|
console.log(JSON.stringify({ instructions }, null, 2));
|
|
42488
42636
|
return;
|
|
42489
42637
|
}
|
|
42490
42638
|
if (opts.file) {
|
|
42491
|
-
const result2 = appendToSpecificFile(opts.file, VERSION);
|
|
42639
|
+
const result2 = appendToSpecificFile(opts.file, VERSION, reviewRequired);
|
|
42492
42640
|
const rel = opts.file.replace(process.cwd() + "/", "");
|
|
42493
42641
|
switch (result2.action) {
|
|
42494
42642
|
case "created":
|
|
@@ -42505,7 +42653,7 @@ var onboardCommand = new Command("onboard").description("Print or inject agent p
|
|
|
42505
42653
|
}
|
|
42506
42654
|
if (opts.append) {
|
|
42507
42655
|
const root = findProjectRoot();
|
|
42508
|
-
const result2 = injectOrCreateAgentFile(root, VERSION);
|
|
42656
|
+
const result2 = injectOrCreateAgentFile(root, VERSION, reviewRequired);
|
|
42509
42657
|
const rel = result2.filePath.replace(process.cwd() + "/", "");
|
|
42510
42658
|
switch (result2.action) {
|
|
42511
42659
|
case "created":
|
|
@@ -42676,7 +42824,7 @@ function getStatusIcon(status) {
|
|
|
42676
42824
|
ready: "\u25D0",
|
|
42677
42825
|
in_progress: "\u25B6",
|
|
42678
42826
|
blocked: "\u2298",
|
|
42679
|
-
|
|
42827
|
+
review: "\u25C8",
|
|
42680
42828
|
done: "\u2713"
|
|
42681
42829
|
};
|
|
42682
42830
|
return icons[status] || "\u25CB";
|
|
@@ -42716,7 +42864,7 @@ var searchCommand = new Command("search").description("Full-text search across t
|
|
|
42716
42864
|
const statusIcon = getStatusIcon2(task.status);
|
|
42717
42865
|
const priColor = getPriorityColor(task.priority);
|
|
42718
42866
|
console.log("");
|
|
42719
|
-
console.log(`${source_default.bold.white(task.id)} ` + `${priColor(task.priority[0].toUpperCase())} ` + `${statusIcon} ` + highlightMatches(task.title, searchTerms));
|
|
42867
|
+
console.log(`${source_default.bold.white(task.id)} ` + `${priColor((task.priority[0] ?? "?").toUpperCase())} ` + `${statusIcon} ` + highlightMatches(task.title, searchTerms));
|
|
42720
42868
|
const context = getSearchContext(task, searchTerms);
|
|
42721
42869
|
if (context) {
|
|
42722
42870
|
console.log(source_default.dim(` ${context}`));
|
|
@@ -42775,7 +42923,7 @@ function getStatusIcon2(status) {
|
|
|
42775
42923
|
ready: source_default.blue("\u25D0"),
|
|
42776
42924
|
in_progress: source_default.yellow("\u25B6"),
|
|
42777
42925
|
blocked: source_default.hex("#FF5C5C")("\u2298"),
|
|
42778
|
-
|
|
42926
|
+
review: source_default.magenta("\u25C8"),
|
|
42779
42927
|
done: source_default.green("\u2713")
|
|
42780
42928
|
};
|
|
42781
42929
|
return icons[status] || "\u25CB";
|
|
@@ -42865,25 +43013,26 @@ var undoCommand = new Command("undo").description("Undo the last action on a tas
|
|
|
42865
43013
|
}
|
|
42866
43014
|
});
|
|
42867
43015
|
function undoComplete(task) {
|
|
43016
|
+
const newStatus = getReviewRequired() ? "review" : "in_progress";
|
|
42868
43017
|
updateTask(task.id, {
|
|
42869
|
-
status:
|
|
43018
|
+
status: newStatus,
|
|
42870
43019
|
completed_at: null
|
|
42871
43020
|
});
|
|
42872
43021
|
return {
|
|
42873
43022
|
success: true,
|
|
42874
|
-
message: `${task.id} reverted from done to
|
|
42875
|
-
data: { task: task.id, previousStatus: "done", newStatus
|
|
43023
|
+
message: `${task.id} reverted from done to ${newStatus}`,
|
|
43024
|
+
data: { task: task.id, previousStatus: "done", newStatus }
|
|
42876
43025
|
};
|
|
42877
43026
|
}
|
|
42878
43027
|
function undoReviewRequested(task) {
|
|
42879
|
-
if (task.status !== "
|
|
42880
|
-
return { success: false, message: `${task.id} is not in
|
|
43028
|
+
if (task.status !== "review") {
|
|
43029
|
+
return { success: false, message: `${task.id} is not in review status` };
|
|
42881
43030
|
}
|
|
42882
43031
|
updateTask(task.id, { status: "in_progress" });
|
|
42883
43032
|
return {
|
|
42884
43033
|
success: true,
|
|
42885
|
-
message: `${task.id} reverted from
|
|
42886
|
-
data: { task: task.id, previousStatus: "
|
|
43034
|
+
message: `${task.id} reverted from review to in_progress`,
|
|
43035
|
+
data: { task: task.id, previousStatus: "review", newStatus: "in_progress" }
|
|
42887
43036
|
};
|
|
42888
43037
|
}
|
|
42889
43038
|
function undoStarted(task) {
|
|
@@ -43073,4 +43222,4 @@ program2.parseAsync(process.argv).catch((err) => {
|
|
|
43073
43222
|
process.exit(1);
|
|
43074
43223
|
});
|
|
43075
43224
|
|
|
43076
|
-
//# debugId=
|
|
43225
|
+
//# debugId=B0EAB2DE4FE47CF564756E2164756E21
|