@nickmeriano/task 0.7.0 → 0.8.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/README.md +12 -7
- package/dist/check.d.ts +42 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +364 -0
- package/dist/check.js.map +1 -0
- package/dist/check.test.d.ts +9 -0
- package/dist/check.test.d.ts.map +1 -0
- package/dist/check.test.js +209 -0
- package/dist/check.test.js.map +1 -0
- package/dist/claim.d.ts +56 -3
- package/dist/claim.d.ts.map +1 -1
- package/dist/claim.js +164 -9
- package/dist/claim.js.map +1 -1
- package/dist/claim.test.js +168 -14
- package/dist/claim.test.js.map +1 -1
- package/dist/cli.js +504 -93
- package/dist/cli.js.map +1 -1
- package/dist/file-store.d.ts +41 -15
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +197 -99
- package/dist/file-store.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/overview.d.ts +43 -0
- package/dist/overview.d.ts.map +1 -0
- package/dist/overview.js +53 -0
- package/dist/overview.js.map +1 -0
- package/dist/overview.test.d.ts +8 -0
- package/dist/overview.test.d.ts.map +1 -0
- package/dist/overview.test.js +47 -0
- package/dist/overview.test.js.map +1 -0
- package/dist/promote.test.d.ts +14 -0
- package/dist/promote.test.d.ts.map +1 -0
- package/dist/promote.test.js +106 -0
- package/dist/promote.test.js.map +1 -0
- package/dist/search.d.ts +32 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +66 -0
- package/dist/search.js.map +1 -0
- package/dist/search.test.d.ts +2 -0
- package/dist/search.test.d.ts.map +1 -0
- package/dist/search.test.js +53 -0
- package/dist/search.test.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +10 -2
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +16 -50
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +0 -368
- package/dist/store.js.map +1 -1
- package/dist/store.test.d.ts +1 -2
- package/dist/store.test.d.ts.map +1 -1
- package/dist/store.test.js +77 -48
- package/dist/store.test.js.map +1 -1
- package/dist/ticket-doc.d.ts +55 -2
- package/dist/ticket-doc.d.ts.map +1 -1
- package/dist/ticket-doc.js +177 -7
- package/dist/ticket-doc.js.map +1 -1
- package/dist/types.d.ts +67 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +97 -31
- package/src/check.test.ts +271 -0
- package/src/check.ts +454 -0
- package/src/claim.test.ts +202 -14
- package/src/claim.ts +194 -10
- package/src/cli.ts +510 -89
- package/src/file-store.ts +217 -111
- package/src/index.ts +4 -2
- package/src/overview.test.ts +51 -0
- package/src/overview.ts +90 -0
- package/src/promote.test.ts +131 -0
- package/src/search.test.ts +64 -0
- package/src/search.ts +91 -0
- package/src/server.ts +11 -2
- package/src/store.test.ts +89 -57
- package/src/store.ts +18 -431
- package/src/ticket-doc.ts +210 -10
- package/src/types.ts +71 -14
- package/ui/dist/assets/{index-oJzomUDL.js → index-BJmOsOdR.js} +76 -76
- package/ui/dist/assets/index-BoqQlqSU.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-CXW8uT5f.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,32 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// The `task` CLI — tasks that live in your repo. Zero dependencies: hand-rolled
|
|
3
|
-
// flag parsing, plain-text tickets in .task/tickets/ for storage
|
|
4
|
-
//
|
|
5
|
-
// agents).
|
|
3
|
+
// flag parsing, plain-text tickets in .task/tickets/ for storage, plain text
|
|
4
|
+
// out (--json for agents).
|
|
6
5
|
//
|
|
7
6
|
// task init
|
|
8
|
-
// task add "Wire up webhooks" --tags api,infra --
|
|
7
|
+
// task add "Wire up webhooks" --tags api,infra --goal launch
|
|
9
8
|
// task list --status todo,in_progress
|
|
10
|
-
// task
|
|
9
|
+
// task move TAS-3 done
|
|
11
10
|
// task serve
|
|
12
11
|
import { spawn } from "node:child_process";
|
|
13
12
|
import { readFileSync } from "node:fs";
|
|
14
13
|
import { basename, join } from "node:path";
|
|
15
14
|
import process from "node:process";
|
|
16
15
|
import { resolveAuthor } from "./author.js";
|
|
17
|
-
import {
|
|
16
|
+
import { checkBoards, checkRoot } from "./check.js";
|
|
17
|
+
import { ClaimError, claim, claimNext, claimableTasks, promote, release, selectionBoards, } from "./claim.js";
|
|
18
|
+
import { STALE_DAYS, buildOverview } from "./overview.js";
|
|
19
|
+
import { searchStore } from "./search.js";
|
|
18
20
|
import { detectRepo, parseSlug, publish, resolveHost } from "./publish.js";
|
|
19
21
|
import { createTaskServer } from "./server.js";
|
|
20
|
-
import { initProject,
|
|
22
|
+
import { initProject, openBoard } from "./file-store.js";
|
|
21
23
|
import { CONFIG_FILE, TASK_DIR, boardConfig, findBoards, findBoardsByPrefix, findRoot, findScopeRoot, } from "./store.js";
|
|
22
24
|
import { STATUSES, isStatus } from "./types.js";
|
|
23
|
-
// node:sqlite still emits an ExperimentalWarning on Node 22 — noise in a CLI
|
|
24
|
-
// that runs it on every invocation. Filter that one warning, keep the rest.
|
|
25
|
-
process.removeAllListeners("warning");
|
|
26
|
-
process.on("warning", (warning) => {
|
|
27
|
-
if (warning.name !== "ExperimentalWarning")
|
|
28
|
-
console.error(warning);
|
|
29
|
-
});
|
|
30
25
|
/**
|
|
31
26
|
* Read from package.json rather than duplicated as a literal. Releases here are
|
|
32
27
|
* cut by changesets, which bumps package.json and nothing else — so a hardcoded
|
|
@@ -60,7 +55,16 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
60
55
|
"public",
|
|
61
56
|
"private",
|
|
62
57
|
"no-wait",
|
|
58
|
+
"fix",
|
|
59
|
+
"next",
|
|
60
|
+
"force",
|
|
63
61
|
]);
|
|
62
|
+
/**
|
|
63
|
+
* Value flags that may repeat — `--board NIC --board TAS` accumulates in
|
|
64
|
+
* given order, equivalent to `--board NIC,TAS`. Everything else keeps
|
|
65
|
+
* last-one-wins.
|
|
66
|
+
*/
|
|
67
|
+
const REPEATABLE_FLAGS = new Set(["board"]);
|
|
64
68
|
function parseArgs(argv) {
|
|
65
69
|
const positional = [];
|
|
66
70
|
const flags = {};
|
|
@@ -68,15 +72,20 @@ function parseArgs(argv) {
|
|
|
68
72
|
const arg = argv[i];
|
|
69
73
|
if (arg.startsWith("--")) {
|
|
70
74
|
const name = arg.slice(2);
|
|
75
|
+
const set = (key, value) => {
|
|
76
|
+
const prior = flags[key];
|
|
77
|
+
flags[key] =
|
|
78
|
+
REPEATABLE_FLAGS.has(key) && typeof prior === "string" ? `${prior},${value}` : value;
|
|
79
|
+
};
|
|
71
80
|
const eq = name.indexOf("=");
|
|
72
81
|
if (eq >= 0) {
|
|
73
|
-
|
|
82
|
+
set(name.slice(0, eq), name.slice(eq + 1));
|
|
74
83
|
}
|
|
75
84
|
else if (BOOLEAN_FLAGS.has(name) || i + 1 >= argv.length || argv[i + 1].startsWith("--")) {
|
|
76
85
|
flags[name] = true;
|
|
77
86
|
}
|
|
78
87
|
else {
|
|
79
|
-
|
|
88
|
+
set(name, argv[++i]);
|
|
80
89
|
}
|
|
81
90
|
}
|
|
82
91
|
else {
|
|
@@ -170,17 +179,32 @@ function patchFromFlags(flags) {
|
|
|
170
179
|
if (title !== undefined)
|
|
171
180
|
patch.title = title;
|
|
172
181
|
const description = str(flags, "description") ?? str(flags, "desc");
|
|
182
|
+
// Long markdown doesn't survive shell quoting — `--description-file` keeps
|
|
183
|
+
// agents writing plans and specs on the CLI path instead of hand-editing
|
|
184
|
+
// ticket files. `-` reads stdin.
|
|
185
|
+
const descriptionFile = str(flags, "description-file");
|
|
186
|
+
if (description !== undefined && descriptionFile !== undefined) {
|
|
187
|
+
fail("pass --description or --description-file, not both");
|
|
188
|
+
}
|
|
173
189
|
if (description !== undefined)
|
|
174
190
|
patch.description = description;
|
|
191
|
+
if (descriptionFile !== undefined) {
|
|
192
|
+
try {
|
|
193
|
+
patch.description = readFileSync(descriptionFile === "-" ? 0 : descriptionFile, "utf8");
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
fail(`--description-file: can't read ${descriptionFile === "-" ? "stdin" : descriptionFile}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
175
199
|
const status = str(flags, "status");
|
|
176
200
|
if (status !== undefined)
|
|
177
201
|
patch.status = parseStatus(status);
|
|
178
202
|
const tags = str(flags, "tags") ?? str(flags, "tag");
|
|
179
203
|
if (tags !== undefined)
|
|
180
204
|
patch.tags = parseTags(tags);
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
patch.
|
|
205
|
+
const goal = str(flags, "goal");
|
|
206
|
+
if (goal !== undefined)
|
|
207
|
+
patch.goal = goal.trim() || null;
|
|
184
208
|
if (flags["needs-human"])
|
|
185
209
|
patch.needsHuman = true;
|
|
186
210
|
if (flags["no-needs-human"])
|
|
@@ -216,7 +240,7 @@ function taskRow(t) {
|
|
|
216
240
|
t.needsHuman ? "⚑" : "",
|
|
217
241
|
t.title,
|
|
218
242
|
t.tags.join(","),
|
|
219
|
-
t.
|
|
243
|
+
t.goal ?? "",
|
|
220
244
|
];
|
|
221
245
|
}
|
|
222
246
|
function printTask(t) {
|
|
@@ -244,7 +268,7 @@ function cmdInit(args) {
|
|
|
244
268
|
function cmdAdd(args) {
|
|
245
269
|
const title = args.positional.join(" ").trim();
|
|
246
270
|
if (!title)
|
|
247
|
-
fail(`usage: task add <title> [--description …] [--status …] [--tags a,b] [--
|
|
271
|
+
fail(`usage: task add <title> [--description …] [--status …] [--tags a,b] [--goal <slug>] [--needs-human]`);
|
|
248
272
|
const store = openStore();
|
|
249
273
|
const patch = patchFromFlags(args.flags);
|
|
250
274
|
const pr = str(args.flags, "pr");
|
|
@@ -258,8 +282,17 @@ function cmdAdd(args) {
|
|
|
258
282
|
printTask(task);
|
|
259
283
|
}
|
|
260
284
|
}
|
|
285
|
+
/** `--board NIC --board TAS` (or `--board NIC,TAS`) → ["NIC", "TAS"]. */
|
|
286
|
+
function boardFlag(args) {
|
|
287
|
+
const raw = str(args.flags, "board");
|
|
288
|
+
if (raw === undefined)
|
|
289
|
+
return undefined;
|
|
290
|
+
const prefixes = raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
291
|
+
if (prefixes.length === 0)
|
|
292
|
+
fail("--board needs a prefix, e.g. --board TAS");
|
|
293
|
+
return prefixes;
|
|
294
|
+
}
|
|
261
295
|
function cmdList(args) {
|
|
262
|
-
const store = openStore();
|
|
263
296
|
if (args.flags.claimable) {
|
|
264
297
|
// The dispatcher's queue view (TAS-21): claimable already pins the status
|
|
265
298
|
// set and consults origin, so the board-shaping flags don't compose.
|
|
@@ -267,7 +300,17 @@ function cmdList(args) {
|
|
|
267
300
|
if (args.flags[flag] !== undefined)
|
|
268
301
|
fail(`--claimable can't be combined with --${flag}`);
|
|
269
302
|
}
|
|
270
|
-
|
|
303
|
+
// Selection scope, same resolution as `claim --next`: --board flag > root
|
|
304
|
+
// board's `boards` list > nearest board. Board order is priority order.
|
|
305
|
+
let tasks;
|
|
306
|
+
try {
|
|
307
|
+
tasks = selectionBoards(process.cwd(), boardFlag(args)).flatMap((s) => claimableTasks(s));
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
if (error instanceof ClaimError)
|
|
311
|
+
fail(error.message);
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
271
314
|
if (args.flags.json) {
|
|
272
315
|
console.log(JSON.stringify({ tasks }, null, 2));
|
|
273
316
|
}
|
|
@@ -279,6 +322,9 @@ function cmdList(args) {
|
|
|
279
322
|
}
|
|
280
323
|
return;
|
|
281
324
|
}
|
|
325
|
+
if (args.flags.board !== undefined)
|
|
326
|
+
fail("--board only applies to list --claimable");
|
|
327
|
+
const store = openStore();
|
|
282
328
|
const archived = Boolean(args.flags.archived);
|
|
283
329
|
const statusFlag = str(args.flags, "status");
|
|
284
330
|
const statuses = statusFlag
|
|
@@ -292,7 +338,7 @@ function cmdList(args) {
|
|
|
292
338
|
const tasks = store.list({
|
|
293
339
|
statuses,
|
|
294
340
|
tags: tagFlag ? parseTags(tagFlag) : undefined,
|
|
295
|
-
|
|
341
|
+
goal: str(args.flags, "goal"),
|
|
296
342
|
needsHuman: args.flags["needs-human"] ? true : undefined,
|
|
297
343
|
archived: archived || undefined,
|
|
298
344
|
});
|
|
@@ -313,6 +359,43 @@ function cmdList(args) {
|
|
|
313
359
|
table(tasks.map(taskRow));
|
|
314
360
|
}
|
|
315
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* `task search <query>` — find tickets by what they say. Case-insensitive
|
|
364
|
+
* substring over titles, descriptions and comments, scoped by the usual list
|
|
365
|
+
* filters; `--archived` searches the archive *in addition to* the board (a
|
|
366
|
+
* search is a question about everything ever written down, unlike
|
|
367
|
+
* `list --archived`, which is a view of one place).
|
|
368
|
+
*/
|
|
369
|
+
function cmdSearch(args) {
|
|
370
|
+
const query = args.positional.join(" ").trim();
|
|
371
|
+
if (!query) {
|
|
372
|
+
fail("usage: task search <query> [--status <s1,s2>] [--tag <a,b>] [--goal <slug>] [--needs-human] [--archived]");
|
|
373
|
+
}
|
|
374
|
+
const store = openStore();
|
|
375
|
+
const statusFlag = str(args.flags, "status");
|
|
376
|
+
const tagFlag = str(args.flags, "tags") ?? str(args.flags, "tag");
|
|
377
|
+
const results = searchStore(store, query, {
|
|
378
|
+
statuses: statusFlag ? statusFlag.split(",").map(parseStatus) : undefined,
|
|
379
|
+
tags: tagFlag ? parseTags(tagFlag) : undefined,
|
|
380
|
+
goal: str(args.flags, "goal"),
|
|
381
|
+
needsHuman: args.flags["needs-human"] ? true : undefined,
|
|
382
|
+
}, Boolean(args.flags.archived));
|
|
383
|
+
if (args.flags.json) {
|
|
384
|
+
console.log(JSON.stringify({ query, results }, null, 2));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (results.length === 0) {
|
|
388
|
+
console.log(`no matches for "${query}"${args.flags.archived ? "" : " (--archived searches the archive too)"}`);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
for (const { task, matches } of results) {
|
|
392
|
+
console.log(`${task.id} ${STATUS_GLYPH[task.status]} ${task.status}${task.archived ? " (archived)" : ""} ${task.title}`);
|
|
393
|
+
for (const match of matches) {
|
|
394
|
+
const label = match.field === "comment" ? `comment${match.author ? ` by ${match.author}` : ""}` : match.field;
|
|
395
|
+
console.log(` ${label}: ${match.snippet}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
316
399
|
/**
|
|
317
400
|
* `task boards` — every board in this repo, found the way `task serve` finds
|
|
318
401
|
* them but anchored at the *outermost* board root, so it answers from anywhere
|
|
@@ -412,8 +495,12 @@ function cmdShow(args) {
|
|
|
412
495
|
if (!task)
|
|
413
496
|
fail(`no such task: ${store.displayId(number)}`);
|
|
414
497
|
const comments = store.comments(number);
|
|
498
|
+
// The goal rides along in full — title *and* description — so an agent
|
|
499
|
+
// picking the ticket up cold inherits the shared context without it being
|
|
500
|
+
// pasted into every ticket. null when unset or when the ref dangles.
|
|
501
|
+
const goal = task.goal ? store.getGoal(task.goal) : null;
|
|
415
502
|
if (args.flags.json) {
|
|
416
|
-
console.log(JSON.stringify({ task, comments }, null, 2));
|
|
503
|
+
console.log(JSON.stringify({ task, comments, goal }, null, 2));
|
|
417
504
|
return;
|
|
418
505
|
}
|
|
419
506
|
// A linked task is only as useful as knowing whether it's still in the way.
|
|
@@ -431,8 +518,9 @@ function cmdShow(args) {
|
|
|
431
518
|
console.log(`needs a human`);
|
|
432
519
|
if (task.tags.length)
|
|
433
520
|
console.log(`tags ${task.tags.join(", ")}`);
|
|
434
|
-
if (task.
|
|
435
|
-
console.log(`
|
|
521
|
+
if (task.goal) {
|
|
522
|
+
console.log(`goal ${task.goal}${goal ? ` — ${goal.title}${goal.archived ? " (archived)" : ""}` : ""}`);
|
|
523
|
+
}
|
|
436
524
|
if (task.blockedBy.length)
|
|
437
525
|
console.log(`blocked by ${describeLinks(task.blockedBy)}`);
|
|
438
526
|
if (task.blocks.length)
|
|
@@ -443,6 +531,10 @@ function cmdShow(args) {
|
|
|
443
531
|
console.log(`updated ${task.updatedAt}`);
|
|
444
532
|
if (task.description)
|
|
445
533
|
console.log(`\n${task.description}`);
|
|
534
|
+
if (goal?.description) {
|
|
535
|
+
console.log(`\n── goal: ${goal.title} (${goal.slug})`);
|
|
536
|
+
console.log(goal.description.split("\n").map((line) => ` ${line}`).join("\n"));
|
|
537
|
+
}
|
|
446
538
|
if (comments.length) {
|
|
447
539
|
console.log(``);
|
|
448
540
|
for (const c of comments) {
|
|
@@ -451,15 +543,13 @@ function cmdShow(args) {
|
|
|
451
543
|
}
|
|
452
544
|
}
|
|
453
545
|
}
|
|
454
|
-
function cmdUpdate(args
|
|
546
|
+
function cmdUpdate(args) {
|
|
455
547
|
const ref = args.positional[0];
|
|
456
548
|
if (!ref)
|
|
457
549
|
fail("usage: task update <id> [--status …] [--title …] …");
|
|
458
550
|
const store = openStoreFor(ref);
|
|
459
551
|
const number = store.parseId(ref);
|
|
460
552
|
const patch = patchFromFlags(args.flags);
|
|
461
|
-
if (forcedStatus)
|
|
462
|
-
patch.status = forcedStatus;
|
|
463
553
|
const pr = str(args.flags, "pr");
|
|
464
554
|
if (pr) {
|
|
465
555
|
// Append, dedup — `--pr <url>` is "attach this PR", not "replace the list".
|
|
@@ -482,6 +572,36 @@ function cmdMove(args) {
|
|
|
482
572
|
fail("usage: task move <id> <status>");
|
|
483
573
|
cmdUpdate({ positional: [ref], flags: { ...args.flags, status } });
|
|
484
574
|
}
|
|
575
|
+
/**
|
|
576
|
+
* `task promote <id>` — backlog → todo, the human "build this" call; see
|
|
577
|
+
* claim.ts for why blocked / needs-human are holds (reported), not refusals.
|
|
578
|
+
* Exit codes follow claim's contract: 0 promoted, 2 refused (fix something).
|
|
579
|
+
*/
|
|
580
|
+
function cmdPromote(args) {
|
|
581
|
+
const ref = args.positional[0];
|
|
582
|
+
if (!ref)
|
|
583
|
+
fail("usage: task promote <id>");
|
|
584
|
+
const store = openStoreFor(ref);
|
|
585
|
+
try {
|
|
586
|
+
const result = promote(store, store.parseId(ref));
|
|
587
|
+
if (args.flags.json) {
|
|
588
|
+
console.log(JSON.stringify({ task: result.task, holds: result.holds }, null, 2));
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
printTask(result.task);
|
|
592
|
+
for (const hold of result.holds) {
|
|
593
|
+
console.log(`note: ${hold} — in todo, but not claimable until that clears`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
if (error instanceof ClaimError) {
|
|
599
|
+
console.error(`error: ${error.message}`);
|
|
600
|
+
process.exit(2);
|
|
601
|
+
}
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
485
605
|
function cmdComment(args) {
|
|
486
606
|
const [ref, ...rest] = args.positional;
|
|
487
607
|
const body = rest.join(" ").trim();
|
|
@@ -525,39 +645,80 @@ function cmdLink(args, action) {
|
|
|
525
645
|
console.log(`${subject} ${verb} blocks ${object}`);
|
|
526
646
|
}
|
|
527
647
|
}
|
|
648
|
+
const CLAIM_USAGE = 'usage: task claim <id> | task claim --next | task claim --release <id> --comment "<why>"';
|
|
528
649
|
/**
|
|
529
|
-
* `task claim <id>` / `task claim --release <id>` — see
|
|
530
|
-
* mechanics. Exit codes are the contract callers script
|
|
531
|
-
* 1 already claimed
|
|
650
|
+
* `task claim <id>` / `task claim --next` / `task claim --release <id>` — see
|
|
651
|
+
* claim.ts for the mechanics. Exit codes are the contract callers script
|
|
652
|
+
* against: 0 claimed, 1 already claimed / queue empty (nothing to do here —
|
|
653
|
+
* a scheduled worker stops cleanly), 2 preconditions failed (fix something).
|
|
532
654
|
*/
|
|
533
655
|
function cmdClaim(args) {
|
|
534
656
|
const ref = args.positional[0];
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
657
|
+
const options = { force: Boolean(args.flags.force) };
|
|
658
|
+
const printClaim = (result) => {
|
|
659
|
+
if (args.flags.json) {
|
|
660
|
+
console.log(JSON.stringify({ task: result.task, branch: result.branch }, null, 2));
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
console.log(`claimed ${result.task.id} — on ${result.branch} (from ${result.base}), status in_progress`);
|
|
664
|
+
}
|
|
665
|
+
};
|
|
539
666
|
try {
|
|
667
|
+
if (args.flags.next) {
|
|
668
|
+
// The first line of every scheduled worker: claim the top claimable
|
|
669
|
+
// ticket across the selection scope (--board > root `boards` > nearest),
|
|
670
|
+
// retrying past lost races internally.
|
|
671
|
+
if (ref)
|
|
672
|
+
fail("pass an id or --next, not both");
|
|
673
|
+
if (args.flags.release)
|
|
674
|
+
fail(CLAIM_USAGE);
|
|
675
|
+
const result = claimNext(selectionBoards(process.cwd(), boardFlag(args)), options);
|
|
676
|
+
if (!result) {
|
|
677
|
+
if (args.flags.json) {
|
|
678
|
+
console.log(JSON.stringify({ task: null }, null, 2));
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
console.log("nothing claimable — no unblocked, unclaimed todo tickets in scope");
|
|
682
|
+
}
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
printClaim(result);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (!ref)
|
|
689
|
+
fail(CLAIM_USAGE);
|
|
690
|
+
const store = openStoreFor(ref);
|
|
691
|
+
const number = store.parseId(ref);
|
|
540
692
|
if (args.flags.release) {
|
|
693
|
+
// A released claim must leave its failure context behind: the branch
|
|
694
|
+
// (and whatever was tried on it) is about to evaporate, so the reason
|
|
695
|
+
// is required, and lands as an ordinary attributed comment.
|
|
696
|
+
const why = str(args.flags, "comment")?.trim();
|
|
697
|
+
if (!why) {
|
|
698
|
+
fail('task claim --release requires --comment "<why>" — the next worker inherits what was tried');
|
|
699
|
+
}
|
|
541
700
|
const result = release(store, number);
|
|
701
|
+
const commented = result.remote || result.local;
|
|
702
|
+
// After the branch deletion, so the comment survives it: release leaves
|
|
703
|
+
// the checkout on the default branch, and the comment file sits there
|
|
704
|
+
// uncommitted, to ride along with the next commit.
|
|
705
|
+
const comment = commented
|
|
706
|
+
? store.addComment(number, why, resolveAuthor(str(args.flags, "author")).name)
|
|
707
|
+
: null;
|
|
542
708
|
if (args.flags.json) {
|
|
543
|
-
console.log(JSON.stringify({ released: result }, null, 2));
|
|
709
|
+
console.log(JSON.stringify({ released: result, comment }, null, 2));
|
|
544
710
|
}
|
|
545
|
-
else if (!
|
|
711
|
+
else if (!commented) {
|
|
546
712
|
console.log(`${store.displayId(number)} wasn't claimed — no ${result.branch} to delete`);
|
|
547
713
|
}
|
|
548
714
|
else {
|
|
549
715
|
const where = [result.remote && "origin", result.local && "local"].filter(Boolean);
|
|
550
716
|
console.log(`released ${store.displayId(number)} — deleted ${result.branch} (${where.join(" and ")})`);
|
|
717
|
+
console.log(`left the reason as a comment on ${store.displayId(number)} — uncommitted, commit it with your next change`);
|
|
551
718
|
}
|
|
552
719
|
return;
|
|
553
720
|
}
|
|
554
|
-
|
|
555
|
-
if (args.flags.json) {
|
|
556
|
-
console.log(JSON.stringify({ task: result.task, branch: result.branch }, null, 2));
|
|
557
|
-
}
|
|
558
|
-
else {
|
|
559
|
-
console.log(`claimed ${result.task.id} — on ${result.branch} (from ${result.base}), status in_progress`);
|
|
560
|
-
}
|
|
721
|
+
printClaim(claim(store, number, options));
|
|
561
722
|
}
|
|
562
723
|
catch (error) {
|
|
563
724
|
if (error instanceof ClaimError) {
|
|
@@ -567,6 +728,164 @@ function cmdClaim(args) {
|
|
|
567
728
|
throw error;
|
|
568
729
|
}
|
|
569
730
|
}
|
|
731
|
+
/** `task instructions` — the shipped agent conventions, self-served at runtime. */
|
|
732
|
+
function cmdInstructions() {
|
|
733
|
+
// skill/ ships in the npm package; `../` from both dist/cli.js and
|
|
734
|
+
// src/cli.ts is the package root, same trick VERSION uses.
|
|
735
|
+
process.stdout.write(readFileSync(new URL("../skill/SKILL.md", import.meta.url), "utf8"));
|
|
736
|
+
}
|
|
737
|
+
/** `task overview [--json]` — one screen of board health. See overview.ts. */
|
|
738
|
+
function cmdOverview(args) {
|
|
739
|
+
const overview = buildOverview(openStore());
|
|
740
|
+
if (args.flags.json) {
|
|
741
|
+
console.log(JSON.stringify({ overview }, null, 2));
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
console.log(`${overview.board} (${overview.prefix}) — ${overview.open} open`);
|
|
745
|
+
console.log(` ${STATUSES.map((s) => `${STATUS_GLYPH[s]} ${s} ${overview.statuses[s]}`).join(" ")}`);
|
|
746
|
+
const claimed = overview.claimed === null
|
|
747
|
+
? "claimed ? (origin unreachable)"
|
|
748
|
+
: `claimed ${overview.claimed.length}${overview.claimed.length ? ` (${overview.claimed.join(", ")})` : ""}`;
|
|
749
|
+
console.log(` ${claimed} needs-human ${overview.needsHuman}`);
|
|
750
|
+
if (overview.goals.length) {
|
|
751
|
+
console.log("goals");
|
|
752
|
+
table(overview.goals.map((g) => [
|
|
753
|
+
` ${g.slug}`,
|
|
754
|
+
g.title,
|
|
755
|
+
`${g.done}/${g.total} done${g.total ? ` (${Math.round((g.done / g.total) * 100)}%)` : ""}`,
|
|
756
|
+
]));
|
|
757
|
+
}
|
|
758
|
+
if (overview.stale.length) {
|
|
759
|
+
console.log(`stale (untouched ${STALE_DAYS}+ days)`);
|
|
760
|
+
table(overview.stale.map((t) => [` ${t.id}`, t.title, t.updatedAt.slice(0, 10)]));
|
|
761
|
+
}
|
|
762
|
+
if (overview.oldestOpen) {
|
|
763
|
+
console.log(`oldest open ${overview.oldestOpen.id} ${overview.oldestOpen.title} (created ${overview.oldestOpen.createdAt.slice(0, 10)})`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* `task goal …` — the goals a board's tasks belong to. A goal is a titled,
|
|
768
|
+
* described destination (`.task/goals/<slug>.md`); a task carries at most one.
|
|
769
|
+
* There is no goal status anywhere here on purpose: progress is derived from
|
|
770
|
+
* the tasks, and `archive` is the only lifecycle a goal has.
|
|
771
|
+
*/
|
|
772
|
+
function cmdGoal(args) {
|
|
773
|
+
const [sub, ...rest] = args.positional;
|
|
774
|
+
const usage = "usage: task goal add <title> [--slug <s>] [--description …] | list [--archived] | " +
|
|
775
|
+
"show <slug> | update <slug> [--title …] [--description …] | archive <slug> | " +
|
|
776
|
+
"unarchive <slug> | delete <slug>";
|
|
777
|
+
if (!sub)
|
|
778
|
+
fail(usage);
|
|
779
|
+
const store = openStore();
|
|
780
|
+
const slugArg = () => {
|
|
781
|
+
const slug = rest[0];
|
|
782
|
+
if (!slug)
|
|
783
|
+
fail(`usage: task goal ${sub} <slug>`);
|
|
784
|
+
return slug;
|
|
785
|
+
};
|
|
786
|
+
const printGoal = (goal, verb) => console.log(`${verb} ${goal.slug} ${goal.title}${goal.archived ? " (archived)" : ""}`);
|
|
787
|
+
switch (sub) {
|
|
788
|
+
case "add": {
|
|
789
|
+
const title = rest.join(" ").trim();
|
|
790
|
+
if (!title)
|
|
791
|
+
fail("usage: task goal add <title> [--slug <s>] [--description …]");
|
|
792
|
+
const goal = store.createGoal({
|
|
793
|
+
title,
|
|
794
|
+
slug: str(args.flags, "slug"),
|
|
795
|
+
description: str(args.flags, "description") ?? str(args.flags, "desc"),
|
|
796
|
+
});
|
|
797
|
+
if (args.flags.json)
|
|
798
|
+
console.log(JSON.stringify({ goal }, null, 2));
|
|
799
|
+
else
|
|
800
|
+
printGoal(goal, "created");
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
case "list": {
|
|
804
|
+
const archived = Boolean(args.flags.archived);
|
|
805
|
+
const goals = store.goals(archived);
|
|
806
|
+
// Progress is derived here, at read time — a goal file never stores it.
|
|
807
|
+
const tasks = store.list({ statuses: undefined });
|
|
808
|
+
const rows = goals.map((goal) => {
|
|
809
|
+
const mine = tasks.filter((t) => t.goal === goal.slug);
|
|
810
|
+
const done = mine.filter((t) => t.status === "done" || t.status === "canceled").length;
|
|
811
|
+
return { ...goal, open: mine.length - done, done, total: mine.length };
|
|
812
|
+
});
|
|
813
|
+
if (args.flags.json) {
|
|
814
|
+
console.log(JSON.stringify({ goals: rows }, null, 2));
|
|
815
|
+
}
|
|
816
|
+
else if (rows.length === 0) {
|
|
817
|
+
console.log(archived ? "no archived goals" : "no goals — create one with `task goal add`");
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
table(rows.map((g) => [g.slug, g.title, `${g.done}/${g.total} done`]));
|
|
821
|
+
}
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
case "show": {
|
|
825
|
+
const slug = slugArg();
|
|
826
|
+
const goal = store.getGoal(slug);
|
|
827
|
+
if (!goal)
|
|
828
|
+
fail(`no such goal: "${slug}"`);
|
|
829
|
+
const tasks = store.list({ statuses: undefined, goal: slug });
|
|
830
|
+
if (args.flags.json) {
|
|
831
|
+
console.log(JSON.stringify({ goal, tasks }, null, 2));
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
console.log(`${goal.slug} ${goal.title}`);
|
|
835
|
+
if (goal.archived)
|
|
836
|
+
console.log(`archived yes — \`task goal unarchive ${goal.slug}\` to edit`);
|
|
837
|
+
console.log(`created ${goal.createdAt}`);
|
|
838
|
+
console.log(`updated ${goal.updatedAt}`);
|
|
839
|
+
if (goal.description)
|
|
840
|
+
console.log(`\n${goal.description}`);
|
|
841
|
+
if (tasks.length) {
|
|
842
|
+
console.log("");
|
|
843
|
+
table(tasks.map(taskRow));
|
|
844
|
+
}
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
case "update": {
|
|
848
|
+
const patch = {
|
|
849
|
+
...(str(args.flags, "title") !== undefined && { title: str(args.flags, "title") }),
|
|
850
|
+
...((str(args.flags, "description") ?? str(args.flags, "desc")) !== undefined && {
|
|
851
|
+
description: str(args.flags, "description") ?? str(args.flags, "desc"),
|
|
852
|
+
}),
|
|
853
|
+
};
|
|
854
|
+
if (Object.keys(patch).length === 0)
|
|
855
|
+
fail("nothing to update — pass --title or --description");
|
|
856
|
+
const goal = store.updateGoal(slugArg(), patch);
|
|
857
|
+
if (args.flags.json)
|
|
858
|
+
console.log(JSON.stringify({ goal }, null, 2));
|
|
859
|
+
else
|
|
860
|
+
printGoal(goal, "updated");
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
case "archive": {
|
|
864
|
+
const goal = store.archiveGoal(slugArg());
|
|
865
|
+
if (args.flags.json)
|
|
866
|
+
console.log(JSON.stringify({ goal }, null, 2));
|
|
867
|
+
else
|
|
868
|
+
printGoal(goal, "archived");
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
case "unarchive": {
|
|
872
|
+
const goal = store.unarchiveGoal(slugArg());
|
|
873
|
+
if (args.flags.json)
|
|
874
|
+
console.log(JSON.stringify({ goal }, null, 2));
|
|
875
|
+
else
|
|
876
|
+
printGoal(goal, "unarchived");
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
case "delete": {
|
|
880
|
+
const slug = slugArg();
|
|
881
|
+
store.deleteGoal(slug);
|
|
882
|
+
console.log(`deleted goal ${slug}`);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
default:
|
|
886
|
+
fail(usage);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
570
889
|
const AUTHOR_SOURCE = {
|
|
571
890
|
flag: "--author",
|
|
572
891
|
env: "$TASK_AUTHOR",
|
|
@@ -584,26 +903,43 @@ function cmdWhoami(args) {
|
|
|
584
903
|
}
|
|
585
904
|
}
|
|
586
905
|
/**
|
|
587
|
-
* `task
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
906
|
+
* `task check [--fix]` — lint the board files. Git merges and hand-edits are
|
|
907
|
+
* write paths no CLI code sees, so validation is a command you can run (and
|
|
908
|
+
* put in pre-commit or CI, where merge-introduced drift gets caught). Covers
|
|
909
|
+
* every board at or below here, the same set `task serve` would serve. Exits
|
|
910
|
+
* non-zero while problems remain; `--fix` first applies the mechanical
|
|
911
|
+
* repairs — pruning dangling references, rewriting files into canonical form.
|
|
591
912
|
*/
|
|
592
|
-
function
|
|
593
|
-
const root =
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
913
|
+
function cmdCheck(args) {
|
|
914
|
+
const root = checkRoot(process.cwd());
|
|
915
|
+
const fix = Boolean(args.flags.fix);
|
|
916
|
+
const result = checkBoards(root, fix);
|
|
917
|
+
if (result.boards === 0) {
|
|
918
|
+
fail("no .task directory found in this directory, any parent, or below — run `task init` first");
|
|
919
|
+
}
|
|
920
|
+
// In fix mode the fixable issues were just repaired; what's left needs a human.
|
|
921
|
+
const remaining = fix ? result.issues.filter((issue) => !issue.fixable) : result.issues;
|
|
597
922
|
if (args.flags.json) {
|
|
598
|
-
console.log(JSON.stringify({
|
|
599
|
-
|
|
923
|
+
console.log(JSON.stringify({ ...result, remaining }, null, 2));
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
for (const file of result.fixed)
|
|
927
|
+
console.log(`fixed ${file}`);
|
|
928
|
+
for (const issue of remaining) {
|
|
929
|
+
console.log(`${issue.fixable ? "fixable" : "problem"} ${issue.file}`);
|
|
930
|
+
console.log(` ${issue.message}`);
|
|
931
|
+
}
|
|
932
|
+
const scope = `${result.files} file${result.files === 1 ? "" : "s"} across ${result.boards} board${result.boards === 1 ? "" : "s"}`;
|
|
933
|
+
if (remaining.length === 0) {
|
|
934
|
+
console.log(result.fixed.length ? `fixed ${result.fixed.length}, ${scope} clean` : `ok — ${scope}`);
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
const fixable = remaining.filter((issue) => issue.fixable).length;
|
|
938
|
+
console.log(`${remaining.length} problem${remaining.length === 1 ? "" : "s"} in ${scope}${fixable ? ` (${fixable} fixable — run \`task check --fix\`)` : ""}`);
|
|
939
|
+
}
|
|
600
940
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
console.log(``);
|
|
604
|
-
console.log(`Next:`);
|
|
605
|
-
console.log(` git rm --cached ${join(TASK_DIR, "tasks.db")} stop tracking the database`);
|
|
606
|
-
console.log(` git add ${TASK_DIR} commit the tickets`);
|
|
941
|
+
if (remaining.length > 0)
|
|
942
|
+
process.exitCode = 1;
|
|
607
943
|
}
|
|
608
944
|
function cmdDelete(args) {
|
|
609
945
|
const ref = args.positional[0];
|
|
@@ -790,42 +1126,102 @@ subdirectory (it walks up to find .task/, like git).
|
|
|
790
1126
|
|
|
791
1127
|
Usage
|
|
792
1128
|
task init [--name <name>] [--prefix <PREFIX>]
|
|
793
|
-
task add <title> [--description <text>
|
|
794
|
-
[--
|
|
795
|
-
|
|
1129
|
+
task add <title> [--description <text> | --description-file <path|->]
|
|
1130
|
+
[--status <s>] [--tags <a,b>] [--goal <slug>] [--needs-human]
|
|
1131
|
+
new tickets land in backlog (the dump list)
|
|
1132
|
+
unless --status says otherwise;
|
|
1133
|
+
--description-file reads markdown from a file
|
|
1134
|
+
(or stdin with -) — no shell-quoting fights
|
|
1135
|
+
task list [--status <s1,s2>] [--tag <a,b>] [--goal <slug>]
|
|
796
1136
|
[--needs-human] [--all] [--archived] [--claimable]
|
|
797
|
-
task
|
|
798
|
-
|
|
799
|
-
|
|
1137
|
+
task search <query> [--status <s1,s2>] [--tag <a,b>] [--goal <slug>]
|
|
1138
|
+
[--needs-human] [--archived]
|
|
1139
|
+
find tickets by what they say — case-insensitive
|
|
1140
|
+
match over titles, descriptions and comments,
|
|
1141
|
+
with a snippet of the matching line. --archived
|
|
1142
|
+
searches the archive too, on top of the board
|
|
1143
|
+
task show <id> the ticket, its comments — and its goal's title
|
|
1144
|
+
and description inline, so a cold pickup gets
|
|
1145
|
+
the shared context without chasing it
|
|
1146
|
+
task update <id> [--title <t>] [--description <text> | --description-file <path|->]
|
|
1147
|
+
[--status <s>] [--tags <a,b>] [--goal <slug>]
|
|
800
1148
|
[--needs-human | --no-needs-human]
|
|
801
1149
|
[--pr <url>] [--prs <url1,url2>]
|
|
802
|
-
task move <id> <status>
|
|
803
|
-
|
|
804
|
-
|
|
1150
|
+
task move <id> <status> change a ticket's status — the one general
|
|
1151
|
+
status API (backlog todo in_progress done
|
|
1152
|
+
canceled). The named verbs below exist only
|
|
1153
|
+
where a transition does more than write the
|
|
1154
|
+
status field
|
|
1155
|
+
task promote <id> backlog → todo, the human "build this" call.
|
|
1156
|
+
No quality gate — planning happens at claim
|
|
1157
|
+
time. Blocked / needs-human tickets still
|
|
1158
|
+
promote — those are claim-time gates, reported
|
|
1159
|
+
as a note — so promoting can queue work behind
|
|
1160
|
+
its blockers. Exit codes: 0 promoted, 2
|
|
1161
|
+
refused (not a backlog ticket)
|
|
805
1162
|
task link <id> --blocked-by <id>
|
|
806
1163
|
task link <id> --blocks <id> mark a dependency — one relation, visible from
|
|
807
1164
|
both tasks (A blocked by B ⇔ B blocks A)
|
|
808
1165
|
task unlink <id> (--blocks <id> | --blocked-by <id>)
|
|
809
|
-
task claim <id>
|
|
810
|
-
<
|
|
811
|
-
branch, ticket → in_progress as its
|
|
812
|
-
commit, pushed. The namespace defaults
|
|
813
|
-
task/claim/ — set
|
|
1166
|
+
task claim <id> [--force] claim a ticket before working it: branch
|
|
1167
|
+
<branchPrefix><prefix>-<n> off origin's
|
|
1168
|
+
default branch, ticket → in_progress as its
|
|
1169
|
+
first commit, pushed. The namespace defaults
|
|
1170
|
+
to task/claim/ — set claims.branchPrefix in
|
|
814
1171
|
.task/config.json to change it (e.g.
|
|
815
1172
|
"claude/task/", which Claude cloud sessions
|
|
816
1173
|
can push). Branch on origin = claimed —
|
|
817
1174
|
git's atomic ref creation is the lock, so two
|
|
818
|
-
concurrent claimers can't both win.
|
|
1175
|
+
concurrent claimers can't both win. While the
|
|
1176
|
+
root board sets claims.maxOpenCount and that
|
|
1177
|
+
many claim branches exist on origin, claiming
|
|
1178
|
+
is refused (--force overrides). Exit codes:
|
|
819
1179
|
0 claimed, 1 already claimed, 2 not claimable
|
|
820
|
-
(not todo, blocked, needs-human, dirty tree
|
|
821
|
-
|
|
1180
|
+
(not todo, blocked, needs-human, dirty tree,
|
|
1181
|
+
at the cap)
|
|
1182
|
+
task claim --next [--board <P>] [--force]
|
|
1183
|
+
claim the top claimable ticket in one call,
|
|
1184
|
+
retrying past lost races internally — the
|
|
1185
|
+
first line of every scheduled worker. Scope:
|
|
1186
|
+
--board prefixes (repeatable, given order =
|
|
1187
|
+
priority) > the root board's "boards" list >
|
|
1188
|
+
the nearest board. Exit codes: 0 claimed
|
|
1189
|
+
(prints the ticket), 1 queue empty, 2
|
|
1190
|
+
preconditions failed
|
|
1191
|
+
task claim --release <id> --comment "<why>"
|
|
1192
|
+
abandon a claim: delete the branch on origin
|
|
822
1193
|
and locally — the status flip only lived on
|
|
823
|
-
the branch, so deleting it is the revert
|
|
824
|
-
|
|
1194
|
+
the branch, so deleting it is the revert. The
|
|
1195
|
+
comment is required and lands on the ticket
|
|
1196
|
+
(attributed like task comment, uncommitted),
|
|
1197
|
+
so the next worker inherits what was tried
|
|
1198
|
+
task list --claimable [--board <P>]
|
|
1199
|
+
the claim queue: todo tickets in position
|
|
825
1200
|
order, minus blocked / needs-human / already
|
|
826
|
-
claimed on origin. Top entry is next up
|
|
1201
|
+
claimed on origin. Top entry is next up. Same
|
|
1202
|
+
board scope as claim --next
|
|
1203
|
+
task instructions print the shipped agent conventions
|
|
1204
|
+
(skill/SKILL.md) — agents without the skill
|
|
1205
|
+
installed self-serve the house rules
|
|
1206
|
+
task overview [--json] one screen of board health: counts per
|
|
1207
|
+
status, per-goal progress, claimed and
|
|
1208
|
+
needs-human counts, stale tickets (untouched
|
|
1209
|
+
${STALE_DAYS}+ days), oldest open ticket
|
|
827
1210
|
task comment <id> <text> [--author <who>]
|
|
828
1211
|
task delete <id>
|
|
1212
|
+
task goal add <title> [--slug <s>] [--description <text>]
|
|
1213
|
+
create a goal: a titled, described destination
|
|
1214
|
+
tasks belong to (.task/goals/<slug>.md). The
|
|
1215
|
+
description is the strategic why — the context
|
|
1216
|
+
every ticket under it shares
|
|
1217
|
+
task goal list [--archived] every goal with derived progress (done/total —
|
|
1218
|
+
a goal stores no status of its own, ever)
|
|
1219
|
+
task goal show <slug> the goal, its description, and its tasks
|
|
1220
|
+
task goal update <slug> [--title <t>] [--description <text>]
|
|
1221
|
+
task goal archive <slug> shelve a finished/abandoned goal — refused
|
|
1222
|
+
while it still has open tasks
|
|
1223
|
+
task goal unarchive <slug>
|
|
1224
|
+
task goal delete <slug> only when no live task references it
|
|
829
1225
|
task boards every board in this repo — prefix, name, path,
|
|
830
1226
|
open count; * marks the one commands target here
|
|
831
1227
|
task archive <id> move a done/canceled ticket to .task/archive/,
|
|
@@ -835,6 +1231,15 @@ Usage
|
|
|
835
1231
|
task archive --all archive everything done or canceled
|
|
836
1232
|
task unarchive <id> put an archived ticket back on the board
|
|
837
1233
|
task whoami who your comments are attributed to
|
|
1234
|
+
task check [--fix] lint the board files — merges and hand-edits
|
|
1235
|
+
are write paths no CLI code sees. Reports
|
|
1236
|
+
unparseable files, dangling blocked_by refs,
|
|
1237
|
+
bad timestamps, unknown frontmatter keys and
|
|
1238
|
+
formatting drift, for every board at or below
|
|
1239
|
+
here; exits non-zero while problems remain.
|
|
1240
|
+
--fix applies the mechanical repairs first
|
|
1241
|
+
(prune dangling refs, rewrite to canonical
|
|
1242
|
+
form). Cheap enough for pre-commit and CI
|
|
838
1243
|
task serve [--port <n>] [--no-open] [--strict-port]
|
|
839
1244
|
board + table UI with live updates, opened in
|
|
840
1245
|
your browser (default port ${DEFAULT_PORT}; taken ports
|
|
@@ -852,9 +1257,6 @@ Usage
|
|
|
852
1257
|
task unpublish [--repo <owner/name>]
|
|
853
1258
|
take that URL down. Removes the board, not the
|
|
854
1259
|
tasks — those are in .task/ either way
|
|
855
|
-
task migrate move a pre-0.6 board off its committed SQLite
|
|
856
|
-
database and onto text files in .task/tickets/.
|
|
857
|
-
The database stays on disk as an ignored backup
|
|
858
1260
|
|
|
859
1261
|
Values
|
|
860
1262
|
<id> TAS-12, or just 12. A bare number means the nearest board; a
|
|
@@ -862,9 +1264,12 @@ Values
|
|
|
862
1264
|
prefix, so TAS-12 works from anywhere in a monorepo
|
|
863
1265
|
status ${STATUSES.join(" ")}
|
|
864
1266
|
--tag a,b matches a task carrying *either* tag
|
|
1267
|
+
--goal <slug> a task belongs to at most one goal. Goals answer "where is
|
|
1268
|
+
this going" (one); tags answer "what kind" (many) — something
|
|
1269
|
+
that wants two goals is a tag
|
|
865
1270
|
--needs-human this can't be finished by an agent alone
|
|
866
1271
|
--pr <url> attach a pull request (appends); --prs replaces the whole list
|
|
867
|
-
clearing --tags "" drops all tags, --
|
|
1272
|
+
clearing --tags "" drops all tags, --goal "" clears it, --prs ""
|
|
868
1273
|
detaches all PRs
|
|
869
1274
|
|
|
870
1275
|
Comment authors resolve --author → $TASK_AUTHOR → git config user.name →
|
|
@@ -892,22 +1297,28 @@ function main() {
|
|
|
892
1297
|
return cmdAdd(args);
|
|
893
1298
|
case "list":
|
|
894
1299
|
return cmdList(args);
|
|
1300
|
+
case "search":
|
|
1301
|
+
return cmdSearch(args);
|
|
895
1302
|
case "show":
|
|
896
1303
|
return cmdShow(args);
|
|
897
1304
|
case "update":
|
|
898
1305
|
return cmdUpdate(args);
|
|
899
1306
|
case "move":
|
|
900
1307
|
return cmdMove(args);
|
|
901
|
-
case "
|
|
902
|
-
return
|
|
903
|
-
case "done":
|
|
904
|
-
return cmdUpdate(args, "done");
|
|
1308
|
+
case "promote":
|
|
1309
|
+
return cmdPromote(args);
|
|
905
1310
|
case "link":
|
|
906
1311
|
return cmdLink(args, "link");
|
|
907
1312
|
case "unlink":
|
|
908
1313
|
return cmdLink(args, "unlink");
|
|
909
1314
|
case "claim":
|
|
910
1315
|
return cmdClaim(args);
|
|
1316
|
+
case "instructions":
|
|
1317
|
+
return cmdInstructions();
|
|
1318
|
+
case "overview":
|
|
1319
|
+
return cmdOverview(args);
|
|
1320
|
+
case "goal":
|
|
1321
|
+
return cmdGoal(args);
|
|
911
1322
|
case "comment":
|
|
912
1323
|
return cmdComment(args);
|
|
913
1324
|
case "whoami":
|
|
@@ -920,8 +1331,8 @@ function main() {
|
|
|
920
1331
|
return cmdArchive(args);
|
|
921
1332
|
case "unarchive":
|
|
922
1333
|
return cmdUnarchive(args);
|
|
923
|
-
case "
|
|
924
|
-
return
|
|
1334
|
+
case "check":
|
|
1335
|
+
return cmdCheck(args);
|
|
925
1336
|
case "serve":
|
|
926
1337
|
return cmdServe(args);
|
|
927
1338
|
case "publish":
|