@nickmeriano/task 0.8.0 → 0.10.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 +137 -22
- package/dist/asks.test.d.ts +17 -0
- package/dist/asks.test.d.ts.map +1 -0
- package/dist/asks.test.js +278 -0
- package/dist/asks.test.js.map +1 -0
- package/dist/check.d.ts +3 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +99 -60
- package/dist/check.js.map +1 -1
- package/dist/check.test.js +99 -60
- package/dist/check.test.js.map +1 -1
- package/dist/claim-io.d.ts +73 -0
- package/dist/claim-io.d.ts.map +1 -0
- package/dist/claim-io.js +344 -0
- package/dist/claim-io.js.map +1 -0
- package/dist/claim.d.ts +108 -23
- package/dist/claim.d.ts.map +1 -1
- package/dist/claim.js +320 -86
- package/dist/claim.js.map +1 -1
- package/dist/claim.test.d.ts +10 -2
- package/dist/claim.test.d.ts.map +1 -1
- package/dist/claim.test.js +282 -96
- package/dist/claim.test.js.map +1 -1
- package/dist/cli.js +346 -68
- package/dist/cli.js.map +1 -1
- package/dist/file-store.d.ts +70 -24
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +319 -141
- package/dist/file-store.js.map +1 -1
- package/dist/git-serve.d.ts +183 -0
- package/dist/git-serve.d.ts.map +1 -0
- package/dist/git-serve.js +503 -0
- package/dist/git-serve.js.map +1 -0
- package/dist/git-serve.test.d.ts +16 -0
- package/dist/git-serve.test.d.ts.map +1 -0
- package/dist/git-serve.test.js +183 -0
- package/dist/git-serve.test.js.map +1 -0
- package/dist/git.d.ts +65 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +114 -0
- package/dist/git.js.map +1 -0
- package/dist/id.d.ts +39 -0
- package/dist/id.d.ts.map +1 -0
- package/dist/id.js +67 -0
- package/dist/id.js.map +1 -0
- package/dist/inbox.d.ts +41 -0
- package/dist/inbox.d.ts.map +1 -0
- package/dist/inbox.js +56 -0
- package/dist/inbox.js.map +1 -0
- package/dist/overview.d.ts +9 -0
- package/dist/overview.d.ts.map +1 -1
- package/dist/overview.js +19 -11
- package/dist/overview.js.map +1 -1
- package/dist/overview.test.js +6 -5
- package/dist/overview.test.js.map +1 -1
- package/dist/promote.test.d.ts +3 -2
- package/dist/promote.test.d.ts.map +1 -1
- package/dist/promote.test.js +29 -31
- package/dist/promote.test.js.map +1 -1
- package/dist/publish.d.ts +2 -17
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +4 -49
- package/dist/publish.js.map +1 -1
- package/dist/search.d.ts +3 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +15 -2
- package/dist/search.js.map +1 -1
- package/dist/search.test.js +9 -9
- package/dist/search.test.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +218 -21
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +28 -14
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js.map +1 -1
- package/dist/store.test.js +88 -75
- package/dist/store.test.js.map +1 -1
- package/dist/ticket-doc.d.ts +26 -10
- package/dist/ticket-doc.d.ts.map +1 -1
- package/dist/ticket-doc.js +71 -27
- package/dist/ticket-doc.js.map +1 -1
- package/dist/types.d.ts +51 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +96 -24
- package/src/asks.test.ts +355 -0
- package/src/check.test.ts +129 -72
- package/src/check.ts +101 -58
- package/src/claim-io.ts +401 -0
- package/src/claim.test.ts +372 -101
- package/src/claim.ts +418 -99
- package/src/cli.ts +370 -67
- package/src/file-store.ts +357 -145
- package/src/git-serve.test.ts +240 -0
- package/src/git-serve.ts +595 -0
- package/src/git.ts +141 -0
- package/src/id.ts +68 -0
- package/src/inbox.ts +77 -0
- package/src/overview.test.ts +6 -5
- package/src/overview.ts +25 -10
- package/src/promote.test.ts +40 -28
- package/src/publish.ts +6 -53
- package/src/search.test.ts +11 -11
- package/src/search.ts +17 -3
- package/src/server.ts +221 -19
- package/src/store.test.ts +95 -77
- package/src/store.ts +28 -13
- package/src/ticket-doc.ts +93 -30
- package/src/types.ts +52 -17
- package/ui/dist/assets/index-BjsorZOU.js +229 -0
- package/ui/dist/assets/index-CoKCUYic.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-BJmOsOdR.js +0 -229
- package/ui/dist/assets/index-BoqQlqSU.css +0 -1
package/src/cli.ts
CHANGED
|
@@ -6,25 +6,29 @@
|
|
|
6
6
|
// task init
|
|
7
7
|
// task add "Wire up webhooks" --tags api,infra --goal launch
|
|
8
8
|
// task list --status todo,in_progress
|
|
9
|
-
// task move TAS-
|
|
9
|
+
// task move TAS-x7k4m done
|
|
10
10
|
// task serve
|
|
11
11
|
|
|
12
12
|
import { spawn } from "node:child_process"
|
|
13
13
|
import { readFileSync } from "node:fs"
|
|
14
14
|
import type { Server } from "node:http"
|
|
15
|
-
import { basename, join } from "node:path"
|
|
15
|
+
import { basename, join, relative, sep } from "node:path"
|
|
16
16
|
import process from "node:process"
|
|
17
17
|
import { resolveAuthor } from "./author.ts"
|
|
18
18
|
import { checkBoards, checkRoot } from "./check.ts"
|
|
19
19
|
import {
|
|
20
20
|
ClaimError,
|
|
21
21
|
claim,
|
|
22
|
+
claimLockOnly,
|
|
22
23
|
claimNext,
|
|
23
24
|
claimableTasks,
|
|
24
25
|
promote,
|
|
25
26
|
release,
|
|
26
27
|
selectionBoards,
|
|
28
|
+
sweep,
|
|
27
29
|
} from "./claim.ts"
|
|
30
|
+
import { addAskRouted, setAskResolvedRouted } from "./claim-io.ts"
|
|
31
|
+
import { buildInbox, type InboxEntry } from "./inbox.ts"
|
|
28
32
|
import { STALE_DAYS, buildOverview } from "./overview.ts"
|
|
29
33
|
import { searchStore } from "./search.ts"
|
|
30
34
|
import { detectRepo, parseSlug, publish, resolveHost } from "./publish.ts"
|
|
@@ -66,6 +70,8 @@ const PORT_ATTEMPTS = 20
|
|
|
66
70
|
interface Args {
|
|
67
71
|
positional: string[]
|
|
68
72
|
flags: Record<string, string | boolean>
|
|
73
|
+
/** Values of MULTI_FLAGS, in given order — e.g. multi.ask from `--ask "…"`. */
|
|
74
|
+
multi: Record<string, string[]>
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
// Every valueless flag has to be listed here: an unregistered one swallows the
|
|
@@ -77,7 +83,6 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
77
83
|
"version",
|
|
78
84
|
"yes",
|
|
79
85
|
"needs-human",
|
|
80
|
-
"no-needs-human",
|
|
81
86
|
"archived",
|
|
82
87
|
"claimable",
|
|
83
88
|
"release",
|
|
@@ -90,6 +95,8 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
90
95
|
"fix",
|
|
91
96
|
"next",
|
|
92
97
|
"force",
|
|
98
|
+
"reopen",
|
|
99
|
+
"lock-only",
|
|
93
100
|
])
|
|
94
101
|
|
|
95
102
|
/**
|
|
@@ -99,14 +106,26 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
99
106
|
*/
|
|
100
107
|
const REPEATABLE_FLAGS = new Set(["board"])
|
|
101
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Repeatable flags whose values are free text — collected verbatim into a
|
|
111
|
+
* list, never comma-joined, because an ask like "check A, then B" must
|
|
112
|
+
* survive as one ask.
|
|
113
|
+
*/
|
|
114
|
+
const MULTI_FLAGS = new Set(["ask"])
|
|
115
|
+
|
|
102
116
|
function parseArgs(argv: string[]): Args {
|
|
103
117
|
const positional: string[] = []
|
|
104
118
|
const flags: Record<string, string | boolean> = {}
|
|
119
|
+
const multi: Record<string, string[]> = {}
|
|
105
120
|
for (let i = 0; i < argv.length; i++) {
|
|
106
121
|
const arg = argv[i]
|
|
107
122
|
if (arg.startsWith("--")) {
|
|
108
123
|
const name = arg.slice(2)
|
|
109
124
|
const set = (key: string, value: string): void => {
|
|
125
|
+
if (MULTI_FLAGS.has(key)) {
|
|
126
|
+
;(multi[key] ??= []).push(value)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
110
129
|
const prior = flags[key]
|
|
111
130
|
flags[key] =
|
|
112
131
|
REPEATABLE_FLAGS.has(key) && typeof prior === "string" ? `${prior},${value}` : value
|
|
@@ -123,7 +142,7 @@ function parseArgs(argv: string[]): Args {
|
|
|
123
142
|
positional.push(arg)
|
|
124
143
|
}
|
|
125
144
|
}
|
|
126
|
-
return { positional, flags }
|
|
145
|
+
return { positional, flags, multi }
|
|
127
146
|
}
|
|
128
147
|
|
|
129
148
|
function str(flags: Args["flags"], name: string): string | undefined {
|
|
@@ -142,19 +161,19 @@ function openStore(): Store {
|
|
|
142
161
|
return openBoard(root)
|
|
143
162
|
}
|
|
144
163
|
|
|
145
|
-
/** The prefix a ref carries, if any: "TAS-
|
|
164
|
+
/** The prefix a ref carries, if any: "TAS-x7k4m" → "TAS", "x7k4m" → null. */
|
|
146
165
|
function refPrefix(ref: string): string | null {
|
|
147
|
-
const match = /^([A-Za-z0-9]+)
|
|
166
|
+
const match = /^([A-Za-z0-9]+)-[A-Za-z0-9]+$/.exec(ref.trim())
|
|
148
167
|
return match ? match[1].toUpperCase() : null
|
|
149
168
|
}
|
|
150
169
|
|
|
151
170
|
/**
|
|
152
|
-
* The board a ref belongs to. A bare
|
|
171
|
+
* The board a ref belongs to. A bare key means the nearest board, as ever —
|
|
153
172
|
* but a prefixed id is an address, and it routes: if the prefix isn't the
|
|
154
173
|
* nearest board's, every board in the repo (walking up to the outermost board
|
|
155
|
-
* root, then down) is searched for it, so `task show TAS-
|
|
174
|
+
* root, then down) is searched for it, so `task show TAS-x7k4m` works from
|
|
156
175
|
* anywhere in a monorepo. What this must never do is what it used to: silently
|
|
157
|
-
* strip a foreign prefix and act on the nearest board's ticket of that
|
|
176
|
+
* strip a foreign prefix and act on the nearest board's ticket of that key.
|
|
158
177
|
*/
|
|
159
178
|
function openStoreFor(ref: string | undefined): Store {
|
|
160
179
|
const prefix = ref ? refPrefix(ref) : null
|
|
@@ -192,7 +211,7 @@ function openStoreFor(ref: string | undefined): Store {
|
|
|
192
211
|
* instead of reinterpreting it — this guards the second id in `task link`,
|
|
193
212
|
* where the board was picked by the first.
|
|
194
213
|
*/
|
|
195
|
-
function parseRefOn(store: Store, ref: string):
|
|
214
|
+
function parseRefOn(store: Store, ref: string): string {
|
|
196
215
|
const prefix = refPrefix(ref)
|
|
197
216
|
if (prefix && prefix !== store.config.prefix.toUpperCase()) {
|
|
198
217
|
fail(`${ref} is not on the ${store.config.prefix} board — links can't cross boards`)
|
|
@@ -238,8 +257,6 @@ function patchFromFlags(flags: Args["flags"]): TaskPatch {
|
|
|
238
257
|
if (tags !== undefined) patch.tags = parseTags(tags)
|
|
239
258
|
const goal = str(flags, "goal")
|
|
240
259
|
if (goal !== undefined) patch.goal = goal.trim() || null
|
|
241
|
-
if (flags["needs-human"]) patch.needsHuman = true
|
|
242
|
-
if (flags["no-needs-human"]) patch.needsHuman = false
|
|
243
260
|
// Replace semantics like --tags; `--pr` (append one) is handled per-command
|
|
244
261
|
// because appending needs the task's current list.
|
|
245
262
|
const prs = str(flags, "prs")
|
|
@@ -304,16 +321,23 @@ function cmdInit(args: Args): void {
|
|
|
304
321
|
|
|
305
322
|
function cmdAdd(args: Args): void {
|
|
306
323
|
const title = args.positional.join(" ").trim()
|
|
307
|
-
if (!title) fail(`usage: task add <title> [--description …] [--status …] [--tags a,b] [--goal <slug>] [--
|
|
324
|
+
if (!title) fail(`usage: task add <title> [--description …] [--status …] [--tags a,b] [--goal <slug>] [--ask "<text>"]…`)
|
|
308
325
|
const store = openStore()
|
|
309
326
|
const patch = patchFromFlags(args.flags)
|
|
310
327
|
const pr = str(args.flags, "pr")
|
|
311
328
|
if (pr) patch.prs = [...(patch.prs ?? []), pr]
|
|
312
|
-
|
|
329
|
+
let task = store.create({ title, ...patch })
|
|
330
|
+
const asks = (args.multi.ask ?? []).map((a) => a.trim()).filter(Boolean)
|
|
331
|
+
if (asks.length > 0) {
|
|
332
|
+
const author = resolveAuthor(str(args.flags, "author")).name
|
|
333
|
+
for (const text of asks) store.addAsk(task.key, text, author)
|
|
334
|
+
task = store.get(task.key)!
|
|
335
|
+
}
|
|
313
336
|
if (args.flags.json) {
|
|
314
337
|
console.log(JSON.stringify({ task }, null, 2))
|
|
315
338
|
} else {
|
|
316
339
|
printTask(task)
|
|
340
|
+
for (const ask of task.asks) console.log(` ask [${ask.ordinal}] ${ask.text.split("\n")[0]}`)
|
|
317
341
|
}
|
|
318
342
|
}
|
|
319
343
|
|
|
@@ -500,7 +524,7 @@ function cmdArchive(args: Args): void {
|
|
|
500
524
|
|
|
501
525
|
const store = openStore()
|
|
502
526
|
const finished = store.list({ statuses: ["done", "canceled"] })
|
|
503
|
-
const archived = finished.map((t) => store.archive(t.
|
|
527
|
+
const archived = finished.map((t) => store.archive(t.key))
|
|
504
528
|
if (args.flags.json) {
|
|
505
529
|
console.log(JSON.stringify({ archived }, null, 2))
|
|
506
530
|
} else if (archived.length === 0) {
|
|
@@ -526,10 +550,10 @@ function cmdShow(args: Args): void {
|
|
|
526
550
|
const ref = args.positional[0]
|
|
527
551
|
if (!ref) fail("usage: task show <id>")
|
|
528
552
|
const store = openStoreFor(ref)
|
|
529
|
-
const
|
|
530
|
-
const task = store.get(
|
|
531
|
-
if (!task) fail(`no such task: ${store.displayId(
|
|
532
|
-
const comments = store.comments(
|
|
553
|
+
const key = store.parseId(ref)
|
|
554
|
+
const task = store.get(key)
|
|
555
|
+
if (!task) fail(`no such task: ${store.displayId(key)}`)
|
|
556
|
+
const comments = store.comments(key)
|
|
533
557
|
// The goal rides along in full — title *and* description — so an agent
|
|
534
558
|
// picking the ticket up cold inherits the shared context without it being
|
|
535
559
|
// pasted into every ticket. null when unset or when the ref dangles.
|
|
@@ -539,17 +563,27 @@ function cmdShow(args: Args): void {
|
|
|
539
563
|
return
|
|
540
564
|
}
|
|
541
565
|
// A linked task is only as useful as knowing whether it's still in the way.
|
|
542
|
-
const describeLinks = (
|
|
543
|
-
|
|
544
|
-
.map((
|
|
545
|
-
const other = store.get(
|
|
546
|
-
return other ? `${other.id} (${other.status})` : store.displayId(
|
|
566
|
+
const describeLinks = (keys: string[]): string =>
|
|
567
|
+
keys
|
|
568
|
+
.map((k) => {
|
|
569
|
+
const other = store.get(k)
|
|
570
|
+
return other ? `${other.id} (${other.status})` : store.displayId(k)
|
|
547
571
|
})
|
|
548
572
|
.join(", ")
|
|
549
573
|
console.log(`${task.id} ${task.title}`)
|
|
550
574
|
console.log(`status ${task.status}`)
|
|
551
575
|
if (task.archived) console.log(`archived yes — \`task unarchive ${task.id}\` to edit`)
|
|
552
|
-
|
|
576
|
+
const openAsks = task.asks.filter((a) => !a.resolvedAt)
|
|
577
|
+
if (openAsks.length) {
|
|
578
|
+
console.log(`needs a human — ${openAsks.length} open ask${openAsks.length === 1 ? "" : "s"}`)
|
|
579
|
+
}
|
|
580
|
+
for (const ask of task.asks) {
|
|
581
|
+
const state = ask.resolvedAt
|
|
582
|
+
? `✓ resolved${ask.resolvedBy ? ` by ${ask.resolvedBy}` : ""}`
|
|
583
|
+
: `○ open${ask.author ? ` · ${ask.author}` : ""} · ${ask.createdAt.slice(0, 10)}`
|
|
584
|
+
console.log(`ask [${ask.ordinal}] ${ask.text.split("\n").join("\n ")}`)
|
|
585
|
+
console.log(` ${state}`)
|
|
586
|
+
}
|
|
553
587
|
if (task.tags.length) console.log(`tags ${task.tags.join(", ")}`)
|
|
554
588
|
if (task.goal) {
|
|
555
589
|
console.log(`goal ${task.goal}${goal ? ` — ${goal.title}${goal.archived ? " (archived)" : ""}` : ""}`)
|
|
@@ -577,16 +611,16 @@ function cmdUpdate(args: Args): void {
|
|
|
577
611
|
const ref = args.positional[0]
|
|
578
612
|
if (!ref) fail("usage: task update <id> [--status …] [--title …] …")
|
|
579
613
|
const store = openStoreFor(ref)
|
|
580
|
-
const
|
|
614
|
+
const key = store.parseId(ref)
|
|
581
615
|
const patch = patchFromFlags(args.flags)
|
|
582
616
|
const pr = str(args.flags, "pr")
|
|
583
617
|
if (pr) {
|
|
584
618
|
// Append, dedup — `--pr <url>` is "attach this PR", not "replace the list".
|
|
585
|
-
const current = patch.prs ?? store.get(
|
|
619
|
+
const current = patch.prs ?? store.get(key)?.prs ?? []
|
|
586
620
|
patch.prs = current.includes(pr) ? current : [...current, pr]
|
|
587
621
|
}
|
|
588
622
|
if (Object.keys(patch).length === 0) fail("nothing to update — pass at least one flag")
|
|
589
|
-
const task = store.update(
|
|
623
|
+
const task = store.update(key, patch)
|
|
590
624
|
if (args.flags.json) {
|
|
591
625
|
console.log(JSON.stringify({ task }, null, 2))
|
|
592
626
|
} else {
|
|
@@ -597,7 +631,7 @@ function cmdUpdate(args: Args): void {
|
|
|
597
631
|
function cmdMove(args: Args): void {
|
|
598
632
|
const [ref, status] = args.positional
|
|
599
633
|
if (!ref || !status) fail("usage: task move <id> <status>")
|
|
600
|
-
cmdUpdate({ positional: [ref], flags: { ...args.flags, status } })
|
|
634
|
+
cmdUpdate({ positional: [ref], flags: { ...args.flags, status }, multi: {} })
|
|
601
635
|
}
|
|
602
636
|
|
|
603
637
|
/**
|
|
@@ -628,6 +662,129 @@ function cmdPromote(args: Args): void {
|
|
|
628
662
|
}
|
|
629
663
|
}
|
|
630
664
|
|
|
665
|
+
/**
|
|
666
|
+
* `task ask <id> <text>` — file one thing a person owes this ticket. The ask
|
|
667
|
+
* is the primitive every human requirement funnels through: it derives the
|
|
668
|
+
* needs-human badge, feeds `task inbox`, and gates `done` until resolved.
|
|
669
|
+
* On a claimed ticket the write lands on the claim branch as a pushed commit
|
|
670
|
+
* (see claim-io.ts) — the worker's done gate has to see debt filed mid-claim.
|
|
671
|
+
*/
|
|
672
|
+
function cmdAsk(args: Args): void {
|
|
673
|
+
const [ref, ...rest] = args.positional
|
|
674
|
+
const text = rest.join(" ").trim()
|
|
675
|
+
if (!ref || !text) fail(`usage: task ask <id> "<what a person needs to do>" [--author <who>]`)
|
|
676
|
+
const store = openStoreFor(ref)
|
|
677
|
+
const author = resolveAuthor(str(args.flags, "author")).name
|
|
678
|
+
const { ask, branch } = addAskRouted(store, store.parseId(ref), text, author)
|
|
679
|
+
if (args.flags.json) {
|
|
680
|
+
console.log(JSON.stringify({ ask, branch }, null, 2))
|
|
681
|
+
} else {
|
|
682
|
+
console.log(`asked on ${ask.taskId} [${ask.ordinal}] ${ask.text.split("\n")[0]}`)
|
|
683
|
+
if (branch) console.log(`committed and pushed to ${branch} — the claim owns this ticket's asks`)
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* `task resolve <id> <n>` — check an ask off (`--reopen` puts it back). The
|
|
689
|
+
* ordinal is the one `task show` prints; the optional --comment lands as an
|
|
690
|
+
* attributed comment so the answer travels with the ticket, not just the
|
|
691
|
+
* checkbox. Resolving the last open ask is the moment a ticket usually moves
|
|
692
|
+
* to done — the hint below says so rather than doing it, because finishing a
|
|
693
|
+
* ticket is a statement about the *work*, not the asks.
|
|
694
|
+
*/
|
|
695
|
+
function cmdResolve(args: Args): void {
|
|
696
|
+
const [ref, askRef] = args.positional
|
|
697
|
+
if (!ref || !askRef) {
|
|
698
|
+
fail('usage: task resolve <id> <ordinal> [--comment "<note>"] [--reopen] [--author <who>]')
|
|
699
|
+
}
|
|
700
|
+
const store = openStoreFor(ref)
|
|
701
|
+
const key = store.parseId(ref)
|
|
702
|
+
const author = resolveAuthor(str(args.flags, "author")).name
|
|
703
|
+
const { ask, branch } = setAskResolvedRouted(
|
|
704
|
+
store,
|
|
705
|
+
key,
|
|
706
|
+
askRef,
|
|
707
|
+
!args.flags.reopen,
|
|
708
|
+
author,
|
|
709
|
+
)
|
|
710
|
+
const note = str(args.flags, "comment")?.trim()
|
|
711
|
+
const comment = note
|
|
712
|
+
? store.addComment(
|
|
713
|
+
key,
|
|
714
|
+
`${args.flags.reopen ? "Reopened" : "Resolved"} ask [${ask.ordinal}] (${ask.text.split("\n")[0]}): ${note}`,
|
|
715
|
+
author,
|
|
716
|
+
)
|
|
717
|
+
: null
|
|
718
|
+
const task = store.get(key)!
|
|
719
|
+
// Branch-resolved asks aren't in the local files — count what's still open
|
|
720
|
+
// on the side that was just written.
|
|
721
|
+
const remaining = branch
|
|
722
|
+
? undefined
|
|
723
|
+
: task.asks.filter((a) => !a.resolvedAt).length
|
|
724
|
+
if (args.flags.json) {
|
|
725
|
+
console.log(JSON.stringify({ ask, branch, comment, openAsks: remaining ?? null }, null, 2))
|
|
726
|
+
return
|
|
727
|
+
}
|
|
728
|
+
const verb = args.flags.reopen ? "reopened" : "resolved"
|
|
729
|
+
console.log(`${verb} ${ask.taskId} [${ask.ordinal}] ${ask.text.split("\n")[0]}`)
|
|
730
|
+
if (branch) {
|
|
731
|
+
console.log(`committed and pushed to ${branch} — the worker sees it on its next pull`)
|
|
732
|
+
return
|
|
733
|
+
}
|
|
734
|
+
if (args.flags.reopen) return
|
|
735
|
+
if (remaining !== undefined && remaining > 0) {
|
|
736
|
+
console.log(`${remaining} ask${remaining === 1 ? "" : "s"} still open on ${ask.taskId}`)
|
|
737
|
+
} else if (task.status === "in_progress") {
|
|
738
|
+
console.log(`that was the last open ask — \`task move ${ask.taskId} done\` if the work is finished too`)
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* `task inbox` — what do I need to do NOW to unblock work: every open ask on
|
|
744
|
+
* effectively-in-progress tickets (file status, or a live claim branch —
|
|
745
|
+
* see inbox.ts), across the repo's boards, oldest owed first. Deliberately
|
|
746
|
+
* cross-board, like `claim --next`: the queue's attention is per-board, a
|
|
747
|
+
* person's isn't.
|
|
748
|
+
*/
|
|
749
|
+
function cmdInbox(args: Args): void {
|
|
750
|
+
let entries: InboxEntry[]
|
|
751
|
+
try {
|
|
752
|
+
const scope = findScopeRoot(process.cwd())
|
|
753
|
+
const boards = selectionBoards(process.cwd(), boardFlag(args)).map((store) => {
|
|
754
|
+
const rel = relative(scope, store.root).split(sep).join("/")
|
|
755
|
+
return { id: rel === "" ? "." : rel, store }
|
|
756
|
+
})
|
|
757
|
+
entries = buildInbox(boards)
|
|
758
|
+
} catch (error) {
|
|
759
|
+
if (error instanceof ClaimError) fail(error.message)
|
|
760
|
+
throw error
|
|
761
|
+
}
|
|
762
|
+
if (args.flags.json) {
|
|
763
|
+
console.log(JSON.stringify({ inbox: entries }, null, 2))
|
|
764
|
+
return
|
|
765
|
+
}
|
|
766
|
+
if (entries.length === 0) {
|
|
767
|
+
console.log("inbox zero — no open asks on in-progress work")
|
|
768
|
+
return
|
|
769
|
+
}
|
|
770
|
+
const open = entries.reduce((sum, e) => sum + e.asks.length, 0)
|
|
771
|
+
console.log(
|
|
772
|
+
`${open} open ask${open === 1 ? "" : "s"} across ${entries.length} ticket${entries.length === 1 ? "" : "s"}`,
|
|
773
|
+
)
|
|
774
|
+
const age = (iso: string): string => {
|
|
775
|
+
const days = Math.floor((Date.now() - Date.parse(iso)) / 86_400_000)
|
|
776
|
+
return days > 0 ? `${days}d` : "today"
|
|
777
|
+
}
|
|
778
|
+
for (const entry of entries) {
|
|
779
|
+
const via = entry.claim ? ` (claimed: ${entry.claim.branch})` : ""
|
|
780
|
+
console.log(`\n${entry.task.id} ${entry.task.title}${via}`)
|
|
781
|
+
for (const ask of entry.asks) {
|
|
782
|
+
const who = ask.author ? ` · ${ask.author}` : ""
|
|
783
|
+
console.log(` [${ask.ordinal}] ${ask.text.split("\n")[0]} (${age(ask.createdAt)}${who})`)
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
631
788
|
function cmdComment(args: Args): void {
|
|
632
789
|
const [ref, ...rest] = args.positional
|
|
633
790
|
const body = rest.join(" ").trim()
|
|
@@ -657,9 +814,9 @@ function cmdLink(args: Args, action: "link" | "unlink"): void {
|
|
|
657
814
|
const store = openStoreFor(ref)
|
|
658
815
|
const relation = blocks ? "blocks" : "blocked_by"
|
|
659
816
|
const target = parseRefOn(store, (blocks ?? blockedBy)!)
|
|
660
|
-
const
|
|
817
|
+
const key = store.parseId(ref)
|
|
661
818
|
const task =
|
|
662
|
-
action === "link" ? store.link(
|
|
819
|
+
action === "link" ? store.link(key, relation, target) : store.unlink(key, relation, target)
|
|
663
820
|
if (args.flags.json) {
|
|
664
821
|
console.log(JSON.stringify({ task }, null, 2))
|
|
665
822
|
} else {
|
|
@@ -673,7 +830,7 @@ function cmdLink(args: Args, action: "link" | "unlink"): void {
|
|
|
673
830
|
}
|
|
674
831
|
|
|
675
832
|
const CLAIM_USAGE =
|
|
676
|
-
'usage: task claim <id> | task claim --next | task claim --release <id> --comment "<why>"'
|
|
833
|
+
'usage: task claim <id> [--lock-only] | task claim --next | task claim --release <id> --comment "<why>"'
|
|
677
834
|
|
|
678
835
|
/**
|
|
679
836
|
* `task claim <id>` / `task claim --next` / `task claim --release <id>` — see
|
|
@@ -684,11 +841,21 @@ const CLAIM_USAGE =
|
|
|
684
841
|
function cmdClaim(args: Args): void {
|
|
685
842
|
const ref = args.positional[0]
|
|
686
843
|
const options = { force: Boolean(args.flags.force) }
|
|
687
|
-
const printClaim = (result: { task: Task; branch: string; base: string }): void => {
|
|
844
|
+
const printClaim = (result: { task: Task; branch: string; base: string; previous: string }): void => {
|
|
688
845
|
if (args.flags.json) {
|
|
689
846
|
console.log(JSON.stringify({ task: result.task, branch: result.branch }, null, 2))
|
|
690
847
|
} else {
|
|
691
848
|
console.log(`claimed ${result.task.id} — on ${result.branch} (from ${result.base}), status in_progress`)
|
|
849
|
+
// The backstop for branch-pinned sessions: a harness that assigned this
|
|
850
|
+
// checkout a branch expects the work to land there, and a plain claim
|
|
851
|
+
// just moved HEAD somewhere else. The skill carries the rule; this line
|
|
852
|
+
// is for the worker that hasn't read it.
|
|
853
|
+
const defaultLocal = result.base.replace(/^origin\//, "")
|
|
854
|
+
if (result.previous !== defaultLocal && result.previous !== result.branch) {
|
|
855
|
+
console.log(
|
|
856
|
+
`note: this moved you off ${result.previous} — if your work must deliver on ${result.previous}, \`task claim --release ${result.task.id} --comment "…"\` and re-claim with --lock-only`,
|
|
857
|
+
)
|
|
858
|
+
}
|
|
692
859
|
}
|
|
693
860
|
}
|
|
694
861
|
try {
|
|
@@ -697,7 +864,7 @@ function cmdClaim(args: Args): void {
|
|
|
697
864
|
// ticket across the selection scope (--board > root `boards` > nearest),
|
|
698
865
|
// retrying past lost races internally.
|
|
699
866
|
if (ref) fail("pass an id or --next, not both")
|
|
700
|
-
if (args.flags.release) fail(CLAIM_USAGE)
|
|
867
|
+
if (args.flags.release || args.flags["lock-only"]) fail(CLAIM_USAGE)
|
|
701
868
|
const result = claimNext(selectionBoards(process.cwd(), boardFlag(args)), options)
|
|
702
869
|
if (!result) {
|
|
703
870
|
if (args.flags.json) {
|
|
@@ -712,7 +879,31 @@ function cmdClaim(args: Args): void {
|
|
|
712
879
|
}
|
|
713
880
|
if (!ref) fail(CLAIM_USAGE)
|
|
714
881
|
const store = openStoreFor(ref)
|
|
715
|
-
const
|
|
882
|
+
const key = store.parseId(ref)
|
|
883
|
+
if (args.flags["lock-only"]) {
|
|
884
|
+
// The branch-pinned session's claim: same lock, no checkout switch —
|
|
885
|
+
// the work delivers on the branch this session was handed, and the flip
|
|
886
|
+
// commit's delivered_by: trailer says so. See claimLockOnly.
|
|
887
|
+
if (args.flags.release) fail(CLAIM_USAGE)
|
|
888
|
+
const result = claimLockOnly(store, key, options)
|
|
889
|
+
if (args.flags.json) {
|
|
890
|
+
console.log(
|
|
891
|
+
JSON.stringify(
|
|
892
|
+
{ task: result.task, branch: result.branch, deliveredBy: result.deliveredBy },
|
|
893
|
+
null,
|
|
894
|
+
2,
|
|
895
|
+
),
|
|
896
|
+
)
|
|
897
|
+
} else {
|
|
898
|
+
console.log(
|
|
899
|
+
`claimed ${result.task.id} — ${result.branch} on origin (lock only, from ${result.base}), status in_progress`,
|
|
900
|
+
)
|
|
901
|
+
console.log(
|
|
902
|
+
`checkout untouched — keep working on ${result.deliveredBy} (recorded as delivered_by in the claim commit)`,
|
|
903
|
+
)
|
|
904
|
+
}
|
|
905
|
+
return
|
|
906
|
+
}
|
|
716
907
|
if (args.flags.release) {
|
|
717
908
|
// A released claim must leave its failure context behind: the branch
|
|
718
909
|
// (and whatever was tried on it) is about to evaporate, so the reason
|
|
@@ -723,26 +914,26 @@ function cmdClaim(args: Args): void {
|
|
|
723
914
|
'task claim --release requires --comment "<why>" — the next worker inherits what was tried',
|
|
724
915
|
)
|
|
725
916
|
}
|
|
726
|
-
const result = release(store,
|
|
917
|
+
const result = release(store, key)
|
|
727
918
|
const commented = result.remote || result.local
|
|
728
919
|
// After the branch deletion, so the comment survives it: release leaves
|
|
729
920
|
// the checkout on the default branch, and the comment file sits there
|
|
730
921
|
// uncommitted, to ride along with the next commit.
|
|
731
922
|
const comment = commented
|
|
732
|
-
? store.addComment(
|
|
923
|
+
? store.addComment(key, why, resolveAuthor(str(args.flags, "author")).name)
|
|
733
924
|
: null
|
|
734
925
|
if (args.flags.json) {
|
|
735
926
|
console.log(JSON.stringify({ released: result, comment }, null, 2))
|
|
736
927
|
} else if (!commented) {
|
|
737
|
-
console.log(`${store.displayId(
|
|
928
|
+
console.log(`${store.displayId(key)} wasn't claimed — no ${result.branch} to delete`)
|
|
738
929
|
} else {
|
|
739
930
|
const where = [result.remote && "origin", result.local && "local"].filter(Boolean)
|
|
740
|
-
console.log(`released ${store.displayId(
|
|
741
|
-
console.log(`left the reason as a comment on ${store.displayId(
|
|
931
|
+
console.log(`released ${store.displayId(key)} — deleted ${result.branch} (${where.join(" and ")})`)
|
|
932
|
+
console.log(`left the reason as a comment on ${store.displayId(key)} — uncommitted, commit it with your next change`)
|
|
742
933
|
}
|
|
743
934
|
return
|
|
744
935
|
}
|
|
745
|
-
printClaim(claim(store,
|
|
936
|
+
printClaim(claim(store, key, options))
|
|
746
937
|
} catch (error) {
|
|
747
938
|
if (error instanceof ClaimError) {
|
|
748
939
|
console.error(`error: ${error.message}`)
|
|
@@ -752,6 +943,50 @@ function cmdClaim(args: Args): void {
|
|
|
752
943
|
}
|
|
753
944
|
}
|
|
754
945
|
|
|
946
|
+
/**
|
|
947
|
+
* `task sweep` — the janitor: delete every claim branch on origin whose ticket
|
|
948
|
+
* is done, canceled, or missing on the current checkout. Zero judgment on
|
|
949
|
+
* purpose — the trigger belongs to a runner (CI on push-to-main or cron), not
|
|
950
|
+
* an agent. Exit codes: 0 clean (including nothing to do), 1 a deletion was
|
|
951
|
+
* attempted and failed — a failing sweep means this runner can't delete
|
|
952
|
+
* branches either, which is worth an alert.
|
|
953
|
+
*/
|
|
954
|
+
function cmdSweep(args: Args): void {
|
|
955
|
+
try {
|
|
956
|
+
const result = sweep(process.cwd())
|
|
957
|
+
if (args.flags.json) {
|
|
958
|
+
console.log(JSON.stringify({ sweep: result }, null, 2))
|
|
959
|
+
} else {
|
|
960
|
+
for (const entry of result.swept) {
|
|
961
|
+
console.log(
|
|
962
|
+
`swept ${entry.branch} — ${entry.reason === "missing" ? `no ticket ${entry.ticket}` : `${entry.ticket} is ${entry.reason}`}`,
|
|
963
|
+
)
|
|
964
|
+
}
|
|
965
|
+
for (const entry of result.kept) {
|
|
966
|
+
console.log(`kept ${entry.branch} — ${entry.ticket} is ${entry.status}`)
|
|
967
|
+
}
|
|
968
|
+
for (const branch of result.skipped) {
|
|
969
|
+
console.log(
|
|
970
|
+
`skipped ${branch} — not a name \`task claim\` mints (foreign or pre-migration); delete it by hand if it's stale`,
|
|
971
|
+
)
|
|
972
|
+
}
|
|
973
|
+
for (const entry of result.failed) {
|
|
974
|
+
console.error(`error: couldn't delete ${entry.branch} — ${entry.error}`)
|
|
975
|
+
}
|
|
976
|
+
if (!result.swept.length && !result.kept.length && !result.skipped.length && !result.failed.length) {
|
|
977
|
+
console.log("no claim branches on origin — nothing to sweep")
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (result.failed.length) process.exit(1)
|
|
981
|
+
} catch (error) {
|
|
982
|
+
if (error instanceof ClaimError) {
|
|
983
|
+
console.error(`error: ${error.message}`)
|
|
984
|
+
process.exit(2)
|
|
985
|
+
}
|
|
986
|
+
throw error
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
755
990
|
/** `task instructions` — the shipped agent conventions, self-served at runtime. */
|
|
756
991
|
function cmdInstructions(): void {
|
|
757
992
|
// skill/ ships in the npm package; `../` from both dist/cli.js and
|
|
@@ -774,7 +1009,10 @@ function cmdOverview(args: Args): void {
|
|
|
774
1009
|
overview.claimed === null
|
|
775
1010
|
? "claimed ? (origin unreachable)"
|
|
776
1011
|
: `claimed ${overview.claimed.length}${overview.claimed.length ? ` (${overview.claimed.join(", ")})` : ""}`
|
|
777
|
-
|
|
1012
|
+
const asks = overview.oldestAsk
|
|
1013
|
+
? `asks ${overview.openAsks} open (oldest ${overview.oldestAsk.taskId}, ${overview.oldestAsk.createdAt.slice(0, 10)})`
|
|
1014
|
+
: "asks 0 open"
|
|
1015
|
+
console.log(` ${claimed} ${asks}`)
|
|
778
1016
|
if (overview.goals.length) {
|
|
779
1017
|
console.log("goals")
|
|
780
1018
|
table(
|
|
@@ -967,9 +1205,9 @@ function cmdDelete(args: Args): void {
|
|
|
967
1205
|
const ref = args.positional[0]
|
|
968
1206
|
if (!ref) fail("usage: task delete <id>")
|
|
969
1207
|
const store = openStoreFor(ref)
|
|
970
|
-
const
|
|
971
|
-
store.delete(
|
|
972
|
-
console.log(`deleted ${store.displayId(
|
|
1208
|
+
const key = store.parseId(ref)
|
|
1209
|
+
store.delete(key)
|
|
1210
|
+
console.log(`deleted ${store.displayId(key)}`)
|
|
973
1211
|
}
|
|
974
1212
|
|
|
975
1213
|
/**
|
|
@@ -1165,11 +1403,14 @@ subdirectory (it walks up to find .task/, like git).
|
|
|
1165
1403
|
Usage
|
|
1166
1404
|
task init [--name <name>] [--prefix <PREFIX>]
|
|
1167
1405
|
task add <title> [--description <text> | --description-file <path|->]
|
|
1168
|
-
[--status <s>] [--tags <a,b>] [--goal <slug>]
|
|
1406
|
+
[--status <s>] [--tags <a,b>] [--goal <slug>]
|
|
1407
|
+
[--ask "<text>"]…
|
|
1169
1408
|
new tickets land in backlog (the dump list)
|
|
1170
1409
|
unless --status says otherwise;
|
|
1171
1410
|
--description-file reads markdown from a file
|
|
1172
|
-
(or stdin with -) — no shell-quoting fights
|
|
1411
|
+
(or stdin with -) — no shell-quoting fights;
|
|
1412
|
+
--ask (repeatable) files what a person owes
|
|
1413
|
+
the ticket, right at creation
|
|
1173
1414
|
task list [--status <s1,s2>] [--tag <a,b>] [--goal <slug>]
|
|
1174
1415
|
[--needs-human] [--all] [--archived] [--claimable]
|
|
1175
1416
|
task search <query> [--status <s1,s2>] [--tag <a,b>] [--goal <slug>]
|
|
@@ -1183,18 +1424,38 @@ Usage
|
|
|
1183
1424
|
the shared context without chasing it
|
|
1184
1425
|
task update <id> [--title <t>] [--description <text> | --description-file <path|->]
|
|
1185
1426
|
[--status <s>] [--tags <a,b>] [--goal <slug>]
|
|
1186
|
-
[--needs-human | --no-needs-human]
|
|
1187
1427
|
[--pr <url>] [--prs <url1,url2>]
|
|
1188
1428
|
task move <id> <status> change a ticket's status — the one general
|
|
1189
1429
|
status API (backlog todo in_progress done
|
|
1190
|
-
canceled).
|
|
1191
|
-
|
|
1192
|
-
|
|
1430
|
+
canceled). Moving to done is refused while
|
|
1431
|
+
the ticket has open asks: done means nobody
|
|
1432
|
+
owes anything. The named verbs below exist
|
|
1433
|
+
only where a transition does more than write
|
|
1434
|
+
the status field
|
|
1435
|
+
task ask <id> "<text>" file an ask: one thing a person owes this
|
|
1436
|
+
ticket before it can be done. Asks derive the
|
|
1437
|
+
needs-human badge, feed \`task inbox\`, and
|
|
1438
|
+
gate \`done\` — they never block claiming (an
|
|
1439
|
+
agent works the code while a person mints the
|
|
1440
|
+
token; a true precondition is a blocked_by on
|
|
1441
|
+
a thin human ticket instead)
|
|
1442
|
+
task resolve <id> <n> [--comment "<note>"] [--reopen]
|
|
1443
|
+
check ask <n> off (\`task show\` prints the
|
|
1444
|
+
ordinals); the note lands as an attributed
|
|
1445
|
+
comment. --reopen puts a resolved ask back
|
|
1446
|
+
task inbox [--board <P>] what needs YOU, now: every open ask on
|
|
1447
|
+
effectively in-progress tickets — file status
|
|
1448
|
+
in_progress, or a live claim branch on origin
|
|
1449
|
+
(a claimed ticket is in progress whatever the
|
|
1450
|
+
merged file says, and its asks are read from
|
|
1451
|
+
the branch). Cross-board like claim --next;
|
|
1452
|
+
oldest owed first. Backlog/todo asks are out
|
|
1453
|
+
by design — claiming is what makes them yours
|
|
1193
1454
|
task promote <id> backlog → todo, the human "build this" call.
|
|
1194
1455
|
No quality gate — planning happens at claim
|
|
1195
|
-
time. Blocked
|
|
1196
|
-
|
|
1197
|
-
|
|
1456
|
+
time. Blocked tickets still promote —
|
|
1457
|
+
blockers are claim-time gates, reported as a
|
|
1458
|
+
note — so promoting can queue work behind
|
|
1198
1459
|
its blockers. Exit codes: 0 promoted, 2
|
|
1199
1460
|
refused (not a backlog ticket)
|
|
1200
1461
|
task link <id> --blocked-by <id>
|
|
@@ -1202,7 +1463,7 @@ Usage
|
|
|
1202
1463
|
both tasks (A blocked by B ⇔ B blocks A)
|
|
1203
1464
|
task unlink <id> (--blocks <id> | --blocked-by <id>)
|
|
1204
1465
|
task claim <id> [--force] claim a ticket before working it: branch
|
|
1205
|
-
<branchPrefix><prefix>-<
|
|
1466
|
+
<branchPrefix><prefix>-<key> off origin's
|
|
1206
1467
|
default branch, ticket → in_progress as its
|
|
1207
1468
|
first commit, pushed. The namespace defaults
|
|
1208
1469
|
to task/claim/ — set claims.branchPrefix in
|
|
@@ -1215,8 +1476,21 @@ Usage
|
|
|
1215
1476
|
many claim branches exist on origin, claiming
|
|
1216
1477
|
is refused (--force overrides). Exit codes:
|
|
1217
1478
|
0 claimed, 1 already claimed, 2 not claimable
|
|
1218
|
-
(not todo, blocked,
|
|
1219
|
-
|
|
1479
|
+
(not todo, blocked, dirty tree, at the cap)
|
|
1480
|
+
task claim <id> --lock-only [--force]
|
|
1481
|
+
take the lock without touching the checkout:
|
|
1482
|
+
the in_progress flip commit is built against
|
|
1483
|
+
origin's default branch in a temporary index
|
|
1484
|
+
and pushed as the claim branch — same atomic
|
|
1485
|
+
lock, same exit codes, but HEAD, the working
|
|
1486
|
+
tree and the index stay exactly where they
|
|
1487
|
+
are (dirty is fine). For sessions pinned to
|
|
1488
|
+
a provisioned branch (CI, a Claude web
|
|
1489
|
+
session): the lock lives on the claim
|
|
1490
|
+
branch, the work delivers on yours, and the
|
|
1491
|
+
flip commit's delivered_by: trailer records
|
|
1492
|
+
which. --release works on a lock-only claim
|
|
1493
|
+
the same way
|
|
1220
1494
|
task claim --next [--board <P>] [--force]
|
|
1221
1495
|
claim the top claimable ticket in one call,
|
|
1222
1496
|
retrying past lost races internally — the
|
|
@@ -1232,18 +1506,36 @@ Usage
|
|
|
1232
1506
|
the branch, so deleting it is the revert. The
|
|
1233
1507
|
comment is required and lands on the ticket
|
|
1234
1508
|
(attributed like task comment, uncommitted),
|
|
1235
|
-
so the next worker inherits what was tried
|
|
1509
|
+
so the next worker inherits what was tried.
|
|
1510
|
+
The deletion is verified with ls-remote: an
|
|
1511
|
+
environment that silently drops deletion
|
|
1512
|
+
pushes fails loudly instead of reporting a
|
|
1513
|
+
release that didn't happen
|
|
1514
|
+
task sweep the janitor: delete every claim branch on
|
|
1515
|
+
origin whose ticket is done, canceled, or
|
|
1516
|
+
missing on the current checkout, across all
|
|
1517
|
+
the repo's boards. Scoped to names \`claim\`
|
|
1518
|
+
mints (<prefix>-<valid key>): anything else
|
|
1519
|
+
under the namespace — pre-migration numeric
|
|
1520
|
+
claims, hand-made branches — is reported and
|
|
1521
|
+
left alone, since it may be an open PR's
|
|
1522
|
+
head. Pure git, zero judgment — run it from
|
|
1523
|
+
a runner that can delete remote branches (CI
|
|
1524
|
+
on push-to-main or cron) to heal stale
|
|
1525
|
+
claims left by environments that can't.
|
|
1526
|
+
Prints swept/kept/skipped; exits non-zero if
|
|
1527
|
+
a deletion was attempted and failed
|
|
1236
1528
|
task list --claimable [--board <P>]
|
|
1237
1529
|
the claim queue: todo tickets in position
|
|
1238
|
-
order, minus blocked /
|
|
1239
|
-
|
|
1240
|
-
|
|
1530
|
+
order, minus blocked / already claimed on
|
|
1531
|
+
origin. Top entry is next up. Same board
|
|
1532
|
+
scope as claim --next
|
|
1241
1533
|
task instructions print the shipped agent conventions
|
|
1242
1534
|
(skill/SKILL.md) — agents without the skill
|
|
1243
1535
|
installed self-serve the house rules
|
|
1244
1536
|
task overview [--json] one screen of board health: counts per
|
|
1245
1537
|
status, per-goal progress, claimed and
|
|
1246
|
-
|
|
1538
|
+
open-ask counts, stale tickets (untouched
|
|
1247
1539
|
${STALE_DAYS}+ days), oldest open ticket
|
|
1248
1540
|
task comment <id> <text> [--author <who>]
|
|
1249
1541
|
task delete <id>
|
|
@@ -1265,7 +1557,7 @@ Usage
|
|
|
1265
1557
|
task archive <id> move a done/canceled ticket to .task/archive/,
|
|
1266
1558
|
out of the board and off the hot path — still
|
|
1267
1559
|
readable via show and list --archived, and its
|
|
1268
|
-
|
|
1560
|
+
key stays reserved
|
|
1269
1561
|
task archive --all archive everything done or canceled
|
|
1270
1562
|
task unarchive <id> put an archived ticket back on the board
|
|
1271
1563
|
task whoami who your comments are attributed to
|
|
@@ -1297,15 +1589,18 @@ Usage
|
|
|
1297
1589
|
tasks — those are in .task/ either way
|
|
1298
1590
|
|
|
1299
1591
|
Values
|
|
1300
|
-
<id> TAS-
|
|
1301
|
-
|
|
1302
|
-
|
|
1592
|
+
<id> TAS-x7k4m, x7k4m, or any unique prefix of a key (task show x7
|
|
1593
|
+
works, git-style — ambiguity is an error naming the matches).
|
|
1594
|
+
Ids are random 5-character keys, minted branch-safely at
|
|
1595
|
+
\`task add\`. A bare key means the nearest board; a prefixed id
|
|
1596
|
+
routes to whichever board in the repo owns that prefix, so
|
|
1597
|
+
TAS-x7k4m works from anywhere in a monorepo
|
|
1303
1598
|
status ${STATUSES.join(" ")}
|
|
1304
1599
|
--tag a,b matches a task carrying *either* tag
|
|
1305
1600
|
--goal <slug> a task belongs to at most one goal. Goals answer "where is
|
|
1306
1601
|
this going" (one); tags answer "what kind" (many) — something
|
|
1307
1602
|
that wants two goals is a tag
|
|
1308
|
-
--needs-human
|
|
1603
|
+
--needs-human filter: tickets with open asks — something a person owes
|
|
1309
1604
|
--pr <url> attach a pull request (appends); --prs replaces the whole list
|
|
1310
1605
|
clearing --tags "" drops all tags, --goal "" clears it, --prs ""
|
|
1311
1606
|
detaches all PRs
|
|
@@ -1354,8 +1649,16 @@ function main(): void | Promise<void> {
|
|
|
1354
1649
|
return cmdLink(args, "unlink")
|
|
1355
1650
|
case "claim":
|
|
1356
1651
|
return cmdClaim(args)
|
|
1652
|
+
case "sweep":
|
|
1653
|
+
return cmdSweep(args)
|
|
1357
1654
|
case "instructions":
|
|
1358
1655
|
return cmdInstructions()
|
|
1656
|
+
case "ask":
|
|
1657
|
+
return cmdAsk(args)
|
|
1658
|
+
case "resolve":
|
|
1659
|
+
return cmdResolve(args)
|
|
1660
|
+
case "inbox":
|
|
1661
|
+
return cmdInbox(args)
|
|
1359
1662
|
case "overview":
|
|
1360
1663
|
return cmdOverview(args)
|
|
1361
1664
|
case "goal":
|