@nickmeriano/task 0.5.0 → 0.7.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 +104 -8
- package/dist/claim.d.ts +76 -0
- package/dist/claim.d.ts.map +1 -0
- package/dist/claim.js +237 -0
- package/dist/claim.js.map +1 -0
- package/dist/claim.test.d.ts +15 -0
- package/dist/claim.test.d.ts.map +1 -0
- package/dist/claim.test.js +185 -0
- package/dist/claim.test.js.map +1 -0
- package/dist/cli.js +304 -17
- package/dist/cli.js.map +1 -1
- package/dist/file-store.d.ts +113 -0
- package/dist/file-store.d.ts.map +1 -0
- package/dist/file-store.js +604 -0
- package/dist/file-store.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +11 -7
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +55 -15
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +63 -29
- package/dist/store.js.map +1 -1
- package/dist/store.test.d.ts +12 -0
- package/dist/store.test.d.ts.map +1 -0
- package/dist/store.test.js +252 -0
- package/dist/store.test.js.map +1 -0
- package/dist/ticket-doc.d.ts +57 -0
- package/dist/ticket-doc.d.ts.map +1 -0
- package/dist/ticket-doc.js +197 -0
- package/dist/ticket-doc.js.map +1 -0
- package/dist/types.d.ts +22 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -4
- package/skill/SKILL.md +45 -8
- package/src/claim.test.ts +242 -0
- package/src/claim.ts +302 -0
- package/src/cli.ts +313 -20
- package/src/file-store.ts +693 -0
- package/src/index.ts +42 -4
- package/src/server.ts +15 -11
- package/src/store.test.ts +305 -0
- package/src/store.ts +99 -40
- package/src/ticket-doc.ts +226 -0
- package/src/types.ts +22 -1
- package/ui/dist/assets/index-CXW8uT5f.css +1 -0
- package/ui/dist/assets/{index-D_qmmh3D.js → index-oJzomUDL.js} +58 -58
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-Da14ye1f.css +0 -1
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// The `task` CLI — tasks that live in your repo. Zero dependencies: hand-rolled
|
|
3
|
-
// flag parsing,
|
|
3
|
+
// flag parsing, plain-text tickets in .task/tickets/ for storage (node:sqlite
|
|
4
|
+
// still reads boards from before `task migrate`), plain text out (--json for
|
|
5
|
+
// agents).
|
|
4
6
|
//
|
|
5
7
|
// task init
|
|
6
8
|
// task add "Wire up webhooks" --tags api,infra --milestone launch
|
|
@@ -13,11 +15,22 @@ import { readFileSync } from "node:fs"
|
|
|
13
15
|
import type { Server } from "node:http"
|
|
14
16
|
import { basename, join } from "node:path"
|
|
15
17
|
import process from "node:process"
|
|
16
|
-
import { resolveAuthor } from "./author.
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
18
|
+
import { resolveAuthor } from "./author.ts"
|
|
19
|
+
import { ClaimError, claim, claimableTasks, release } from "./claim.ts"
|
|
20
|
+
import { detectRepo, parseSlug, publish, resolveHost } from "./publish.ts"
|
|
21
|
+
import { createTaskServer } from "./server.ts"
|
|
22
|
+
import { initProject, migrateBoard, openBoard } from "./file-store.ts"
|
|
23
|
+
import {
|
|
24
|
+
CONFIG_FILE,
|
|
25
|
+
TASK_DIR,
|
|
26
|
+
boardConfig,
|
|
27
|
+
findBoards,
|
|
28
|
+
findBoardsByPrefix,
|
|
29
|
+
findRoot,
|
|
30
|
+
findScopeRoot,
|
|
31
|
+
type Store,
|
|
32
|
+
} from "./store.ts"
|
|
33
|
+
import { STATUSES, isStatus, type Status, type Task, type TaskPatch } from "./types.ts"
|
|
21
34
|
|
|
22
35
|
// node:sqlite still emits an ExperimentalWarning on Node 22 — noise in a CLI
|
|
23
36
|
// that runs it on every invocation. Filter that one warning, keep the rest.
|
|
@@ -62,6 +75,9 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
62
75
|
"yes",
|
|
63
76
|
"needs-human",
|
|
64
77
|
"no-needs-human",
|
|
78
|
+
"archived",
|
|
79
|
+
"claimable",
|
|
80
|
+
"release",
|
|
65
81
|
"open",
|
|
66
82
|
"no-open",
|
|
67
83
|
"strict-port",
|
|
@@ -102,10 +118,68 @@ function fail(message: string): never {
|
|
|
102
118
|
process.exit(1)
|
|
103
119
|
}
|
|
104
120
|
|
|
105
|
-
function openStore():
|
|
121
|
+
function openStore(): Store {
|
|
106
122
|
const root = findRoot(process.cwd())
|
|
107
123
|
if (!root) fail("no .task directory found in this directory or any parent — run `task init` first")
|
|
108
|
-
return
|
|
124
|
+
return openBoard(root)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The prefix a ref carries, if any: "TAS-12" → "TAS", "12" → null. */
|
|
128
|
+
function refPrefix(ref: string): string | null {
|
|
129
|
+
const match = /^([A-Za-z0-9]+)-\d+$/.exec(ref.trim())
|
|
130
|
+
return match ? match[1].toUpperCase() : null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The board a ref belongs to. A bare number means the nearest board, as ever —
|
|
135
|
+
* but a prefixed id is an address, and it routes: if the prefix isn't the
|
|
136
|
+
* nearest board's, every board in the repo (walking up to the outermost board
|
|
137
|
+
* root, then down) is searched for it, so `task show TAS-12` works from
|
|
138
|
+
* anywhere in a monorepo. What this must never do is what it used to: silently
|
|
139
|
+
* strip a foreign prefix and act on the nearest board's ticket of that number.
|
|
140
|
+
*/
|
|
141
|
+
function openStoreFor(ref: string | undefined): Store {
|
|
142
|
+
const prefix = ref ? refPrefix(ref) : null
|
|
143
|
+
if (!prefix) return openStore()
|
|
144
|
+
|
|
145
|
+
const nearest = findRoot(process.cwd())
|
|
146
|
+
if (nearest) {
|
|
147
|
+
const store = openBoard(nearest)
|
|
148
|
+
if (store.config.prefix.toUpperCase() === prefix) return store
|
|
149
|
+
store.close()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const scope = findScopeRoot(process.cwd())
|
|
153
|
+
const matches = findBoardsByPrefix(scope, prefix)
|
|
154
|
+
if (matches.length === 1) return openBoard(matches[0].root)
|
|
155
|
+
if (matches.length > 1) {
|
|
156
|
+
fail(`prefix ${prefix} is ambiguous — boards at: ${matches.map((m) => m.id).join(", ")}`)
|
|
157
|
+
}
|
|
158
|
+
const known = findBoards(scope)
|
|
159
|
+
.map((b) => {
|
|
160
|
+
try {
|
|
161
|
+
return `${boardConfig(b.root).prefix} (${b.id})`
|
|
162
|
+
} catch {
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
.filter(Boolean)
|
|
167
|
+
fail(
|
|
168
|
+
`no board with prefix ${prefix} in this repo${known.length ? ` — boards here: ${known.join(", ")}` : ""}`,
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Parse a ref against an already-chosen board, refusing a foreign prefix
|
|
174
|
+
* instead of reinterpreting it — this guards the second id in `task link`,
|
|
175
|
+
* where the board was picked by the first.
|
|
176
|
+
*/
|
|
177
|
+
function parseRefOn(store: Store, ref: string): number {
|
|
178
|
+
const prefix = refPrefix(ref)
|
|
179
|
+
if (prefix && prefix !== store.config.prefix.toUpperCase()) {
|
|
180
|
+
fail(`${ref} is not on the ${store.config.prefix} board — links can't cross boards`)
|
|
181
|
+
}
|
|
182
|
+
return store.parseId(ref)
|
|
109
183
|
}
|
|
110
184
|
|
|
111
185
|
// ── Input normalization ──────────────────────────────────────────────────────
|
|
@@ -213,10 +287,29 @@ function cmdAdd(args: Args): void {
|
|
|
213
287
|
|
|
214
288
|
function cmdList(args: Args): void {
|
|
215
289
|
const store = openStore()
|
|
290
|
+
if (args.flags.claimable) {
|
|
291
|
+
// The dispatcher's queue view (TAS-21): claimable already pins the status
|
|
292
|
+
// set and consults origin, so the board-shaping flags don't compose.
|
|
293
|
+
for (const flag of ["status", "all", "archived"]) {
|
|
294
|
+
if (args.flags[flag] !== undefined) fail(`--claimable can't be combined with --${flag}`)
|
|
295
|
+
}
|
|
296
|
+
const tasks = claimableTasks(store)
|
|
297
|
+
if (args.flags.json) {
|
|
298
|
+
console.log(JSON.stringify({ tasks }, null, 2))
|
|
299
|
+
} else if (tasks.length === 0) {
|
|
300
|
+
console.log("nothing claimable — no unblocked, unclaimed todo tickets")
|
|
301
|
+
} else {
|
|
302
|
+
table(tasks.map(taskRow))
|
|
303
|
+
}
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
const archived = Boolean(args.flags.archived)
|
|
216
307
|
const statusFlag = str(args.flags, "status")
|
|
217
308
|
const statuses = statusFlag
|
|
218
309
|
? statusFlag.split(",").map(parseStatus)
|
|
219
|
-
:
|
|
310
|
+
: // The archive is all done/canceled, and --all already means everything —
|
|
311
|
+
// the open-tickets default only applies to the plain board listing.
|
|
312
|
+
args.flags.all || archived
|
|
220
313
|
? undefined
|
|
221
314
|
: (["backlog", "todo", "in_progress"] as Status[])
|
|
222
315
|
const tagFlag = str(args.flags, "tags") ?? str(args.flags, "tag")
|
|
@@ -225,6 +318,7 @@ function cmdList(args: Args): void {
|
|
|
225
318
|
tags: tagFlag ? parseTags(tagFlag) : undefined,
|
|
226
319
|
milestone: str(args.flags, "milestone"),
|
|
227
320
|
needsHuman: args.flags["needs-human"] ? true : undefined,
|
|
321
|
+
archived: archived || undefined,
|
|
228
322
|
})
|
|
229
323
|
// Present in board order: grouped by status column, then position.
|
|
230
324
|
const order = new Map(STATUSES.map((s, i) => [s, i]))
|
|
@@ -232,16 +326,109 @@ function cmdList(args: Args): void {
|
|
|
232
326
|
if (args.flags.json) {
|
|
233
327
|
console.log(JSON.stringify({ tasks }, null, 2))
|
|
234
328
|
} else if (tasks.length === 0) {
|
|
235
|
-
console.log(
|
|
329
|
+
console.log(
|
|
330
|
+
archived
|
|
331
|
+
? "no archived tasks"
|
|
332
|
+
: args.flags.all
|
|
333
|
+
? "no tasks"
|
|
334
|
+
: "no open tasks (--all includes done/canceled)",
|
|
335
|
+
)
|
|
236
336
|
} else {
|
|
237
337
|
table(tasks.map(taskRow))
|
|
238
338
|
}
|
|
239
339
|
}
|
|
240
340
|
|
|
341
|
+
/**
|
|
342
|
+
* `task boards` — every board in this repo, found the way `task serve` finds
|
|
343
|
+
* them but anchored at the *outermost* board root, so it answers from anywhere
|
|
344
|
+
* in a monorepo. The `*` marks the board the other commands would target from
|
|
345
|
+
* here.
|
|
346
|
+
*/
|
|
347
|
+
function cmdBoards(args: Args): void {
|
|
348
|
+
const cwd = process.cwd()
|
|
349
|
+
const scope = findScopeRoot(cwd)
|
|
350
|
+
const boards = findBoards(scope)
|
|
351
|
+
if (boards.length === 0) {
|
|
352
|
+
fail("no .task directory found in this directory, any parent, or below — run `task init` first")
|
|
353
|
+
}
|
|
354
|
+
const nearest = findRoot(cwd)
|
|
355
|
+
const rows = boards.map((ref) => {
|
|
356
|
+
const store = openBoard(ref.root)
|
|
357
|
+
const open = store.list({ statuses: ["backlog", "todo", "in_progress"] }).length
|
|
358
|
+
store.close()
|
|
359
|
+
return {
|
|
360
|
+
id: ref.id,
|
|
361
|
+
name: store.config.name,
|
|
362
|
+
prefix: store.config.prefix,
|
|
363
|
+
open,
|
|
364
|
+
current: ref.root === nearest,
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
if (args.flags.json) {
|
|
368
|
+
console.log(JSON.stringify({ boards: rows }, null, 2))
|
|
369
|
+
} else {
|
|
370
|
+
table(
|
|
371
|
+
rows.map((b) => [
|
|
372
|
+
b.current ? "*" : "",
|
|
373
|
+
b.prefix,
|
|
374
|
+
b.name,
|
|
375
|
+
b.id,
|
|
376
|
+
`${b.open} open`,
|
|
377
|
+
]),
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* `task archive <id>` / `task archive --all` — move finished tickets to
|
|
384
|
+
* `.task/archive/`, out of the board and off every hot path. History, not a
|
|
385
|
+
* hiding place: only done/canceled tickets qualify, everything stays readable
|
|
386
|
+
* via `show` and `list --archived`, and `task unarchive` puts one back.
|
|
387
|
+
*/
|
|
388
|
+
function cmdArchive(args: Args): void {
|
|
389
|
+
const ref = args.positional[0]
|
|
390
|
+
if (args.flags.all && ref) fail("pass an id or --all, not both")
|
|
391
|
+
if (!args.flags.all && !ref) fail("usage: task archive <id> | task archive --all")
|
|
392
|
+
|
|
393
|
+
if (ref) {
|
|
394
|
+
const store = openStoreFor(ref)
|
|
395
|
+
const task = store.archive(store.parseId(ref))
|
|
396
|
+
if (args.flags.json) {
|
|
397
|
+
console.log(JSON.stringify({ task }, null, 2))
|
|
398
|
+
} else {
|
|
399
|
+
console.log(`archived ${task.id} ${task.title}`)
|
|
400
|
+
}
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const store = openStore()
|
|
405
|
+
const finished = store.list({ statuses: ["done", "canceled"] })
|
|
406
|
+
const archived = finished.map((t) => store.archive(t.number))
|
|
407
|
+
if (args.flags.json) {
|
|
408
|
+
console.log(JSON.stringify({ archived }, null, 2))
|
|
409
|
+
} else if (archived.length === 0) {
|
|
410
|
+
console.log("nothing to archive — no done or canceled tasks on the board")
|
|
411
|
+
} else {
|
|
412
|
+
for (const task of archived) console.log(`archived ${task.id} ${task.title}`)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function cmdUnarchive(args: Args): void {
|
|
417
|
+
const ref = args.positional[0]
|
|
418
|
+
if (!ref) fail("usage: task unarchive <id>")
|
|
419
|
+
const store = openStoreFor(ref)
|
|
420
|
+
const task = store.unarchive(store.parseId(ref))
|
|
421
|
+
if (args.flags.json) {
|
|
422
|
+
console.log(JSON.stringify({ task }, null, 2))
|
|
423
|
+
} else {
|
|
424
|
+
printTask(task)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
241
428
|
function cmdShow(args: Args): void {
|
|
242
429
|
const ref = args.positional[0]
|
|
243
430
|
if (!ref) fail("usage: task show <id>")
|
|
244
|
-
const store =
|
|
431
|
+
const store = openStoreFor(ref)
|
|
245
432
|
const number = store.parseId(ref)
|
|
246
433
|
const task = store.get(number)
|
|
247
434
|
if (!task) fail(`no such task: ${store.displayId(number)}`)
|
|
@@ -260,6 +447,7 @@ function cmdShow(args: Args): void {
|
|
|
260
447
|
.join(", ")
|
|
261
448
|
console.log(`${task.id} ${task.title}`)
|
|
262
449
|
console.log(`status ${task.status}`)
|
|
450
|
+
if (task.archived) console.log(`archived yes — \`task unarchive ${task.id}\` to edit`)
|
|
263
451
|
if (task.needsHuman) console.log(`needs a human`)
|
|
264
452
|
if (task.tags.length) console.log(`tags ${task.tags.join(", ")}`)
|
|
265
453
|
if (task.milestone) console.log(`milestone ${task.milestone}`)
|
|
@@ -281,7 +469,7 @@ function cmdShow(args: Args): void {
|
|
|
281
469
|
function cmdUpdate(args: Args, forcedStatus?: Status): void {
|
|
282
470
|
const ref = args.positional[0]
|
|
283
471
|
if (!ref) fail("usage: task update <id> [--status …] [--title …] …")
|
|
284
|
-
const store =
|
|
472
|
+
const store = openStoreFor(ref)
|
|
285
473
|
const number = store.parseId(ref)
|
|
286
474
|
const patch = patchFromFlags(args.flags)
|
|
287
475
|
if (forcedStatus) patch.status = forcedStatus
|
|
@@ -310,7 +498,7 @@ function cmdComment(args: Args): void {
|
|
|
310
498
|
const [ref, ...rest] = args.positional
|
|
311
499
|
const body = rest.join(" ").trim()
|
|
312
500
|
if (!ref || !body) fail(`usage: task comment <id> <text> [--author <who>]`)
|
|
313
|
-
const store =
|
|
501
|
+
const store = openStoreFor(ref)
|
|
314
502
|
const author = resolveAuthor(str(args.flags, "author")).name
|
|
315
503
|
const comment = store.addComment(store.parseId(ref), body, author)
|
|
316
504
|
if (args.flags.json) {
|
|
@@ -332,9 +520,9 @@ function cmdLink(args: Args, action: "link" | "unlink"): void {
|
|
|
332
520
|
if (!ref || (blocks ? blockedBy : !blockedBy)) {
|
|
333
521
|
fail(`usage: task ${action} <id> (--blocks <id> | --blocked-by <id>)`)
|
|
334
522
|
}
|
|
335
|
-
const store =
|
|
523
|
+
const store = openStoreFor(ref)
|
|
336
524
|
const relation = blocks ? "blocks" : "blocked_by"
|
|
337
|
-
const target = store
|
|
525
|
+
const target = parseRefOn(store, (blocks ?? blockedBy)!)
|
|
338
526
|
const number = store.parseId(ref)
|
|
339
527
|
const task =
|
|
340
528
|
action === "link" ? store.link(number, relation, target) : store.unlink(number, relation, target)
|
|
@@ -350,6 +538,44 @@ function cmdLink(args: Args, action: "link" | "unlink"): void {
|
|
|
350
538
|
}
|
|
351
539
|
}
|
|
352
540
|
|
|
541
|
+
/**
|
|
542
|
+
* `task claim <id>` / `task claim --release <id>` — see claim.ts for the
|
|
543
|
+
* mechanics. Exit codes are the contract callers script against: 0 claimed,
|
|
544
|
+
* 1 already claimed (pick the next ticket), 2 not claimable (fix something).
|
|
545
|
+
*/
|
|
546
|
+
function cmdClaim(args: Args): void {
|
|
547
|
+
const ref = args.positional[0]
|
|
548
|
+
if (!ref) fail("usage: task claim <id> | task claim --release <id>")
|
|
549
|
+
const store = openStoreFor(ref)
|
|
550
|
+
const number = store.parseId(ref)
|
|
551
|
+
try {
|
|
552
|
+
if (args.flags.release) {
|
|
553
|
+
const result = release(store, number)
|
|
554
|
+
if (args.flags.json) {
|
|
555
|
+
console.log(JSON.stringify({ released: result }, null, 2))
|
|
556
|
+
} else if (!result.remote && !result.local) {
|
|
557
|
+
console.log(`${store.displayId(number)} wasn't claimed — no ${result.branch} to delete`)
|
|
558
|
+
} else {
|
|
559
|
+
const where = [result.remote && "origin", result.local && "local"].filter(Boolean)
|
|
560
|
+
console.log(`released ${store.displayId(number)} — deleted ${result.branch} (${where.join(" and ")})`)
|
|
561
|
+
}
|
|
562
|
+
return
|
|
563
|
+
}
|
|
564
|
+
const result = claim(store, number)
|
|
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
|
+
}
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (error instanceof ClaimError) {
|
|
572
|
+
console.error(`error: ${error.message}`)
|
|
573
|
+
process.exit(error.kind === "claimed" ? 1 : 2)
|
|
574
|
+
}
|
|
575
|
+
throw error
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
353
579
|
const AUTHOR_SOURCE: Record<string, string> = {
|
|
354
580
|
flag: "--author",
|
|
355
581
|
env: "$TASK_AUTHOR",
|
|
@@ -367,10 +593,34 @@ function cmdWhoami(args: Args): void {
|
|
|
367
593
|
}
|
|
368
594
|
}
|
|
369
595
|
|
|
596
|
+
/**
|
|
597
|
+
* `task migrate` — from the legacy committed SQLite database to text-canonical
|
|
598
|
+
* storage: one markdown file per ticket and per comment under .task/tickets/,
|
|
599
|
+
* with the database left on disk as an ignored backup. Additive and safe to
|
|
600
|
+
* re-run planning-wise: it refuses to run twice.
|
|
601
|
+
*/
|
|
602
|
+
function cmdMigrate(args: Args): void {
|
|
603
|
+
const root = findRoot(process.cwd())
|
|
604
|
+
if (!root) fail("no .task directory found in this directory or any parent — run `task init` first")
|
|
605
|
+
const result = migrateBoard(root)
|
|
606
|
+
if (args.flags.json) {
|
|
607
|
+
console.log(JSON.stringify({ migrated: result }, null, 2))
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
console.log(
|
|
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`)
|
|
618
|
+
}
|
|
619
|
+
|
|
370
620
|
function cmdDelete(args: Args): void {
|
|
371
621
|
const ref = args.positional[0]
|
|
372
622
|
if (!ref) fail("usage: task delete <id>")
|
|
373
|
-
const store =
|
|
623
|
+
const store = openStoreFor(ref)
|
|
374
624
|
const number = store.parseId(ref)
|
|
375
625
|
store.delete(number)
|
|
376
626
|
console.log(`deleted ${store.displayId(number)}`)
|
|
@@ -560,8 +810,10 @@ function runPublishFlow(
|
|
|
560
810
|
|
|
561
811
|
const HELP = `task — a task manager that lives in your repo
|
|
562
812
|
|
|
563
|
-
State is
|
|
564
|
-
|
|
813
|
+
State is plain text in .task/ at the project root — one markdown file per
|
|
814
|
+
ticket and per comment under .task/tickets/. Commit it: tasks travel with the
|
|
815
|
+
code they describe, ticket changes show up as readable diffs in PRs, and two
|
|
816
|
+
branches editing different tickets merge cleanly. Any command works from any
|
|
565
817
|
subdirectory (it walks up to find .task/, like git).
|
|
566
818
|
|
|
567
819
|
Usage
|
|
@@ -569,7 +821,7 @@ Usage
|
|
|
569
821
|
task add <title> [--description <text>] [--status <s>] [--tags <a,b>]
|
|
570
822
|
[--milestone <m>] [--needs-human]
|
|
571
823
|
task list [--status <s1,s2>] [--tag <a,b>] [--milestone <m>]
|
|
572
|
-
[--needs-human] [--all]
|
|
824
|
+
[--needs-human] [--all] [--archived] [--claimable]
|
|
573
825
|
task show <id>
|
|
574
826
|
task update <id> [--title <t>] [--description <text>] [--status <s>]
|
|
575
827
|
[--tags <a,b>] [--milestone <m>]
|
|
@@ -582,8 +834,34 @@ Usage
|
|
|
582
834
|
task link <id> --blocks <id> mark a dependency — one relation, visible from
|
|
583
835
|
both tasks (A blocked by B ⇔ B blocks A)
|
|
584
836
|
task unlink <id> (--blocks <id> | --blocked-by <id>)
|
|
837
|
+
task claim <id> claim a ticket before working it: branch
|
|
838
|
+
<claimPrefix><prefix>-<n> off origin's default
|
|
839
|
+
branch, ticket → in_progress as its first
|
|
840
|
+
commit, pushed. The namespace defaults to
|
|
841
|
+
task/claim/ — set "claimPrefix" in
|
|
842
|
+
.task/config.json to change it (e.g.
|
|
843
|
+
"claude/task/", which Claude cloud sessions
|
|
844
|
+
can push). Branch on origin = claimed —
|
|
845
|
+
git's atomic ref creation is the lock, so two
|
|
846
|
+
concurrent claimers can't both win. Exit codes:
|
|
847
|
+
0 claimed, 1 already claimed, 2 not claimable
|
|
848
|
+
(not todo, blocked, needs-human, dirty tree)
|
|
849
|
+
task claim --release <id> abandon a claim: delete the branch on origin
|
|
850
|
+
and locally — the status flip only lived on
|
|
851
|
+
the branch, so deleting it is the revert
|
|
852
|
+
task list --claimable the claim queue: todo tickets in position
|
|
853
|
+
order, minus blocked / needs-human / already
|
|
854
|
+
claimed on origin. Top entry is next up
|
|
585
855
|
task comment <id> <text> [--author <who>]
|
|
586
856
|
task delete <id>
|
|
857
|
+
task boards every board in this repo — prefix, name, path,
|
|
858
|
+
open count; * marks the one commands target here
|
|
859
|
+
task archive <id> move a done/canceled ticket to .task/archive/,
|
|
860
|
+
out of the board and off the hot path — still
|
|
861
|
+
readable via show and list --archived, and its
|
|
862
|
+
number stays reserved
|
|
863
|
+
task archive --all archive everything done or canceled
|
|
864
|
+
task unarchive <id> put an archived ticket back on the board
|
|
587
865
|
task whoami who your comments are attributed to
|
|
588
866
|
task serve [--port <n>] [--no-open] [--strict-port]
|
|
589
867
|
board + table UI with live updates, opened in
|
|
@@ -602,9 +880,14 @@ Usage
|
|
|
602
880
|
task unpublish [--repo <owner/name>]
|
|
603
881
|
take that URL down. Removes the board, not the
|
|
604
882
|
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
|
|
605
886
|
|
|
606
887
|
Values
|
|
607
|
-
<id> TAS-12, or just 12
|
|
888
|
+
<id> TAS-12, or just 12. A bare number means the nearest board; a
|
|
889
|
+
prefixed id routes to whichever board in the repo owns that
|
|
890
|
+
prefix, so TAS-12 works from anywhere in a monorepo
|
|
608
891
|
status ${STATUSES.join(" ")}
|
|
609
892
|
--tag a,b matches a task carrying *either* tag
|
|
610
893
|
--needs-human this can't be finished by an agent alone
|
|
@@ -654,12 +937,22 @@ function main(): void | Promise<void> {
|
|
|
654
937
|
return cmdLink(args, "link")
|
|
655
938
|
case "unlink":
|
|
656
939
|
return cmdLink(args, "unlink")
|
|
940
|
+
case "claim":
|
|
941
|
+
return cmdClaim(args)
|
|
657
942
|
case "comment":
|
|
658
943
|
return cmdComment(args)
|
|
659
944
|
case "whoami":
|
|
660
945
|
return cmdWhoami(args)
|
|
661
946
|
case "delete":
|
|
662
947
|
return cmdDelete(args)
|
|
948
|
+
case "boards":
|
|
949
|
+
return cmdBoards(args)
|
|
950
|
+
case "archive":
|
|
951
|
+
return cmdArchive(args)
|
|
952
|
+
case "unarchive":
|
|
953
|
+
return cmdUnarchive(args)
|
|
954
|
+
case "migrate":
|
|
955
|
+
return cmdMigrate(args)
|
|
663
956
|
case "serve":
|
|
664
957
|
return cmdServe(args)
|
|
665
958
|
case "publish":
|