@metyatech/task-tracker 0.2.3 → 0.2.5
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 +15 -5
- package/dist/cli.js +172 -52
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,7 +30,9 @@ npm link
|
|
|
30
30
|
|
|
31
31
|
Tasks are stored in `<git-repo-root>/.tasks.jsonl` (JSONL format, one JSON object per line). All commands must be run from within a git repository. The file is created automatically on first `add`.
|
|
32
32
|
|
|
33
|
-
Each task: `{ id, description, stage, createdAt, updatedAt }`
|
|
33
|
+
Each task: `{ id, description, stage, committedEventId?, createdAt, updatedAt }`
|
|
34
|
+
|
|
35
|
+
`committedEventId` is a unique ID written when the task transitions to `committed`. It is used to locate the git commit that introduced the event into `.tasks.jsonl`, enabling accurate `pushed` detection even when `update --stage committed` is called before the actual commit.
|
|
34
36
|
|
|
35
37
|
To sync tasks across PCs, commit `.tasks.jsonl` and push/pull like any other file.
|
|
36
38
|
|
|
@@ -56,9 +58,9 @@ task-tracker list --json
|
|
|
56
58
|
### Update a task
|
|
57
59
|
|
|
58
60
|
```bash
|
|
59
|
-
task-tracker update <id> --stage
|
|
61
|
+
task-tracker update <id> --stage committed
|
|
60
62
|
task-tracker update <id> --description "Updated description"
|
|
61
|
-
task-tracker update <id> --stage
|
|
63
|
+
task-tracker update <id> --stage released --json
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
### Mark done
|
|
@@ -89,9 +91,17 @@ The `check` command:
|
|
|
89
91
|
|
|
90
92
|
## Lifecycle Stages
|
|
91
93
|
|
|
92
|
-
`pending` → `in-progress` → `
|
|
94
|
+
Persisted stages: `pending` → `in-progress` → `committed` → `released` → `done`
|
|
95
|
+
|
|
96
|
+
The `pushed` stage is **derived** and cannot be set manually. When a task is in `committed`
|
|
97
|
+
stage, `list` and `check` display its effective stage as `pushed` automatically once the
|
|
98
|
+
`.tasks.jsonl` commit that first introduced the task's `committedEventId` is reachable from
|
|
99
|
+
the remote upstream branch (`git merge-base --is-ancestor`). This means derivation is
|
|
100
|
+
accurate even when `update --stage committed` is called before the actual git commit.
|
|
101
|
+
|
|
102
|
+
To filter by effective `pushed` stage: `task-tracker list --stage pushed`
|
|
93
103
|
|
|
94
|
-
|
|
104
|
+
Setting `--stage pushed` on `add` or `update` is an error.
|
|
95
105
|
|
|
96
106
|
## Dev Commands
|
|
97
107
|
|
package/dist/cli.js
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
|
-
import { dirname as dirname3, join as
|
|
6
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
7
7
|
import { readFileSync as readFileSync3 } from "fs";
|
|
8
8
|
|
|
9
9
|
// src/storage.ts
|
|
10
10
|
import { readFileSync, writeFileSync, existsSync, appendFileSync, mkdirSync } from "fs";
|
|
11
|
-
import { dirname, join } from "path";
|
|
11
|
+
import { dirname, join as join2 } from "path";
|
|
12
12
|
|
|
13
13
|
// src/git.ts
|
|
14
14
|
import { execSync } from "child_process";
|
|
15
|
+
import { join, relative } from "path";
|
|
15
16
|
function tryExec(cmd, cwd) {
|
|
16
17
|
try {
|
|
17
18
|
return execSync(cmd, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
@@ -55,11 +56,36 @@ function getRepoStatus(repoPath) {
|
|
|
55
56
|
}
|
|
56
57
|
return status;
|
|
57
58
|
}
|
|
59
|
+
function isCommitReachableFromUpstream(repoPath, commit) {
|
|
60
|
+
if (!/^[0-9a-f]{4,40}$/i.test(commit)) return false;
|
|
61
|
+
const result = tryExec(`git merge-base --is-ancestor ${commit} @{u}`, repoPath);
|
|
62
|
+
return result !== null;
|
|
63
|
+
}
|
|
64
|
+
function findCommitByEventId(repoPath, storageFile, eventId) {
|
|
65
|
+
if (!/^[A-Za-z0-9_-]{4,64}$/.test(eventId)) return null;
|
|
66
|
+
const relPath = relative(repoPath, storageFile).replace(/\\/g, "/");
|
|
67
|
+
const result = tryExec(`git log -S "${eventId}" --format=%H -- "${relPath}"`, repoPath);
|
|
68
|
+
if (!result) return null;
|
|
69
|
+
const hash = result.split("\n")[0].trim();
|
|
70
|
+
return hash || null;
|
|
71
|
+
}
|
|
72
|
+
function deriveEffectiveStage(task, repoPath) {
|
|
73
|
+
if (task.stage === "committed") {
|
|
74
|
+
if (task.committedEventId) {
|
|
75
|
+
const storageFile = join(repoPath, ".tasks.jsonl");
|
|
76
|
+
const commit = findCommitByEventId(repoPath, storageFile, task.committedEventId);
|
|
77
|
+
if (commit && isCommitReachableFromUpstream(repoPath, commit)) {
|
|
78
|
+
return "pushed";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return task.stage;
|
|
83
|
+
}
|
|
58
84
|
|
|
59
85
|
// src/storage.ts
|
|
60
86
|
function getStoragePath() {
|
|
61
87
|
const root = getRepoRoot();
|
|
62
|
-
return
|
|
88
|
+
return join2(root, ".tasks.jsonl");
|
|
63
89
|
}
|
|
64
90
|
function ensureStorageDir(storagePath) {
|
|
65
91
|
const dir = dirname(storagePath);
|
|
@@ -74,7 +100,15 @@ function readTasks(storagePath) {
|
|
|
74
100
|
}
|
|
75
101
|
const content = readFileSync(storagePath, "utf-8").trim();
|
|
76
102
|
if (!content) return [];
|
|
77
|
-
return content.split("\n").filter((line) => line.trim()).map((line) =>
|
|
103
|
+
return content.split("\n").filter((line) => line.trim()).map((line) => {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(line);
|
|
106
|
+
} catch {
|
|
107
|
+
process.stderr.write(`[task-tracker] Warning: skipping malformed line in tasks file
|
|
108
|
+
`);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}).filter((task) => task !== null);
|
|
78
112
|
}
|
|
79
113
|
function writeTasks(storagePath, tasks) {
|
|
80
114
|
ensureStorageDir(storagePath);
|
|
@@ -90,19 +124,7 @@ function addTaskToStorage(storagePath, task) {
|
|
|
90
124
|
import { nanoid } from "nanoid";
|
|
91
125
|
|
|
92
126
|
// src/types.ts
|
|
93
|
-
var STAGES = [
|
|
94
|
-
"pending",
|
|
95
|
-
"in-progress",
|
|
96
|
-
"implemented",
|
|
97
|
-
"verified",
|
|
98
|
-
"committed",
|
|
99
|
-
"pushed",
|
|
100
|
-
"pr-created",
|
|
101
|
-
"merged",
|
|
102
|
-
"released",
|
|
103
|
-
"published",
|
|
104
|
-
"done"
|
|
105
|
-
];
|
|
127
|
+
var STAGES = ["pending", "in-progress", "committed", "released", "done"];
|
|
106
128
|
var DONE_STAGES = ["done"];
|
|
107
129
|
|
|
108
130
|
// src/tasks.ts
|
|
@@ -135,6 +157,9 @@ function updateTask(storagePath, id, updates) {
|
|
|
135
157
|
const task = tasks[idx];
|
|
136
158
|
if (updates.stage !== void 0) task.stage = updates.stage;
|
|
137
159
|
if (updates.description !== void 0) task.description = updates.description;
|
|
160
|
+
if (updates.stage === "committed" && !task.committedEventId) {
|
|
161
|
+
task.committedEventId = nanoid(16);
|
|
162
|
+
}
|
|
138
163
|
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
139
164
|
writeTasks(storagePath, tasks);
|
|
140
165
|
return task;
|
|
@@ -176,32 +201,28 @@ import chalk from "chalk";
|
|
|
176
201
|
var STAGE_COLORS = {
|
|
177
202
|
pending: (s) => chalk.gray(s),
|
|
178
203
|
"in-progress": (s) => chalk.blue(s),
|
|
179
|
-
implemented: (s) => chalk.cyan(s),
|
|
180
|
-
verified: (s) => chalk.yellow(s),
|
|
181
204
|
committed: (s) => chalk.magenta(s),
|
|
182
205
|
pushed: (s) => chalk.green(s),
|
|
183
|
-
"pr-created": (s) => chalk.greenBright(s),
|
|
184
|
-
merged: (s) => chalk.green(s),
|
|
185
206
|
released: (s) => chalk.blueBright(s),
|
|
186
|
-
published: (s) => chalk.greenBright(s),
|
|
187
207
|
done: (s) => chalk.dim(s)
|
|
188
208
|
};
|
|
189
209
|
function stageColor(stage) {
|
|
190
210
|
const fn = STAGE_COLORS[stage] ?? ((s) => chalk.white(s));
|
|
191
211
|
return fn(stage);
|
|
192
212
|
}
|
|
193
|
-
function formatTask(task) {
|
|
194
|
-
const
|
|
213
|
+
function formatTask(task, effectiveStage) {
|
|
214
|
+
const displayStage = effectiveStage ?? task.stage;
|
|
215
|
+
const stage = stageColor(displayStage);
|
|
195
216
|
const id = chalk.bold(task.id);
|
|
196
217
|
return `${id} ${stage} ${task.description}`;
|
|
197
218
|
}
|
|
198
|
-
function formatTaskTable(tasks) {
|
|
219
|
+
function formatTaskTable(tasks, getEffectiveStage) {
|
|
199
220
|
if (tasks.length === 0) {
|
|
200
221
|
return chalk.dim("No tasks found.");
|
|
201
222
|
}
|
|
202
|
-
return tasks.map(formatTask).join("\n");
|
|
223
|
+
return tasks.map((t) => formatTask(t, getEffectiveStage?.(t))).join("\n");
|
|
203
224
|
}
|
|
204
|
-
function formatCheckReport(activeTasks, repoStatus) {
|
|
225
|
+
function formatCheckReport(activeTasks, repoStatus, getEffectiveStage) {
|
|
205
226
|
const lines = [];
|
|
206
227
|
lines.push(chalk.bold("=== Task Tracker Check ==="));
|
|
207
228
|
lines.push("");
|
|
@@ -210,7 +231,7 @@ function formatCheckReport(activeTasks, repoStatus) {
|
|
|
210
231
|
lines.push(chalk.dim(" No active tasks."));
|
|
211
232
|
} else {
|
|
212
233
|
for (const t of activeTasks) {
|
|
213
|
-
lines.push(" " + formatTask(t));
|
|
234
|
+
lines.push(" " + formatTask(t, getEffectiveStage?.(t)));
|
|
214
235
|
}
|
|
215
236
|
}
|
|
216
237
|
lines.push("");
|
|
@@ -242,25 +263,25 @@ function formatCheckReport(activeTasks, repoStatus) {
|
|
|
242
263
|
// src/gui.ts
|
|
243
264
|
import { createServer } from "http";
|
|
244
265
|
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
245
|
-
import { join as
|
|
266
|
+
import { join as join4, dirname as dirname2, resolve } from "path";
|
|
246
267
|
import { fileURLToPath } from "url";
|
|
247
268
|
import { exec } from "child_process";
|
|
248
269
|
|
|
249
270
|
// src/scanner.ts
|
|
250
271
|
import { existsSync as existsSync2, readdirSync, statSync } from "fs";
|
|
251
|
-
import { join as
|
|
272
|
+
import { join as join3, basename } from "path";
|
|
252
273
|
function scanTaskFiles(dir) {
|
|
253
|
-
const rootFile =
|
|
274
|
+
const rootFile = join3(dir, ".tasks.jsonl");
|
|
254
275
|
const root = existsSync2(rootFile) ? { path: rootFile, dir, name: basename(dir) } : null;
|
|
255
276
|
const repos = [];
|
|
256
277
|
try {
|
|
257
278
|
const entries = readdirSync(dir);
|
|
258
279
|
for (const entry of entries) {
|
|
259
280
|
if (entry.startsWith(".")) continue;
|
|
260
|
-
const subDir =
|
|
281
|
+
const subDir = join3(dir, entry);
|
|
261
282
|
try {
|
|
262
283
|
if (statSync(subDir).isDirectory()) {
|
|
263
|
-
const taskFile =
|
|
284
|
+
const taskFile = join3(subDir, ".tasks.jsonl");
|
|
264
285
|
if (existsSync2(taskFile)) {
|
|
265
286
|
repos.push({ path: taskFile, dir: subDir, name: entry });
|
|
266
287
|
}
|
|
@@ -278,8 +299,8 @@ var __filename = fileURLToPath(import.meta.url);
|
|
|
278
299
|
var __dirname = dirname2(__filename);
|
|
279
300
|
function getHtmlPath() {
|
|
280
301
|
const candidates = [
|
|
281
|
-
|
|
282
|
-
|
|
302
|
+
join4(__dirname, "public", "index.html"),
|
|
303
|
+
join4(__dirname, "..", "public", "index.html")
|
|
283
304
|
];
|
|
284
305
|
for (const p of candidates) {
|
|
285
306
|
if (existsSync3(p)) return p;
|
|
@@ -387,7 +408,7 @@ function startGui(dir, port = 3333) {
|
|
|
387
408
|
if (pathname === "/api/tasks/purge" && method === "POST") {
|
|
388
409
|
const body = await parseBody(req);
|
|
389
410
|
const targetDir = typeof body.dir === "string" ? body.dir : resolvedDir;
|
|
390
|
-
const taskFile =
|
|
411
|
+
const taskFile = join4(targetDir, ".tasks.jsonl");
|
|
391
412
|
const result = purgeTasks(taskFile);
|
|
392
413
|
sendJson(res, { count: result.count, ids: result.purged.map((t) => t.id) });
|
|
393
414
|
return;
|
|
@@ -399,7 +420,7 @@ function startGui(dir, port = 3333) {
|
|
|
399
420
|
return;
|
|
400
421
|
}
|
|
401
422
|
const targetDir = typeof body.dir === "string" ? body.dir : resolvedDir;
|
|
402
|
-
const taskFile =
|
|
423
|
+
const taskFile = join4(targetDir, ".tasks.jsonl");
|
|
403
424
|
const stage = typeof body.stage === "string" && STAGES.includes(body.stage) ? body.stage : void 0;
|
|
404
425
|
const task = createTask(taskFile, body.description, { stage });
|
|
405
426
|
sendJson(res, task, 201);
|
|
@@ -411,7 +432,7 @@ function startGui(dir, port = 3333) {
|
|
|
411
432
|
if (method === "PUT") {
|
|
412
433
|
const body = await parseBody(req);
|
|
413
434
|
const targetDir = typeof body.dir === "string" ? body.dir : resolvedDir;
|
|
414
|
-
const taskFile =
|
|
435
|
+
const taskFile = join4(targetDir, ".tasks.jsonl");
|
|
415
436
|
const updates = {};
|
|
416
437
|
if (typeof body.stage === "string" && STAGES.includes(body.stage)) {
|
|
417
438
|
updates.stage = body.stage;
|
|
@@ -429,7 +450,7 @@ function startGui(dir, port = 3333) {
|
|
|
429
450
|
}
|
|
430
451
|
if (method === "DELETE") {
|
|
431
452
|
const queryDir = url.searchParams.get("dir") ?? resolvedDir;
|
|
432
|
-
const taskFile =
|
|
453
|
+
const taskFile = join4(queryDir, ".tasks.jsonl");
|
|
433
454
|
const removed = removeTask(taskFile, id);
|
|
434
455
|
if (!removed) {
|
|
435
456
|
sendError(res, "Task not found", 404);
|
|
@@ -463,15 +484,34 @@ function startGui(dir, port = 3333) {
|
|
|
463
484
|
import process2 from "process";
|
|
464
485
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
465
486
|
var __dirname2 = dirname3(__filename2);
|
|
487
|
+
var HYPHENATED_TASK_ID_PATTERN = /^-[A-Za-z0-9_-]{7}$/;
|
|
488
|
+
var TASK_ID_COMMAND_OPTIONS = {
|
|
489
|
+
update: {
|
|
490
|
+
"--stage": true,
|
|
491
|
+
"--description": true,
|
|
492
|
+
"--json": false
|
|
493
|
+
},
|
|
494
|
+
done: {
|
|
495
|
+
"--keep": true
|
|
496
|
+
},
|
|
497
|
+
remove: {}
|
|
498
|
+
};
|
|
466
499
|
var version = "0.0.0";
|
|
467
500
|
try {
|
|
468
|
-
const pkg = JSON.parse(readFileSync3(
|
|
501
|
+
const pkg = JSON.parse(readFileSync3(join5(__dirname2, "..", "package.json"), "utf-8"));
|
|
469
502
|
version = pkg.version;
|
|
470
503
|
} catch {
|
|
471
504
|
}
|
|
472
505
|
function isValidStage(s) {
|
|
473
506
|
return STAGES.includes(s);
|
|
474
507
|
}
|
|
508
|
+
function isDerivedStage(s) {
|
|
509
|
+
return s === "pushed";
|
|
510
|
+
}
|
|
511
|
+
function derivedStageError(stage) {
|
|
512
|
+
return `\`${stage}\` is a derived stage and cannot be set manually.
|
|
513
|
+
Set stage to \`committed\` instead; \`${stage}\` is derived automatically when the commit containing \`committedEventId\` is reachable from the upstream branch.`;
|
|
514
|
+
}
|
|
475
515
|
function getStorage() {
|
|
476
516
|
try {
|
|
477
517
|
return getStoragePath();
|
|
@@ -480,9 +520,46 @@ function getStorage() {
|
|
|
480
520
|
process2.exit(1);
|
|
481
521
|
}
|
|
482
522
|
}
|
|
523
|
+
function normalizeHyphenatedTaskIdArg(argv) {
|
|
524
|
+
const commandName = argv[2];
|
|
525
|
+
const optionSpec = TASK_ID_COMMAND_OPTIONS[commandName];
|
|
526
|
+
if (!optionSpec) {
|
|
527
|
+
return argv;
|
|
528
|
+
}
|
|
529
|
+
const args = argv.slice(3);
|
|
530
|
+
if (args.includes("--")) {
|
|
531
|
+
return argv;
|
|
532
|
+
}
|
|
533
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
534
|
+
const token = args[i];
|
|
535
|
+
const consumesValue = optionSpec[token];
|
|
536
|
+
if (consumesValue !== void 0) {
|
|
537
|
+
if (consumesValue) {
|
|
538
|
+
i += 1;
|
|
539
|
+
}
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (!token.startsWith("-")) {
|
|
543
|
+
return argv;
|
|
544
|
+
}
|
|
545
|
+
if (!HYPHENATED_TASK_ID_PATTERN.test(token)) {
|
|
546
|
+
return argv;
|
|
547
|
+
}
|
|
548
|
+
return [...argv.slice(0, 3), ...args.slice(0, i), ...args.slice(i + 1), "--", token];
|
|
549
|
+
}
|
|
550
|
+
return argv;
|
|
551
|
+
}
|
|
483
552
|
var program = new Command();
|
|
484
553
|
program.name("task-tracker").description("Persistent task lifecycle tracker for AI agent sessions").version(version, "-V, --version");
|
|
485
|
-
program.command("add <description>").description("Add a new task").option(
|
|
554
|
+
program.command("add <description>").description("Add a new task").option(
|
|
555
|
+
"--stage <stage>",
|
|
556
|
+
`Initial stage (valid: ${STAGES.join(", ")}; \`pushed\` is derived, not settable)`,
|
|
557
|
+
"pending"
|
|
558
|
+
).option("--json", "Output created task as JSON").action((description, opts) => {
|
|
559
|
+
if (isDerivedStage(opts.stage)) {
|
|
560
|
+
console.error(derivedStageError(opts.stage));
|
|
561
|
+
process2.exit(1);
|
|
562
|
+
}
|
|
486
563
|
if (!isValidStage(opts.stage)) {
|
|
487
564
|
console.error(`Invalid stage: ${opts.stage}
|
|
488
565
|
Valid stages: ${STAGES.join(", ")}`);
|
|
@@ -495,30 +572,63 @@ Valid stages: ${STAGES.join(", ")}`);
|
|
|
495
572
|
console.log("Created: " + formatTask(task));
|
|
496
573
|
}
|
|
497
574
|
});
|
|
498
|
-
program.command("list").description("List tasks").option("--all", "Include completed/done tasks").option("--stage <stage>",
|
|
499
|
-
const
|
|
500
|
-
|
|
575
|
+
program.command("list").description("List tasks").option("--all", "Include completed/done tasks").option("--stage <stage>", `Filter by stage; use \`pushed\` to filter by derived effective stage`).option("--json", "JSON output").action((opts) => {
|
|
576
|
+
const storage = getStorage();
|
|
577
|
+
const repoRoot = dirname3(storage);
|
|
578
|
+
if (opts.stage === "pushed") {
|
|
579
|
+
const committed = listTasks(storage, { all: opts.all, stage: "committed" });
|
|
580
|
+
const tasks2 = committed.filter((t) => deriveEffectiveStage(t, repoRoot) === "pushed");
|
|
581
|
+
if (opts.json) {
|
|
582
|
+
console.log(
|
|
583
|
+
JSON.stringify(
|
|
584
|
+
tasks2.map((t) => ({ ...t, effectiveStage: "pushed" })),
|
|
585
|
+
null,
|
|
586
|
+
2
|
|
587
|
+
)
|
|
588
|
+
);
|
|
589
|
+
} else {
|
|
590
|
+
console.log(formatTaskTable(tasks2, () => "pushed"));
|
|
591
|
+
}
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (opts.stage && !isValidStage(opts.stage)) {
|
|
501
595
|
console.error(`Invalid stage: ${opts.stage}
|
|
502
596
|
Valid stages: ${STAGES.join(", ")}`);
|
|
503
597
|
process2.exit(1);
|
|
504
598
|
}
|
|
505
|
-
const
|
|
599
|
+
const stage = opts.stage ? opts.stage : void 0;
|
|
600
|
+
const tasks = listTasks(storage, { all: opts.all, stage });
|
|
601
|
+
const getEffective = (t) => deriveEffectiveStage(t, repoRoot);
|
|
506
602
|
if (opts.json) {
|
|
507
|
-
console.log(
|
|
603
|
+
console.log(
|
|
604
|
+
JSON.stringify(
|
|
605
|
+
tasks.map((t) => ({ ...t, effectiveStage: getEffective(t) })),
|
|
606
|
+
null,
|
|
607
|
+
2
|
|
608
|
+
)
|
|
609
|
+
);
|
|
508
610
|
} else {
|
|
509
|
-
console.log(formatTaskTable(tasks));
|
|
611
|
+
console.log(formatTaskTable(tasks, getEffective));
|
|
510
612
|
}
|
|
511
613
|
});
|
|
512
|
-
program.command("update <id>").description("Update a task").option(
|
|
614
|
+
program.command("update <id>").description("Update a task").option(
|
|
615
|
+
"--stage <stage>",
|
|
616
|
+
`Set lifecycle stage (valid: ${STAGES.join(", ")}; \`pushed\` is derived, not settable)`
|
|
617
|
+
).option("--description <text>", "Update description").option("--json", "JSON output").action((id, opts) => {
|
|
618
|
+
if (opts.stage && isDerivedStage(opts.stage)) {
|
|
619
|
+
console.error(derivedStageError(opts.stage));
|
|
620
|
+
process2.exit(1);
|
|
621
|
+
}
|
|
513
622
|
if (opts.stage && !isValidStage(opts.stage)) {
|
|
514
623
|
console.error(`Invalid stage: ${opts.stage}
|
|
515
624
|
Valid stages: ${STAGES.join(", ")}`);
|
|
516
625
|
process2.exit(1);
|
|
517
626
|
}
|
|
627
|
+
const storage = getStorage();
|
|
518
628
|
const updates = {};
|
|
519
629
|
if (opts.stage) updates.stage = opts.stage;
|
|
520
630
|
if (opts.description) updates.description = opts.description;
|
|
521
|
-
const task = updateTask(
|
|
631
|
+
const task = updateTask(storage, id, updates);
|
|
522
632
|
if (!task) {
|
|
523
633
|
console.error(`Task not found: ${id}`);
|
|
524
634
|
process2.exit(1);
|
|
@@ -555,10 +665,20 @@ program.command("check").description("Show active tasks and git status for this
|
|
|
555
665
|
const repoRoot = dirname3(storage);
|
|
556
666
|
const activeTasks = listTasks(storage, { all: false });
|
|
557
667
|
const repoStatus = getRepoStatus(repoRoot);
|
|
668
|
+
const getEffective = (t) => deriveEffectiveStage(t, repoRoot);
|
|
558
669
|
if (opts.json) {
|
|
559
|
-
console.log(
|
|
670
|
+
console.log(
|
|
671
|
+
JSON.stringify(
|
|
672
|
+
{
|
|
673
|
+
activeTasks: activeTasks.map((t) => ({ ...t, effectiveStage: getEffective(t) })),
|
|
674
|
+
repoStatus
|
|
675
|
+
},
|
|
676
|
+
null,
|
|
677
|
+
2
|
|
678
|
+
)
|
|
679
|
+
);
|
|
560
680
|
} else {
|
|
561
|
-
console.log(formatCheckReport(activeTasks, repoStatus));
|
|
681
|
+
console.log(formatCheckReport(activeTasks, repoStatus, getEffective));
|
|
562
682
|
}
|
|
563
683
|
});
|
|
564
684
|
program.command("purge").description("Remove all done tasks from storage").option("--dry-run", "Show what would be removed without removing").option("--keep <n>", "Keep N most recent done tasks, purge the rest").option("--json", "JSON output").action((opts) => {
|
|
@@ -586,4 +706,4 @@ program.command("gui [dir]").description("Start the web GUI (defaults to current
|
|
|
586
706
|
const port = parseInt(opts.port, 10) || 3333;
|
|
587
707
|
startGui(targetDir, port);
|
|
588
708
|
});
|
|
589
|
-
program.parse(process2.argv);
|
|
709
|
+
program.parse(normalizeHyphenatedTaskIdArg(process2.argv));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metyatech/task-tracker",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Persistent task lifecycle tracker for AI agent sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"vitest": "^4.0.0"
|
|
54
54
|
},
|
|
55
55
|
"overrides": {
|
|
56
|
-
"minimatch": "
|
|
56
|
+
"minimatch": "10.2.4"
|
|
57
57
|
},
|
|
58
58
|
"lint-staged": {
|
|
59
59
|
"src/**/*.ts": [
|