@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/src/cli.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
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
|
|
|
13
12
|
import { spawn } from "node:child_process"
|
|
@@ -16,10 +15,21 @@ import type { Server } from "node:http"
|
|
|
16
15
|
import { basename, join } from "node:path"
|
|
17
16
|
import process from "node:process"
|
|
18
17
|
import { resolveAuthor } from "./author.ts"
|
|
19
|
-
import {
|
|
18
|
+
import { checkBoards, checkRoot } from "./check.ts"
|
|
19
|
+
import {
|
|
20
|
+
ClaimError,
|
|
21
|
+
claim,
|
|
22
|
+
claimNext,
|
|
23
|
+
claimableTasks,
|
|
24
|
+
promote,
|
|
25
|
+
release,
|
|
26
|
+
selectionBoards,
|
|
27
|
+
} from "./claim.ts"
|
|
28
|
+
import { STALE_DAYS, buildOverview } from "./overview.ts"
|
|
29
|
+
import { searchStore } from "./search.ts"
|
|
20
30
|
import { detectRepo, parseSlug, publish, resolveHost } from "./publish.ts"
|
|
21
31
|
import { createTaskServer } from "./server.ts"
|
|
22
|
-
import { initProject,
|
|
32
|
+
import { initProject, openBoard } from "./file-store.ts"
|
|
23
33
|
import {
|
|
24
34
|
CONFIG_FILE,
|
|
25
35
|
TASK_DIR,
|
|
@@ -32,13 +42,6 @@ import {
|
|
|
32
42
|
} from "./store.ts"
|
|
33
43
|
import { STATUSES, isStatus, type Status, type Task, type TaskPatch } from "./types.ts"
|
|
34
44
|
|
|
35
|
-
// node:sqlite still emits an ExperimentalWarning on Node 22 — noise in a CLI
|
|
36
|
-
// that runs it on every invocation. Filter that one warning, keep the rest.
|
|
37
|
-
process.removeAllListeners("warning")
|
|
38
|
-
process.on("warning", (warning) => {
|
|
39
|
-
if (warning.name !== "ExperimentalWarning") console.error(warning)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
45
|
/**
|
|
43
46
|
* Read from package.json rather than duplicated as a literal. Releases here are
|
|
44
47
|
* cut by changesets, which bumps package.json and nothing else — so a hardcoded
|
|
@@ -84,8 +87,18 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
84
87
|
"public",
|
|
85
88
|
"private",
|
|
86
89
|
"no-wait",
|
|
90
|
+
"fix",
|
|
91
|
+
"next",
|
|
92
|
+
"force",
|
|
87
93
|
])
|
|
88
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Value flags that may repeat — `--board NIC --board TAS` accumulates in
|
|
97
|
+
* given order, equivalent to `--board NIC,TAS`. Everything else keeps
|
|
98
|
+
* last-one-wins.
|
|
99
|
+
*/
|
|
100
|
+
const REPEATABLE_FLAGS = new Set(["board"])
|
|
101
|
+
|
|
89
102
|
function parseArgs(argv: string[]): Args {
|
|
90
103
|
const positional: string[] = []
|
|
91
104
|
const flags: Record<string, string | boolean> = {}
|
|
@@ -93,13 +106,18 @@ function parseArgs(argv: string[]): Args {
|
|
|
93
106
|
const arg = argv[i]
|
|
94
107
|
if (arg.startsWith("--")) {
|
|
95
108
|
const name = arg.slice(2)
|
|
109
|
+
const set = (key: string, value: string): void => {
|
|
110
|
+
const prior = flags[key]
|
|
111
|
+
flags[key] =
|
|
112
|
+
REPEATABLE_FLAGS.has(key) && typeof prior === "string" ? `${prior},${value}` : value
|
|
113
|
+
}
|
|
96
114
|
const eq = name.indexOf("=")
|
|
97
115
|
if (eq >= 0) {
|
|
98
|
-
|
|
116
|
+
set(name.slice(0, eq), name.slice(eq + 1))
|
|
99
117
|
} else if (BOOLEAN_FLAGS.has(name) || i + 1 >= argv.length || argv[i + 1].startsWith("--")) {
|
|
100
118
|
flags[name] = true
|
|
101
119
|
} else {
|
|
102
|
-
|
|
120
|
+
set(name, argv[++i])
|
|
103
121
|
}
|
|
104
122
|
} else {
|
|
105
123
|
positional.push(arg)
|
|
@@ -199,13 +217,27 @@ function patchFromFlags(flags: Args["flags"]): TaskPatch {
|
|
|
199
217
|
const title = str(flags, "title")
|
|
200
218
|
if (title !== undefined) patch.title = title
|
|
201
219
|
const description = str(flags, "description") ?? str(flags, "desc")
|
|
220
|
+
// Long markdown doesn't survive shell quoting — `--description-file` keeps
|
|
221
|
+
// agents writing plans and specs on the CLI path instead of hand-editing
|
|
222
|
+
// ticket files. `-` reads stdin.
|
|
223
|
+
const descriptionFile = str(flags, "description-file")
|
|
224
|
+
if (description !== undefined && descriptionFile !== undefined) {
|
|
225
|
+
fail("pass --description or --description-file, not both")
|
|
226
|
+
}
|
|
202
227
|
if (description !== undefined) patch.description = description
|
|
228
|
+
if (descriptionFile !== undefined) {
|
|
229
|
+
try {
|
|
230
|
+
patch.description = readFileSync(descriptionFile === "-" ? 0 : descriptionFile, "utf8")
|
|
231
|
+
} catch {
|
|
232
|
+
fail(`--description-file: can't read ${descriptionFile === "-" ? "stdin" : descriptionFile}`)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
203
235
|
const status = str(flags, "status")
|
|
204
236
|
if (status !== undefined) patch.status = parseStatus(status)
|
|
205
237
|
const tags = str(flags, "tags") ?? str(flags, "tag")
|
|
206
238
|
if (tags !== undefined) patch.tags = parseTags(tags)
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
239
|
+
const goal = str(flags, "goal")
|
|
240
|
+
if (goal !== undefined) patch.goal = goal.trim() || null
|
|
209
241
|
if (flags["needs-human"]) patch.needsHuman = true
|
|
210
242
|
if (flags["no-needs-human"]) patch.needsHuman = false
|
|
211
243
|
// Replace semantics like --tags; `--pr` (append one) is handled per-command
|
|
@@ -241,7 +273,7 @@ function taskRow(t: Task): string[] {
|
|
|
241
273
|
t.needsHuman ? "⚑" : "",
|
|
242
274
|
t.title,
|
|
243
275
|
t.tags.join(","),
|
|
244
|
-
t.
|
|
276
|
+
t.goal ?? "",
|
|
245
277
|
]
|
|
246
278
|
}
|
|
247
279
|
|
|
@@ -272,7 +304,7 @@ function cmdInit(args: Args): void {
|
|
|
272
304
|
|
|
273
305
|
function cmdAdd(args: Args): void {
|
|
274
306
|
const title = args.positional.join(" ").trim()
|
|
275
|
-
if (!title) fail(`usage: task add <title> [--description …] [--status …] [--tags a,b] [--
|
|
307
|
+
if (!title) fail(`usage: task add <title> [--description …] [--status …] [--tags a,b] [--goal <slug>] [--needs-human]`)
|
|
276
308
|
const store = openStore()
|
|
277
309
|
const patch = patchFromFlags(args.flags)
|
|
278
310
|
const pr = str(args.flags, "pr")
|
|
@@ -285,15 +317,31 @@ function cmdAdd(args: Args): void {
|
|
|
285
317
|
}
|
|
286
318
|
}
|
|
287
319
|
|
|
320
|
+
/** `--board NIC --board TAS` (or `--board NIC,TAS`) → ["NIC", "TAS"]. */
|
|
321
|
+
function boardFlag(args: Args): string[] | undefined {
|
|
322
|
+
const raw = str(args.flags, "board")
|
|
323
|
+
if (raw === undefined) return undefined
|
|
324
|
+
const prefixes = raw.split(",").map((p) => p.trim()).filter(Boolean)
|
|
325
|
+
if (prefixes.length === 0) fail("--board needs a prefix, e.g. --board TAS")
|
|
326
|
+
return prefixes
|
|
327
|
+
}
|
|
328
|
+
|
|
288
329
|
function cmdList(args: Args): void {
|
|
289
|
-
const store = openStore()
|
|
290
330
|
if (args.flags.claimable) {
|
|
291
331
|
// The dispatcher's queue view (TAS-21): claimable already pins the status
|
|
292
332
|
// set and consults origin, so the board-shaping flags don't compose.
|
|
293
333
|
for (const flag of ["status", "all", "archived"]) {
|
|
294
334
|
if (args.flags[flag] !== undefined) fail(`--claimable can't be combined with --${flag}`)
|
|
295
335
|
}
|
|
296
|
-
|
|
336
|
+
// Selection scope, same resolution as `claim --next`: --board flag > root
|
|
337
|
+
// board's `boards` list > nearest board. Board order is priority order.
|
|
338
|
+
let tasks: Task[]
|
|
339
|
+
try {
|
|
340
|
+
tasks = selectionBoards(process.cwd(), boardFlag(args)).flatMap((s) => claimableTasks(s))
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (error instanceof ClaimError) fail(error.message)
|
|
343
|
+
throw error
|
|
344
|
+
}
|
|
297
345
|
if (args.flags.json) {
|
|
298
346
|
console.log(JSON.stringify({ tasks }, null, 2))
|
|
299
347
|
} else if (tasks.length === 0) {
|
|
@@ -303,6 +351,8 @@ function cmdList(args: Args): void {
|
|
|
303
351
|
}
|
|
304
352
|
return
|
|
305
353
|
}
|
|
354
|
+
if (args.flags.board !== undefined) fail("--board only applies to list --claimable")
|
|
355
|
+
const store = openStore()
|
|
306
356
|
const archived = Boolean(args.flags.archived)
|
|
307
357
|
const statusFlag = str(args.flags, "status")
|
|
308
358
|
const statuses = statusFlag
|
|
@@ -316,7 +366,7 @@ function cmdList(args: Args): void {
|
|
|
316
366
|
const tasks = store.list({
|
|
317
367
|
statuses,
|
|
318
368
|
tags: tagFlag ? parseTags(tagFlag) : undefined,
|
|
319
|
-
|
|
369
|
+
goal: str(args.flags, "goal"),
|
|
320
370
|
needsHuman: args.flags["needs-human"] ? true : undefined,
|
|
321
371
|
archived: archived || undefined,
|
|
322
372
|
})
|
|
@@ -338,6 +388,53 @@ function cmdList(args: Args): void {
|
|
|
338
388
|
}
|
|
339
389
|
}
|
|
340
390
|
|
|
391
|
+
/**
|
|
392
|
+
* `task search <query>` — find tickets by what they say. Case-insensitive
|
|
393
|
+
* substring over titles, descriptions and comments, scoped by the usual list
|
|
394
|
+
* filters; `--archived` searches the archive *in addition to* the board (a
|
|
395
|
+
* search is a question about everything ever written down, unlike
|
|
396
|
+
* `list --archived`, which is a view of one place).
|
|
397
|
+
*/
|
|
398
|
+
function cmdSearch(args: Args): void {
|
|
399
|
+
const query = args.positional.join(" ").trim()
|
|
400
|
+
if (!query) {
|
|
401
|
+
fail(
|
|
402
|
+
"usage: task search <query> [--status <s1,s2>] [--tag <a,b>] [--goal <slug>] [--needs-human] [--archived]",
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
const store = openStore()
|
|
406
|
+
const statusFlag = str(args.flags, "status")
|
|
407
|
+
const tagFlag = str(args.flags, "tags") ?? str(args.flags, "tag")
|
|
408
|
+
const results = searchStore(
|
|
409
|
+
store,
|
|
410
|
+
query,
|
|
411
|
+
{
|
|
412
|
+
statuses: statusFlag ? statusFlag.split(",").map(parseStatus) : undefined,
|
|
413
|
+
tags: tagFlag ? parseTags(tagFlag) : undefined,
|
|
414
|
+
goal: str(args.flags, "goal"),
|
|
415
|
+
needsHuman: args.flags["needs-human"] ? true : undefined,
|
|
416
|
+
},
|
|
417
|
+
Boolean(args.flags.archived),
|
|
418
|
+
)
|
|
419
|
+
if (args.flags.json) {
|
|
420
|
+
console.log(JSON.stringify({ query, results }, null, 2))
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
if (results.length === 0) {
|
|
424
|
+
console.log(`no matches for "${query}"${args.flags.archived ? "" : " (--archived searches the archive too)"}`)
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
for (const { task, matches } of results) {
|
|
428
|
+
console.log(
|
|
429
|
+
`${task.id} ${STATUS_GLYPH[task.status]} ${task.status}${task.archived ? " (archived)" : ""} ${task.title}`,
|
|
430
|
+
)
|
|
431
|
+
for (const match of matches) {
|
|
432
|
+
const label = match.field === "comment" ? `comment${match.author ? ` by ${match.author}` : ""}` : match.field
|
|
433
|
+
console.log(` ${label}: ${match.snippet}`)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
341
438
|
/**
|
|
342
439
|
* `task boards` — every board in this repo, found the way `task serve` finds
|
|
343
440
|
* them but anchored at the *outermost* board root, so it answers from anywhere
|
|
@@ -433,8 +530,12 @@ function cmdShow(args: Args): void {
|
|
|
433
530
|
const task = store.get(number)
|
|
434
531
|
if (!task) fail(`no such task: ${store.displayId(number)}`)
|
|
435
532
|
const comments = store.comments(number)
|
|
533
|
+
// The goal rides along in full — title *and* description — so an agent
|
|
534
|
+
// picking the ticket up cold inherits the shared context without it being
|
|
535
|
+
// pasted into every ticket. null when unset or when the ref dangles.
|
|
536
|
+
const goal = task.goal ? store.getGoal(task.goal) : null
|
|
436
537
|
if (args.flags.json) {
|
|
437
|
-
console.log(JSON.stringify({ task, comments }, null, 2))
|
|
538
|
+
console.log(JSON.stringify({ task, comments, goal }, null, 2))
|
|
438
539
|
return
|
|
439
540
|
}
|
|
440
541
|
// A linked task is only as useful as knowing whether it's still in the way.
|
|
@@ -450,13 +551,19 @@ function cmdShow(args: Args): void {
|
|
|
450
551
|
if (task.archived) console.log(`archived yes — \`task unarchive ${task.id}\` to edit`)
|
|
451
552
|
if (task.needsHuman) console.log(`needs a human`)
|
|
452
553
|
if (task.tags.length) console.log(`tags ${task.tags.join(", ")}`)
|
|
453
|
-
if (task.
|
|
554
|
+
if (task.goal) {
|
|
555
|
+
console.log(`goal ${task.goal}${goal ? ` — ${goal.title}${goal.archived ? " (archived)" : ""}` : ""}`)
|
|
556
|
+
}
|
|
454
557
|
if (task.blockedBy.length) console.log(`blocked by ${describeLinks(task.blockedBy)}`)
|
|
455
558
|
if (task.blocks.length) console.log(`blocks ${describeLinks(task.blocks)}`)
|
|
456
559
|
for (const pr of task.prs) console.log(`pr ${pr}`)
|
|
457
560
|
console.log(`created ${task.createdAt}`)
|
|
458
561
|
console.log(`updated ${task.updatedAt}`)
|
|
459
562
|
if (task.description) console.log(`\n${task.description}`)
|
|
563
|
+
if (goal?.description) {
|
|
564
|
+
console.log(`\n── goal: ${goal.title} (${goal.slug})`)
|
|
565
|
+
console.log(goal.description.split("\n").map((line) => ` ${line}`).join("\n"))
|
|
566
|
+
}
|
|
460
567
|
if (comments.length) {
|
|
461
568
|
console.log(``)
|
|
462
569
|
for (const c of comments) {
|
|
@@ -466,13 +573,12 @@ function cmdShow(args: Args): void {
|
|
|
466
573
|
}
|
|
467
574
|
}
|
|
468
575
|
|
|
469
|
-
function cmdUpdate(args: Args
|
|
576
|
+
function cmdUpdate(args: Args): void {
|
|
470
577
|
const ref = args.positional[0]
|
|
471
578
|
if (!ref) fail("usage: task update <id> [--status …] [--title …] …")
|
|
472
579
|
const store = openStoreFor(ref)
|
|
473
580
|
const number = store.parseId(ref)
|
|
474
581
|
const patch = patchFromFlags(args.flags)
|
|
475
|
-
if (forcedStatus) patch.status = forcedStatus
|
|
476
582
|
const pr = str(args.flags, "pr")
|
|
477
583
|
if (pr) {
|
|
478
584
|
// Append, dedup — `--pr <url>` is "attach this PR", not "replace the list".
|
|
@@ -494,6 +600,34 @@ function cmdMove(args: Args): void {
|
|
|
494
600
|
cmdUpdate({ positional: [ref], flags: { ...args.flags, status } })
|
|
495
601
|
}
|
|
496
602
|
|
|
603
|
+
/**
|
|
604
|
+
* `task promote <id>` — backlog → todo, the human "build this" call; see
|
|
605
|
+
* claim.ts for why blocked / needs-human are holds (reported), not refusals.
|
|
606
|
+
* Exit codes follow claim's contract: 0 promoted, 2 refused (fix something).
|
|
607
|
+
*/
|
|
608
|
+
function cmdPromote(args: Args): void {
|
|
609
|
+
const ref = args.positional[0]
|
|
610
|
+
if (!ref) fail("usage: task promote <id>")
|
|
611
|
+
const store = openStoreFor(ref)
|
|
612
|
+
try {
|
|
613
|
+
const result = promote(store, store.parseId(ref))
|
|
614
|
+
if (args.flags.json) {
|
|
615
|
+
console.log(JSON.stringify({ task: result.task, holds: result.holds }, null, 2))
|
|
616
|
+
} else {
|
|
617
|
+
printTask(result.task)
|
|
618
|
+
for (const hold of result.holds) {
|
|
619
|
+
console.log(`note: ${hold} — in todo, but not claimable until that clears`)
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
} catch (error) {
|
|
623
|
+
if (error instanceof ClaimError) {
|
|
624
|
+
console.error(`error: ${error.message}`)
|
|
625
|
+
process.exit(2)
|
|
626
|
+
}
|
|
627
|
+
throw error
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
497
631
|
function cmdComment(args: Args): void {
|
|
498
632
|
const [ref, ...rest] = args.positional
|
|
499
633
|
const body = rest.join(" ").trim()
|
|
@@ -538,35 +672,77 @@ function cmdLink(args: Args, action: "link" | "unlink"): void {
|
|
|
538
672
|
}
|
|
539
673
|
}
|
|
540
674
|
|
|
675
|
+
const CLAIM_USAGE =
|
|
676
|
+
'usage: task claim <id> | task claim --next | task claim --release <id> --comment "<why>"'
|
|
677
|
+
|
|
541
678
|
/**
|
|
542
|
-
* `task claim <id>` / `task claim --release <id>` — see
|
|
543
|
-
* mechanics. Exit codes are the contract callers script
|
|
544
|
-
* 1 already claimed
|
|
679
|
+
* `task claim <id>` / `task claim --next` / `task claim --release <id>` — see
|
|
680
|
+
* claim.ts for the mechanics. Exit codes are the contract callers script
|
|
681
|
+
* against: 0 claimed, 1 already claimed / queue empty (nothing to do here —
|
|
682
|
+
* a scheduled worker stops cleanly), 2 preconditions failed (fix something).
|
|
545
683
|
*/
|
|
546
684
|
function cmdClaim(args: Args): void {
|
|
547
685
|
const ref = args.positional[0]
|
|
548
|
-
|
|
549
|
-
const
|
|
550
|
-
|
|
686
|
+
const options = { force: Boolean(args.flags.force) }
|
|
687
|
+
const printClaim = (result: { task: Task; branch: string; base: string }): void => {
|
|
688
|
+
if (args.flags.json) {
|
|
689
|
+
console.log(JSON.stringify({ task: result.task, branch: result.branch }, null, 2))
|
|
690
|
+
} else {
|
|
691
|
+
console.log(`claimed ${result.task.id} — on ${result.branch} (from ${result.base}), status in_progress`)
|
|
692
|
+
}
|
|
693
|
+
}
|
|
551
694
|
try {
|
|
695
|
+
if (args.flags.next) {
|
|
696
|
+
// The first line of every scheduled worker: claim the top claimable
|
|
697
|
+
// ticket across the selection scope (--board > root `boards` > nearest),
|
|
698
|
+
// retrying past lost races internally.
|
|
699
|
+
if (ref) fail("pass an id or --next, not both")
|
|
700
|
+
if (args.flags.release) fail(CLAIM_USAGE)
|
|
701
|
+
const result = claimNext(selectionBoards(process.cwd(), boardFlag(args)), options)
|
|
702
|
+
if (!result) {
|
|
703
|
+
if (args.flags.json) {
|
|
704
|
+
console.log(JSON.stringify({ task: null }, null, 2))
|
|
705
|
+
} else {
|
|
706
|
+
console.log("nothing claimable — no unblocked, unclaimed todo tickets in scope")
|
|
707
|
+
}
|
|
708
|
+
process.exit(1)
|
|
709
|
+
}
|
|
710
|
+
printClaim(result)
|
|
711
|
+
return
|
|
712
|
+
}
|
|
713
|
+
if (!ref) fail(CLAIM_USAGE)
|
|
714
|
+
const store = openStoreFor(ref)
|
|
715
|
+
const number = store.parseId(ref)
|
|
552
716
|
if (args.flags.release) {
|
|
717
|
+
// A released claim must leave its failure context behind: the branch
|
|
718
|
+
// (and whatever was tried on it) is about to evaporate, so the reason
|
|
719
|
+
// is required, and lands as an ordinary attributed comment.
|
|
720
|
+
const why = str(args.flags, "comment")?.trim()
|
|
721
|
+
if (!why) {
|
|
722
|
+
fail(
|
|
723
|
+
'task claim --release requires --comment "<why>" — the next worker inherits what was tried',
|
|
724
|
+
)
|
|
725
|
+
}
|
|
553
726
|
const result = release(store, number)
|
|
727
|
+
const commented = result.remote || result.local
|
|
728
|
+
// After the branch deletion, so the comment survives it: release leaves
|
|
729
|
+
// the checkout on the default branch, and the comment file sits there
|
|
730
|
+
// uncommitted, to ride along with the next commit.
|
|
731
|
+
const comment = commented
|
|
732
|
+
? store.addComment(number, why, resolveAuthor(str(args.flags, "author")).name)
|
|
733
|
+
: null
|
|
554
734
|
if (args.flags.json) {
|
|
555
|
-
console.log(JSON.stringify({ released: result }, null, 2))
|
|
556
|
-
} else if (!
|
|
735
|
+
console.log(JSON.stringify({ released: result, comment }, null, 2))
|
|
736
|
+
} else if (!commented) {
|
|
557
737
|
console.log(`${store.displayId(number)} wasn't claimed — no ${result.branch} to delete`)
|
|
558
738
|
} else {
|
|
559
739
|
const where = [result.remote && "origin", result.local && "local"].filter(Boolean)
|
|
560
740
|
console.log(`released ${store.displayId(number)} — deleted ${result.branch} (${where.join(" and ")})`)
|
|
741
|
+
console.log(`left the reason as a comment on ${store.displayId(number)} — uncommitted, commit it with your next change`)
|
|
561
742
|
}
|
|
562
743
|
return
|
|
563
744
|
}
|
|
564
|
-
|
|
565
|
-
if (args.flags.json) {
|
|
566
|
-
console.log(JSON.stringify({ task: result.task, branch: result.branch }, null, 2))
|
|
567
|
-
} else {
|
|
568
|
-
console.log(`claimed ${result.task.id} — on ${result.branch} (from ${result.base}), status in_progress`)
|
|
569
|
-
}
|
|
745
|
+
printClaim(claim(store, number, options))
|
|
570
746
|
} catch (error) {
|
|
571
747
|
if (error instanceof ClaimError) {
|
|
572
748
|
console.error(`error: ${error.message}`)
|
|
@@ -576,6 +752,161 @@ function cmdClaim(args: Args): void {
|
|
|
576
752
|
}
|
|
577
753
|
}
|
|
578
754
|
|
|
755
|
+
/** `task instructions` — the shipped agent conventions, self-served at runtime. */
|
|
756
|
+
function cmdInstructions(): void {
|
|
757
|
+
// skill/ ships in the npm package; `../` from both dist/cli.js and
|
|
758
|
+
// src/cli.ts is the package root, same trick VERSION uses.
|
|
759
|
+
process.stdout.write(readFileSync(new URL("../skill/SKILL.md", import.meta.url), "utf8"))
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/** `task overview [--json]` — one screen of board health. See overview.ts. */
|
|
763
|
+
function cmdOverview(args: Args): void {
|
|
764
|
+
const overview = buildOverview(openStore())
|
|
765
|
+
if (args.flags.json) {
|
|
766
|
+
console.log(JSON.stringify({ overview }, null, 2))
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
console.log(`${overview.board} (${overview.prefix}) — ${overview.open} open`)
|
|
770
|
+
console.log(
|
|
771
|
+
` ${STATUSES.map((s) => `${STATUS_GLYPH[s]} ${s} ${overview.statuses[s]}`).join(" ")}`,
|
|
772
|
+
)
|
|
773
|
+
const claimed =
|
|
774
|
+
overview.claimed === null
|
|
775
|
+
? "claimed ? (origin unreachable)"
|
|
776
|
+
: `claimed ${overview.claimed.length}${overview.claimed.length ? ` (${overview.claimed.join(", ")})` : ""}`
|
|
777
|
+
console.log(` ${claimed} needs-human ${overview.needsHuman}`)
|
|
778
|
+
if (overview.goals.length) {
|
|
779
|
+
console.log("goals")
|
|
780
|
+
table(
|
|
781
|
+
overview.goals.map((g) => [
|
|
782
|
+
` ${g.slug}`,
|
|
783
|
+
g.title,
|
|
784
|
+
`${g.done}/${g.total} done${g.total ? ` (${Math.round((g.done / g.total) * 100)}%)` : ""}`,
|
|
785
|
+
]),
|
|
786
|
+
)
|
|
787
|
+
}
|
|
788
|
+
if (overview.stale.length) {
|
|
789
|
+
console.log(`stale (untouched ${STALE_DAYS}+ days)`)
|
|
790
|
+
table(overview.stale.map((t) => [` ${t.id}`, t.title, t.updatedAt.slice(0, 10)]))
|
|
791
|
+
}
|
|
792
|
+
if (overview.oldestOpen) {
|
|
793
|
+
console.log(
|
|
794
|
+
`oldest open ${overview.oldestOpen.id} ${overview.oldestOpen.title} (created ${overview.oldestOpen.createdAt.slice(0, 10)})`,
|
|
795
|
+
)
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* `task goal …` — the goals a board's tasks belong to. A goal is a titled,
|
|
801
|
+
* described destination (`.task/goals/<slug>.md`); a task carries at most one.
|
|
802
|
+
* There is no goal status anywhere here on purpose: progress is derived from
|
|
803
|
+
* the tasks, and `archive` is the only lifecycle a goal has.
|
|
804
|
+
*/
|
|
805
|
+
function cmdGoal(args: Args): void {
|
|
806
|
+
const [sub, ...rest] = args.positional
|
|
807
|
+
const usage =
|
|
808
|
+
"usage: task goal add <title> [--slug <s>] [--description …] | list [--archived] | " +
|
|
809
|
+
"show <slug> | update <slug> [--title …] [--description …] | archive <slug> | " +
|
|
810
|
+
"unarchive <slug> | delete <slug>"
|
|
811
|
+
if (!sub) fail(usage)
|
|
812
|
+
const store = openStore()
|
|
813
|
+
|
|
814
|
+
const slugArg = (): string => {
|
|
815
|
+
const slug = rest[0]
|
|
816
|
+
if (!slug) fail(`usage: task goal ${sub} <slug>`)
|
|
817
|
+
return slug
|
|
818
|
+
}
|
|
819
|
+
const printGoal = (goal: { slug: string; title: string; archived?: boolean }, verb: string) =>
|
|
820
|
+
console.log(`${verb} ${goal.slug} ${goal.title}${goal.archived ? " (archived)" : ""}`)
|
|
821
|
+
|
|
822
|
+
switch (sub) {
|
|
823
|
+
case "add": {
|
|
824
|
+
const title = rest.join(" ").trim()
|
|
825
|
+
if (!title) fail("usage: task goal add <title> [--slug <s>] [--description …]")
|
|
826
|
+
const goal = store.createGoal({
|
|
827
|
+
title,
|
|
828
|
+
slug: str(args.flags, "slug"),
|
|
829
|
+
description: str(args.flags, "description") ?? str(args.flags, "desc"),
|
|
830
|
+
})
|
|
831
|
+
if (args.flags.json) console.log(JSON.stringify({ goal }, null, 2))
|
|
832
|
+
else printGoal(goal, "created")
|
|
833
|
+
return
|
|
834
|
+
}
|
|
835
|
+
case "list": {
|
|
836
|
+
const archived = Boolean(args.flags.archived)
|
|
837
|
+
const goals = store.goals(archived)
|
|
838
|
+
// Progress is derived here, at read time — a goal file never stores it.
|
|
839
|
+
const tasks = store.list({ statuses: undefined })
|
|
840
|
+
const rows = goals.map((goal) => {
|
|
841
|
+
const mine = tasks.filter((t) => t.goal === goal.slug)
|
|
842
|
+
const done = mine.filter((t) => t.status === "done" || t.status === "canceled").length
|
|
843
|
+
return { ...goal, open: mine.length - done, done, total: mine.length }
|
|
844
|
+
})
|
|
845
|
+
if (args.flags.json) {
|
|
846
|
+
console.log(JSON.stringify({ goals: rows }, null, 2))
|
|
847
|
+
} else if (rows.length === 0) {
|
|
848
|
+
console.log(archived ? "no archived goals" : "no goals — create one with `task goal add`")
|
|
849
|
+
} else {
|
|
850
|
+
table(rows.map((g) => [g.slug, g.title, `${g.done}/${g.total} done`]))
|
|
851
|
+
}
|
|
852
|
+
return
|
|
853
|
+
}
|
|
854
|
+
case "show": {
|
|
855
|
+
const slug = slugArg()
|
|
856
|
+
const goal = store.getGoal(slug)
|
|
857
|
+
if (!goal) fail(`no such goal: "${slug}"`)
|
|
858
|
+
const tasks = store.list({ statuses: undefined, goal: slug })
|
|
859
|
+
if (args.flags.json) {
|
|
860
|
+
console.log(JSON.stringify({ goal, tasks }, null, 2))
|
|
861
|
+
return
|
|
862
|
+
}
|
|
863
|
+
console.log(`${goal.slug} ${goal.title}`)
|
|
864
|
+
if (goal.archived) console.log(`archived yes — \`task goal unarchive ${goal.slug}\` to edit`)
|
|
865
|
+
console.log(`created ${goal.createdAt}`)
|
|
866
|
+
console.log(`updated ${goal.updatedAt}`)
|
|
867
|
+
if (goal.description) console.log(`\n${goal.description}`)
|
|
868
|
+
if (tasks.length) {
|
|
869
|
+
console.log("")
|
|
870
|
+
table(tasks.map(taskRow))
|
|
871
|
+
}
|
|
872
|
+
return
|
|
873
|
+
}
|
|
874
|
+
case "update": {
|
|
875
|
+
const patch = {
|
|
876
|
+
...(str(args.flags, "title") !== undefined && { title: str(args.flags, "title") }),
|
|
877
|
+
...((str(args.flags, "description") ?? str(args.flags, "desc")) !== undefined && {
|
|
878
|
+
description: str(args.flags, "description") ?? str(args.flags, "desc"),
|
|
879
|
+
}),
|
|
880
|
+
}
|
|
881
|
+
if (Object.keys(patch).length === 0) fail("nothing to update — pass --title or --description")
|
|
882
|
+
const goal = store.updateGoal(slugArg(), patch)
|
|
883
|
+
if (args.flags.json) console.log(JSON.stringify({ goal }, null, 2))
|
|
884
|
+
else printGoal(goal, "updated")
|
|
885
|
+
return
|
|
886
|
+
}
|
|
887
|
+
case "archive": {
|
|
888
|
+
const goal = store.archiveGoal(slugArg())
|
|
889
|
+
if (args.flags.json) console.log(JSON.stringify({ goal }, null, 2))
|
|
890
|
+
else printGoal(goal, "archived")
|
|
891
|
+
return
|
|
892
|
+
}
|
|
893
|
+
case "unarchive": {
|
|
894
|
+
const goal = store.unarchiveGoal(slugArg())
|
|
895
|
+
if (args.flags.json) console.log(JSON.stringify({ goal }, null, 2))
|
|
896
|
+
else printGoal(goal, "unarchived")
|
|
897
|
+
return
|
|
898
|
+
}
|
|
899
|
+
case "delete": {
|
|
900
|
+
const slug = slugArg()
|
|
901
|
+
store.deleteGoal(slug)
|
|
902
|
+
console.log(`deleted goal ${slug}`)
|
|
903
|
+
return
|
|
904
|
+
}
|
|
905
|
+
default:
|
|
906
|
+
fail(usage)
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
579
910
|
const AUTHOR_SOURCE: Record<string, string> = {
|
|
580
911
|
flag: "--author",
|
|
581
912
|
env: "$TASK_AUTHOR",
|
|
@@ -594,27 +925,42 @@ function cmdWhoami(args: Args): void {
|
|
|
594
925
|
}
|
|
595
926
|
|
|
596
927
|
/**
|
|
597
|
-
* `task
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
928
|
+
* `task check [--fix]` — lint the board files. Git merges and hand-edits are
|
|
929
|
+
* write paths no CLI code sees, so validation is a command you can run (and
|
|
930
|
+
* put in pre-commit or CI, where merge-introduced drift gets caught). Covers
|
|
931
|
+
* every board at or below here, the same set `task serve` would serve. Exits
|
|
932
|
+
* non-zero while problems remain; `--fix` first applies the mechanical
|
|
933
|
+
* repairs — pruning dangling references, rewriting files into canonical form.
|
|
601
934
|
*/
|
|
602
|
-
function
|
|
603
|
-
const root =
|
|
604
|
-
|
|
605
|
-
const result =
|
|
935
|
+
function cmdCheck(args: Args): void {
|
|
936
|
+
const root = checkRoot(process.cwd())
|
|
937
|
+
const fix = Boolean(args.flags.fix)
|
|
938
|
+
const result = checkBoards(root, fix)
|
|
939
|
+
if (result.boards === 0) {
|
|
940
|
+
fail("no .task directory found in this directory, any parent, or below — run `task init` first")
|
|
941
|
+
}
|
|
942
|
+
// In fix mode the fixable issues were just repaired; what's left needs a human.
|
|
943
|
+
const remaining = fix ? result.issues.filter((issue) => !issue.fixable) : result.issues
|
|
944
|
+
|
|
606
945
|
if (args.flags.json) {
|
|
607
|
-
console.log(JSON.stringify({
|
|
608
|
-
|
|
946
|
+
console.log(JSON.stringify({ ...result, remaining }, null, 2))
|
|
947
|
+
} else {
|
|
948
|
+
for (const file of result.fixed) console.log(`fixed ${file}`)
|
|
949
|
+
for (const issue of remaining) {
|
|
950
|
+
console.log(`${issue.fixable ? "fixable" : "problem"} ${issue.file}`)
|
|
951
|
+
console.log(` ${issue.message}`)
|
|
952
|
+
}
|
|
953
|
+
const scope = `${result.files} file${result.files === 1 ? "" : "s"} across ${result.boards} board${result.boards === 1 ? "" : "s"}`
|
|
954
|
+
if (remaining.length === 0) {
|
|
955
|
+
console.log(result.fixed.length ? `fixed ${result.fixed.length}, ${scope} clean` : `ok — ${scope}`)
|
|
956
|
+
} else {
|
|
957
|
+
const fixable = remaining.filter((issue) => issue.fixable).length
|
|
958
|
+
console.log(
|
|
959
|
+
`${remaining.length} problem${remaining.length === 1 ? "" : "s"} in ${scope}${fixable ? ` (${fixable} fixable — run \`task check --fix\`)` : ""}`,
|
|
960
|
+
)
|
|
961
|
+
}
|
|
609
962
|
}
|
|
610
|
-
|
|
611
|
-
`Migrated ${result.tasks} task${result.tasks === 1 ? "" : "s"} and ${result.comments} comment${result.comments === 1 ? "" : "s"} to .task/tickets/`,
|
|
612
|
-
)
|
|
613
|
-
console.log(`tasks.db stays on disk as a backup, but it's ignored now — the files are the state.`)
|
|
614
|
-
console.log(``)
|
|
615
|
-
console.log(`Next:`)
|
|
616
|
-
console.log(` git rm --cached ${join(TASK_DIR, "tasks.db")} stop tracking the database`)
|
|
617
|
-
console.log(` git add ${TASK_DIR} commit the tickets`)
|
|
963
|
+
if (remaining.length > 0) process.exitCode = 1
|
|
618
964
|
}
|
|
619
965
|
|
|
620
966
|
function cmdDelete(args: Args): void {
|
|
@@ -818,42 +1164,102 @@ subdirectory (it walks up to find .task/, like git).
|
|
|
818
1164
|
|
|
819
1165
|
Usage
|
|
820
1166
|
task init [--name <name>] [--prefix <PREFIX>]
|
|
821
|
-
task add <title> [--description <text>
|
|
822
|
-
[--
|
|
823
|
-
|
|
1167
|
+
task add <title> [--description <text> | --description-file <path|->]
|
|
1168
|
+
[--status <s>] [--tags <a,b>] [--goal <slug>] [--needs-human]
|
|
1169
|
+
new tickets land in backlog (the dump list)
|
|
1170
|
+
unless --status says otherwise;
|
|
1171
|
+
--description-file reads markdown from a file
|
|
1172
|
+
(or stdin with -) — no shell-quoting fights
|
|
1173
|
+
task list [--status <s1,s2>] [--tag <a,b>] [--goal <slug>]
|
|
824
1174
|
[--needs-human] [--all] [--archived] [--claimable]
|
|
825
|
-
task
|
|
826
|
-
|
|
827
|
-
|
|
1175
|
+
task search <query> [--status <s1,s2>] [--tag <a,b>] [--goal <slug>]
|
|
1176
|
+
[--needs-human] [--archived]
|
|
1177
|
+
find tickets by what they say — case-insensitive
|
|
1178
|
+
match over titles, descriptions and comments,
|
|
1179
|
+
with a snippet of the matching line. --archived
|
|
1180
|
+
searches the archive too, on top of the board
|
|
1181
|
+
task show <id> the ticket, its comments — and its goal's title
|
|
1182
|
+
and description inline, so a cold pickup gets
|
|
1183
|
+
the shared context without chasing it
|
|
1184
|
+
task update <id> [--title <t>] [--description <text> | --description-file <path|->]
|
|
1185
|
+
[--status <s>] [--tags <a,b>] [--goal <slug>]
|
|
828
1186
|
[--needs-human | --no-needs-human]
|
|
829
1187
|
[--pr <url>] [--prs <url1,url2>]
|
|
830
|
-
task move <id> <status>
|
|
831
|
-
|
|
832
|
-
|
|
1188
|
+
task move <id> <status> change a ticket's status — the one general
|
|
1189
|
+
status API (backlog todo in_progress done
|
|
1190
|
+
canceled). The named verbs below exist only
|
|
1191
|
+
where a transition does more than write the
|
|
1192
|
+
status field
|
|
1193
|
+
task promote <id> backlog → todo, the human "build this" call.
|
|
1194
|
+
No quality gate — planning happens at claim
|
|
1195
|
+
time. Blocked / needs-human tickets still
|
|
1196
|
+
promote — those are claim-time gates, reported
|
|
1197
|
+
as a note — so promoting can queue work behind
|
|
1198
|
+
its blockers. Exit codes: 0 promoted, 2
|
|
1199
|
+
refused (not a backlog ticket)
|
|
833
1200
|
task link <id> --blocked-by <id>
|
|
834
1201
|
task link <id> --blocks <id> mark a dependency — one relation, visible from
|
|
835
1202
|
both tasks (A blocked by B ⇔ B blocks A)
|
|
836
1203
|
task unlink <id> (--blocks <id> | --blocked-by <id>)
|
|
837
|
-
task claim <id>
|
|
838
|
-
<
|
|
839
|
-
branch, ticket → in_progress as its
|
|
840
|
-
commit, pushed. The namespace defaults
|
|
841
|
-
task/claim/ — set
|
|
1204
|
+
task claim <id> [--force] claim a ticket before working it: branch
|
|
1205
|
+
<branchPrefix><prefix>-<n> off origin's
|
|
1206
|
+
default branch, ticket → in_progress as its
|
|
1207
|
+
first commit, pushed. The namespace defaults
|
|
1208
|
+
to task/claim/ — set claims.branchPrefix in
|
|
842
1209
|
.task/config.json to change it (e.g.
|
|
843
1210
|
"claude/task/", which Claude cloud sessions
|
|
844
1211
|
can push). Branch on origin = claimed —
|
|
845
1212
|
git's atomic ref creation is the lock, so two
|
|
846
|
-
concurrent claimers can't both win.
|
|
1213
|
+
concurrent claimers can't both win. While the
|
|
1214
|
+
root board sets claims.maxOpenCount and that
|
|
1215
|
+
many claim branches exist on origin, claiming
|
|
1216
|
+
is refused (--force overrides). Exit codes:
|
|
847
1217
|
0 claimed, 1 already claimed, 2 not claimable
|
|
848
|
-
(not todo, blocked, needs-human, dirty tree
|
|
849
|
-
|
|
1218
|
+
(not todo, blocked, needs-human, dirty tree,
|
|
1219
|
+
at the cap)
|
|
1220
|
+
task claim --next [--board <P>] [--force]
|
|
1221
|
+
claim the top claimable ticket in one call,
|
|
1222
|
+
retrying past lost races internally — the
|
|
1223
|
+
first line of every scheduled worker. Scope:
|
|
1224
|
+
--board prefixes (repeatable, given order =
|
|
1225
|
+
priority) > the root board's "boards" list >
|
|
1226
|
+
the nearest board. Exit codes: 0 claimed
|
|
1227
|
+
(prints the ticket), 1 queue empty, 2
|
|
1228
|
+
preconditions failed
|
|
1229
|
+
task claim --release <id> --comment "<why>"
|
|
1230
|
+
abandon a claim: delete the branch on origin
|
|
850
1231
|
and locally — the status flip only lived on
|
|
851
|
-
the branch, so deleting it is the revert
|
|
852
|
-
|
|
1232
|
+
the branch, so deleting it is the revert. The
|
|
1233
|
+
comment is required and lands on the ticket
|
|
1234
|
+
(attributed like task comment, uncommitted),
|
|
1235
|
+
so the next worker inherits what was tried
|
|
1236
|
+
task list --claimable [--board <P>]
|
|
1237
|
+
the claim queue: todo tickets in position
|
|
853
1238
|
order, minus blocked / needs-human / already
|
|
854
|
-
claimed on origin. Top entry is next up
|
|
1239
|
+
claimed on origin. Top entry is next up. Same
|
|
1240
|
+
board scope as claim --next
|
|
1241
|
+
task instructions print the shipped agent conventions
|
|
1242
|
+
(skill/SKILL.md) — agents without the skill
|
|
1243
|
+
installed self-serve the house rules
|
|
1244
|
+
task overview [--json] one screen of board health: counts per
|
|
1245
|
+
status, per-goal progress, claimed and
|
|
1246
|
+
needs-human counts, stale tickets (untouched
|
|
1247
|
+
${STALE_DAYS}+ days), oldest open ticket
|
|
855
1248
|
task comment <id> <text> [--author <who>]
|
|
856
1249
|
task delete <id>
|
|
1250
|
+
task goal add <title> [--slug <s>] [--description <text>]
|
|
1251
|
+
create a goal: a titled, described destination
|
|
1252
|
+
tasks belong to (.task/goals/<slug>.md). The
|
|
1253
|
+
description is the strategic why — the context
|
|
1254
|
+
every ticket under it shares
|
|
1255
|
+
task goal list [--archived] every goal with derived progress (done/total —
|
|
1256
|
+
a goal stores no status of its own, ever)
|
|
1257
|
+
task goal show <slug> the goal, its description, and its tasks
|
|
1258
|
+
task goal update <slug> [--title <t>] [--description <text>]
|
|
1259
|
+
task goal archive <slug> shelve a finished/abandoned goal — refused
|
|
1260
|
+
while it still has open tasks
|
|
1261
|
+
task goal unarchive <slug>
|
|
1262
|
+
task goal delete <slug> only when no live task references it
|
|
857
1263
|
task boards every board in this repo — prefix, name, path,
|
|
858
1264
|
open count; * marks the one commands target here
|
|
859
1265
|
task archive <id> move a done/canceled ticket to .task/archive/,
|
|
@@ -863,6 +1269,15 @@ Usage
|
|
|
863
1269
|
task archive --all archive everything done or canceled
|
|
864
1270
|
task unarchive <id> put an archived ticket back on the board
|
|
865
1271
|
task whoami who your comments are attributed to
|
|
1272
|
+
task check [--fix] lint the board files — merges and hand-edits
|
|
1273
|
+
are write paths no CLI code sees. Reports
|
|
1274
|
+
unparseable files, dangling blocked_by refs,
|
|
1275
|
+
bad timestamps, unknown frontmatter keys and
|
|
1276
|
+
formatting drift, for every board at or below
|
|
1277
|
+
here; exits non-zero while problems remain.
|
|
1278
|
+
--fix applies the mechanical repairs first
|
|
1279
|
+
(prune dangling refs, rewrite to canonical
|
|
1280
|
+
form). Cheap enough for pre-commit and CI
|
|
866
1281
|
task serve [--port <n>] [--no-open] [--strict-port]
|
|
867
1282
|
board + table UI with live updates, opened in
|
|
868
1283
|
your browser (default port ${DEFAULT_PORT}; taken ports
|
|
@@ -880,9 +1295,6 @@ Usage
|
|
|
880
1295
|
task unpublish [--repo <owner/name>]
|
|
881
1296
|
take that URL down. Removes the board, not the
|
|
882
1297
|
tasks — those are in .task/ either way
|
|
883
|
-
task migrate move a pre-0.6 board off its committed SQLite
|
|
884
|
-
database and onto text files in .task/tickets/.
|
|
885
|
-
The database stays on disk as an ignored backup
|
|
886
1298
|
|
|
887
1299
|
Values
|
|
888
1300
|
<id> TAS-12, or just 12. A bare number means the nearest board; a
|
|
@@ -890,9 +1302,12 @@ Values
|
|
|
890
1302
|
prefix, so TAS-12 works from anywhere in a monorepo
|
|
891
1303
|
status ${STATUSES.join(" ")}
|
|
892
1304
|
--tag a,b matches a task carrying *either* tag
|
|
1305
|
+
--goal <slug> a task belongs to at most one goal. Goals answer "where is
|
|
1306
|
+
this going" (one); tags answer "what kind" (many) — something
|
|
1307
|
+
that wants two goals is a tag
|
|
893
1308
|
--needs-human this can't be finished by an agent alone
|
|
894
1309
|
--pr <url> attach a pull request (appends); --prs replaces the whole list
|
|
895
|
-
clearing --tags "" drops all tags, --
|
|
1310
|
+
clearing --tags "" drops all tags, --goal "" clears it, --prs ""
|
|
896
1311
|
detaches all PRs
|
|
897
1312
|
|
|
898
1313
|
Comment authors resolve --author → $TASK_AUTHOR → git config user.name →
|
|
@@ -923,22 +1338,28 @@ function main(): void | Promise<void> {
|
|
|
923
1338
|
return cmdAdd(args)
|
|
924
1339
|
case "list":
|
|
925
1340
|
return cmdList(args)
|
|
1341
|
+
case "search":
|
|
1342
|
+
return cmdSearch(args)
|
|
926
1343
|
case "show":
|
|
927
1344
|
return cmdShow(args)
|
|
928
1345
|
case "update":
|
|
929
1346
|
return cmdUpdate(args)
|
|
930
1347
|
case "move":
|
|
931
1348
|
return cmdMove(args)
|
|
932
|
-
case "
|
|
933
|
-
return
|
|
934
|
-
case "done":
|
|
935
|
-
return cmdUpdate(args, "done")
|
|
1349
|
+
case "promote":
|
|
1350
|
+
return cmdPromote(args)
|
|
936
1351
|
case "link":
|
|
937
1352
|
return cmdLink(args, "link")
|
|
938
1353
|
case "unlink":
|
|
939
1354
|
return cmdLink(args, "unlink")
|
|
940
1355
|
case "claim":
|
|
941
1356
|
return cmdClaim(args)
|
|
1357
|
+
case "instructions":
|
|
1358
|
+
return cmdInstructions()
|
|
1359
|
+
case "overview":
|
|
1360
|
+
return cmdOverview(args)
|
|
1361
|
+
case "goal":
|
|
1362
|
+
return cmdGoal(args)
|
|
942
1363
|
case "comment":
|
|
943
1364
|
return cmdComment(args)
|
|
944
1365
|
case "whoami":
|
|
@@ -951,8 +1372,8 @@ function main(): void | Promise<void> {
|
|
|
951
1372
|
return cmdArchive(args)
|
|
952
1373
|
case "unarchive":
|
|
953
1374
|
return cmdUnarchive(args)
|
|
954
|
-
case "
|
|
955
|
-
return
|
|
1375
|
+
case "check":
|
|
1376
|
+
return cmdCheck(args)
|
|
956
1377
|
case "serve":
|
|
957
1378
|
return cmdServe(args)
|
|
958
1379
|
case "publish":
|