@nickmeriano/task 0.8.0 → 0.9.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 +65 -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 +21 -22
- package/dist/claim.d.ts.map +1 -1
- package/dist/claim.js +58 -83
- package/dist/claim.js.map +1 -1
- package/dist/claim.test.d.ts +2 -2
- package/dist/claim.test.js +111 -94
- package/dist/claim.test.js.map +1 -1
- package/dist/cli.js +241 -64
- 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 +70 -23
- 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 +141 -99
- package/src/claim.ts +69 -96
- package/src/cli.ts +251 -63
- 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,13 +6,13 @@
|
|
|
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"
|
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
release,
|
|
26
26
|
selectionBoards,
|
|
27
27
|
} from "./claim.ts"
|
|
28
|
+
import { addAskRouted, setAskResolvedRouted } from "./claim-io.ts"
|
|
29
|
+
import { buildInbox, type InboxEntry } from "./inbox.ts"
|
|
28
30
|
import { STALE_DAYS, buildOverview } from "./overview.ts"
|
|
29
31
|
import { searchStore } from "./search.ts"
|
|
30
32
|
import { detectRepo, parseSlug, publish, resolveHost } from "./publish.ts"
|
|
@@ -66,6 +68,8 @@ const PORT_ATTEMPTS = 20
|
|
|
66
68
|
interface Args {
|
|
67
69
|
positional: string[]
|
|
68
70
|
flags: Record<string, string | boolean>
|
|
71
|
+
/** Values of MULTI_FLAGS, in given order — e.g. multi.ask from `--ask "…"`. */
|
|
72
|
+
multi: Record<string, string[]>
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
// Every valueless flag has to be listed here: an unregistered one swallows the
|
|
@@ -77,7 +81,6 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
77
81
|
"version",
|
|
78
82
|
"yes",
|
|
79
83
|
"needs-human",
|
|
80
|
-
"no-needs-human",
|
|
81
84
|
"archived",
|
|
82
85
|
"claimable",
|
|
83
86
|
"release",
|
|
@@ -90,6 +93,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
90
93
|
"fix",
|
|
91
94
|
"next",
|
|
92
95
|
"force",
|
|
96
|
+
"reopen",
|
|
93
97
|
])
|
|
94
98
|
|
|
95
99
|
/**
|
|
@@ -99,14 +103,26 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
99
103
|
*/
|
|
100
104
|
const REPEATABLE_FLAGS = new Set(["board"])
|
|
101
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Repeatable flags whose values are free text — collected verbatim into a
|
|
108
|
+
* list, never comma-joined, because an ask like "check A, then B" must
|
|
109
|
+
* survive as one ask.
|
|
110
|
+
*/
|
|
111
|
+
const MULTI_FLAGS = new Set(["ask"])
|
|
112
|
+
|
|
102
113
|
function parseArgs(argv: string[]): Args {
|
|
103
114
|
const positional: string[] = []
|
|
104
115
|
const flags: Record<string, string | boolean> = {}
|
|
116
|
+
const multi: Record<string, string[]> = {}
|
|
105
117
|
for (let i = 0; i < argv.length; i++) {
|
|
106
118
|
const arg = argv[i]
|
|
107
119
|
if (arg.startsWith("--")) {
|
|
108
120
|
const name = arg.slice(2)
|
|
109
121
|
const set = (key: string, value: string): void => {
|
|
122
|
+
if (MULTI_FLAGS.has(key)) {
|
|
123
|
+
;(multi[key] ??= []).push(value)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
110
126
|
const prior = flags[key]
|
|
111
127
|
flags[key] =
|
|
112
128
|
REPEATABLE_FLAGS.has(key) && typeof prior === "string" ? `${prior},${value}` : value
|
|
@@ -123,7 +139,7 @@ function parseArgs(argv: string[]): Args {
|
|
|
123
139
|
positional.push(arg)
|
|
124
140
|
}
|
|
125
141
|
}
|
|
126
|
-
return { positional, flags }
|
|
142
|
+
return { positional, flags, multi }
|
|
127
143
|
}
|
|
128
144
|
|
|
129
145
|
function str(flags: Args["flags"], name: string): string | undefined {
|
|
@@ -142,19 +158,19 @@ function openStore(): Store {
|
|
|
142
158
|
return openBoard(root)
|
|
143
159
|
}
|
|
144
160
|
|
|
145
|
-
/** The prefix a ref carries, if any: "TAS-
|
|
161
|
+
/** The prefix a ref carries, if any: "TAS-x7k4m" → "TAS", "x7k4m" → null. */
|
|
146
162
|
function refPrefix(ref: string): string | null {
|
|
147
|
-
const match = /^([A-Za-z0-9]+)
|
|
163
|
+
const match = /^([A-Za-z0-9]+)-[A-Za-z0-9]+$/.exec(ref.trim())
|
|
148
164
|
return match ? match[1].toUpperCase() : null
|
|
149
165
|
}
|
|
150
166
|
|
|
151
167
|
/**
|
|
152
|
-
* The board a ref belongs to. A bare
|
|
168
|
+
* The board a ref belongs to. A bare key means the nearest board, as ever —
|
|
153
169
|
* but a prefixed id is an address, and it routes: if the prefix isn't the
|
|
154
170
|
* 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-
|
|
171
|
+
* root, then down) is searched for it, so `task show TAS-x7k4m` works from
|
|
156
172
|
* 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
|
|
173
|
+
* strip a foreign prefix and act on the nearest board's ticket of that key.
|
|
158
174
|
*/
|
|
159
175
|
function openStoreFor(ref: string | undefined): Store {
|
|
160
176
|
const prefix = ref ? refPrefix(ref) : null
|
|
@@ -192,7 +208,7 @@ function openStoreFor(ref: string | undefined): Store {
|
|
|
192
208
|
* instead of reinterpreting it — this guards the second id in `task link`,
|
|
193
209
|
* where the board was picked by the first.
|
|
194
210
|
*/
|
|
195
|
-
function parseRefOn(store: Store, ref: string):
|
|
211
|
+
function parseRefOn(store: Store, ref: string): string {
|
|
196
212
|
const prefix = refPrefix(ref)
|
|
197
213
|
if (prefix && prefix !== store.config.prefix.toUpperCase()) {
|
|
198
214
|
fail(`${ref} is not on the ${store.config.prefix} board — links can't cross boards`)
|
|
@@ -238,8 +254,6 @@ function patchFromFlags(flags: Args["flags"]): TaskPatch {
|
|
|
238
254
|
if (tags !== undefined) patch.tags = parseTags(tags)
|
|
239
255
|
const goal = str(flags, "goal")
|
|
240
256
|
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
257
|
// Replace semantics like --tags; `--pr` (append one) is handled per-command
|
|
244
258
|
// because appending needs the task's current list.
|
|
245
259
|
const prs = str(flags, "prs")
|
|
@@ -304,16 +318,23 @@ function cmdInit(args: Args): void {
|
|
|
304
318
|
|
|
305
319
|
function cmdAdd(args: Args): void {
|
|
306
320
|
const title = args.positional.join(" ").trim()
|
|
307
|
-
if (!title) fail(`usage: task add <title> [--description …] [--status …] [--tags a,b] [--goal <slug>] [--
|
|
321
|
+
if (!title) fail(`usage: task add <title> [--description …] [--status …] [--tags a,b] [--goal <slug>] [--ask "<text>"]…`)
|
|
308
322
|
const store = openStore()
|
|
309
323
|
const patch = patchFromFlags(args.flags)
|
|
310
324
|
const pr = str(args.flags, "pr")
|
|
311
325
|
if (pr) patch.prs = [...(patch.prs ?? []), pr]
|
|
312
|
-
|
|
326
|
+
let task = store.create({ title, ...patch })
|
|
327
|
+
const asks = (args.multi.ask ?? []).map((a) => a.trim()).filter(Boolean)
|
|
328
|
+
if (asks.length > 0) {
|
|
329
|
+
const author = resolveAuthor(str(args.flags, "author")).name
|
|
330
|
+
for (const text of asks) store.addAsk(task.key, text, author)
|
|
331
|
+
task = store.get(task.key)!
|
|
332
|
+
}
|
|
313
333
|
if (args.flags.json) {
|
|
314
334
|
console.log(JSON.stringify({ task }, null, 2))
|
|
315
335
|
} else {
|
|
316
336
|
printTask(task)
|
|
337
|
+
for (const ask of task.asks) console.log(` ask [${ask.ordinal}] ${ask.text.split("\n")[0]}`)
|
|
317
338
|
}
|
|
318
339
|
}
|
|
319
340
|
|
|
@@ -500,7 +521,7 @@ function cmdArchive(args: Args): void {
|
|
|
500
521
|
|
|
501
522
|
const store = openStore()
|
|
502
523
|
const finished = store.list({ statuses: ["done", "canceled"] })
|
|
503
|
-
const archived = finished.map((t) => store.archive(t.
|
|
524
|
+
const archived = finished.map((t) => store.archive(t.key))
|
|
504
525
|
if (args.flags.json) {
|
|
505
526
|
console.log(JSON.stringify({ archived }, null, 2))
|
|
506
527
|
} else if (archived.length === 0) {
|
|
@@ -526,10 +547,10 @@ function cmdShow(args: Args): void {
|
|
|
526
547
|
const ref = args.positional[0]
|
|
527
548
|
if (!ref) fail("usage: task show <id>")
|
|
528
549
|
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(
|
|
550
|
+
const key = store.parseId(ref)
|
|
551
|
+
const task = store.get(key)
|
|
552
|
+
if (!task) fail(`no such task: ${store.displayId(key)}`)
|
|
553
|
+
const comments = store.comments(key)
|
|
533
554
|
// The goal rides along in full — title *and* description — so an agent
|
|
534
555
|
// picking the ticket up cold inherits the shared context without it being
|
|
535
556
|
// pasted into every ticket. null when unset or when the ref dangles.
|
|
@@ -539,17 +560,27 @@ function cmdShow(args: Args): void {
|
|
|
539
560
|
return
|
|
540
561
|
}
|
|
541
562
|
// 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(
|
|
563
|
+
const describeLinks = (keys: string[]): string =>
|
|
564
|
+
keys
|
|
565
|
+
.map((k) => {
|
|
566
|
+
const other = store.get(k)
|
|
567
|
+
return other ? `${other.id} (${other.status})` : store.displayId(k)
|
|
547
568
|
})
|
|
548
569
|
.join(", ")
|
|
549
570
|
console.log(`${task.id} ${task.title}`)
|
|
550
571
|
console.log(`status ${task.status}`)
|
|
551
572
|
if (task.archived) console.log(`archived yes — \`task unarchive ${task.id}\` to edit`)
|
|
552
|
-
|
|
573
|
+
const openAsks = task.asks.filter((a) => !a.resolvedAt)
|
|
574
|
+
if (openAsks.length) {
|
|
575
|
+
console.log(`needs a human — ${openAsks.length} open ask${openAsks.length === 1 ? "" : "s"}`)
|
|
576
|
+
}
|
|
577
|
+
for (const ask of task.asks) {
|
|
578
|
+
const state = ask.resolvedAt
|
|
579
|
+
? `✓ resolved${ask.resolvedBy ? ` by ${ask.resolvedBy}` : ""}`
|
|
580
|
+
: `○ open${ask.author ? ` · ${ask.author}` : ""} · ${ask.createdAt.slice(0, 10)}`
|
|
581
|
+
console.log(`ask [${ask.ordinal}] ${ask.text.split("\n").join("\n ")}`)
|
|
582
|
+
console.log(` ${state}`)
|
|
583
|
+
}
|
|
553
584
|
if (task.tags.length) console.log(`tags ${task.tags.join(", ")}`)
|
|
554
585
|
if (task.goal) {
|
|
555
586
|
console.log(`goal ${task.goal}${goal ? ` — ${goal.title}${goal.archived ? " (archived)" : ""}` : ""}`)
|
|
@@ -577,16 +608,16 @@ function cmdUpdate(args: Args): void {
|
|
|
577
608
|
const ref = args.positional[0]
|
|
578
609
|
if (!ref) fail("usage: task update <id> [--status …] [--title …] …")
|
|
579
610
|
const store = openStoreFor(ref)
|
|
580
|
-
const
|
|
611
|
+
const key = store.parseId(ref)
|
|
581
612
|
const patch = patchFromFlags(args.flags)
|
|
582
613
|
const pr = str(args.flags, "pr")
|
|
583
614
|
if (pr) {
|
|
584
615
|
// Append, dedup — `--pr <url>` is "attach this PR", not "replace the list".
|
|
585
|
-
const current = patch.prs ?? store.get(
|
|
616
|
+
const current = patch.prs ?? store.get(key)?.prs ?? []
|
|
586
617
|
patch.prs = current.includes(pr) ? current : [...current, pr]
|
|
587
618
|
}
|
|
588
619
|
if (Object.keys(patch).length === 0) fail("nothing to update — pass at least one flag")
|
|
589
|
-
const task = store.update(
|
|
620
|
+
const task = store.update(key, patch)
|
|
590
621
|
if (args.flags.json) {
|
|
591
622
|
console.log(JSON.stringify({ task }, null, 2))
|
|
592
623
|
} else {
|
|
@@ -597,7 +628,7 @@ function cmdUpdate(args: Args): void {
|
|
|
597
628
|
function cmdMove(args: Args): void {
|
|
598
629
|
const [ref, status] = args.positional
|
|
599
630
|
if (!ref || !status) fail("usage: task move <id> <status>")
|
|
600
|
-
cmdUpdate({ positional: [ref], flags: { ...args.flags, status } })
|
|
631
|
+
cmdUpdate({ positional: [ref], flags: { ...args.flags, status }, multi: {} })
|
|
601
632
|
}
|
|
602
633
|
|
|
603
634
|
/**
|
|
@@ -628,6 +659,129 @@ function cmdPromote(args: Args): void {
|
|
|
628
659
|
}
|
|
629
660
|
}
|
|
630
661
|
|
|
662
|
+
/**
|
|
663
|
+
* `task ask <id> <text>` — file one thing a person owes this ticket. The ask
|
|
664
|
+
* is the primitive every human requirement funnels through: it derives the
|
|
665
|
+
* needs-human badge, feeds `task inbox`, and gates `done` until resolved.
|
|
666
|
+
* On a claimed ticket the write lands on the claim branch as a pushed commit
|
|
667
|
+
* (see claim-io.ts) — the worker's done gate has to see debt filed mid-claim.
|
|
668
|
+
*/
|
|
669
|
+
function cmdAsk(args: Args): void {
|
|
670
|
+
const [ref, ...rest] = args.positional
|
|
671
|
+
const text = rest.join(" ").trim()
|
|
672
|
+
if (!ref || !text) fail(`usage: task ask <id> "<what a person needs to do>" [--author <who>]`)
|
|
673
|
+
const store = openStoreFor(ref)
|
|
674
|
+
const author = resolveAuthor(str(args.flags, "author")).name
|
|
675
|
+
const { ask, branch } = addAskRouted(store, store.parseId(ref), text, author)
|
|
676
|
+
if (args.flags.json) {
|
|
677
|
+
console.log(JSON.stringify({ ask, branch }, null, 2))
|
|
678
|
+
} else {
|
|
679
|
+
console.log(`asked on ${ask.taskId} [${ask.ordinal}] ${ask.text.split("\n")[0]}`)
|
|
680
|
+
if (branch) console.log(`committed and pushed to ${branch} — the claim owns this ticket's asks`)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* `task resolve <id> <n>` — check an ask off (`--reopen` puts it back). The
|
|
686
|
+
* ordinal is the one `task show` prints; the optional --comment lands as an
|
|
687
|
+
* attributed comment so the answer travels with the ticket, not just the
|
|
688
|
+
* checkbox. Resolving the last open ask is the moment a ticket usually moves
|
|
689
|
+
* to done — the hint below says so rather than doing it, because finishing a
|
|
690
|
+
* ticket is a statement about the *work*, not the asks.
|
|
691
|
+
*/
|
|
692
|
+
function cmdResolve(args: Args): void {
|
|
693
|
+
const [ref, askRef] = args.positional
|
|
694
|
+
if (!ref || !askRef) {
|
|
695
|
+
fail('usage: task resolve <id> <ordinal> [--comment "<note>"] [--reopen] [--author <who>]')
|
|
696
|
+
}
|
|
697
|
+
const store = openStoreFor(ref)
|
|
698
|
+
const key = store.parseId(ref)
|
|
699
|
+
const author = resolveAuthor(str(args.flags, "author")).name
|
|
700
|
+
const { ask, branch } = setAskResolvedRouted(
|
|
701
|
+
store,
|
|
702
|
+
key,
|
|
703
|
+
askRef,
|
|
704
|
+
!args.flags.reopen,
|
|
705
|
+
author,
|
|
706
|
+
)
|
|
707
|
+
const note = str(args.flags, "comment")?.trim()
|
|
708
|
+
const comment = note
|
|
709
|
+
? store.addComment(
|
|
710
|
+
key,
|
|
711
|
+
`${args.flags.reopen ? "Reopened" : "Resolved"} ask [${ask.ordinal}] (${ask.text.split("\n")[0]}): ${note}`,
|
|
712
|
+
author,
|
|
713
|
+
)
|
|
714
|
+
: null
|
|
715
|
+
const task = store.get(key)!
|
|
716
|
+
// Branch-resolved asks aren't in the local files — count what's still open
|
|
717
|
+
// on the side that was just written.
|
|
718
|
+
const remaining = branch
|
|
719
|
+
? undefined
|
|
720
|
+
: task.asks.filter((a) => !a.resolvedAt).length
|
|
721
|
+
if (args.flags.json) {
|
|
722
|
+
console.log(JSON.stringify({ ask, branch, comment, openAsks: remaining ?? null }, null, 2))
|
|
723
|
+
return
|
|
724
|
+
}
|
|
725
|
+
const verb = args.flags.reopen ? "reopened" : "resolved"
|
|
726
|
+
console.log(`${verb} ${ask.taskId} [${ask.ordinal}] ${ask.text.split("\n")[0]}`)
|
|
727
|
+
if (branch) {
|
|
728
|
+
console.log(`committed and pushed to ${branch} — the worker sees it on its next pull`)
|
|
729
|
+
return
|
|
730
|
+
}
|
|
731
|
+
if (args.flags.reopen) return
|
|
732
|
+
if (remaining !== undefined && remaining > 0) {
|
|
733
|
+
console.log(`${remaining} ask${remaining === 1 ? "" : "s"} still open on ${ask.taskId}`)
|
|
734
|
+
} else if (task.status === "in_progress") {
|
|
735
|
+
console.log(`that was the last open ask — \`task move ${ask.taskId} done\` if the work is finished too`)
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* `task inbox` — what do I need to do NOW to unblock work: every open ask on
|
|
741
|
+
* effectively-in-progress tickets (file status, or a live claim branch —
|
|
742
|
+
* see inbox.ts), across the repo's boards, oldest owed first. Deliberately
|
|
743
|
+
* cross-board, like `claim --next`: the queue's attention is per-board, a
|
|
744
|
+
* person's isn't.
|
|
745
|
+
*/
|
|
746
|
+
function cmdInbox(args: Args): void {
|
|
747
|
+
let entries: InboxEntry[]
|
|
748
|
+
try {
|
|
749
|
+
const scope = findScopeRoot(process.cwd())
|
|
750
|
+
const boards = selectionBoards(process.cwd(), boardFlag(args)).map((store) => {
|
|
751
|
+
const rel = relative(scope, store.root).split(sep).join("/")
|
|
752
|
+
return { id: rel === "" ? "." : rel, store }
|
|
753
|
+
})
|
|
754
|
+
entries = buildInbox(boards)
|
|
755
|
+
} catch (error) {
|
|
756
|
+
if (error instanceof ClaimError) fail(error.message)
|
|
757
|
+
throw error
|
|
758
|
+
}
|
|
759
|
+
if (args.flags.json) {
|
|
760
|
+
console.log(JSON.stringify({ inbox: entries }, null, 2))
|
|
761
|
+
return
|
|
762
|
+
}
|
|
763
|
+
if (entries.length === 0) {
|
|
764
|
+
console.log("inbox zero — no open asks on in-progress work")
|
|
765
|
+
return
|
|
766
|
+
}
|
|
767
|
+
const open = entries.reduce((sum, e) => sum + e.asks.length, 0)
|
|
768
|
+
console.log(
|
|
769
|
+
`${open} open ask${open === 1 ? "" : "s"} across ${entries.length} ticket${entries.length === 1 ? "" : "s"}`,
|
|
770
|
+
)
|
|
771
|
+
const age = (iso: string): string => {
|
|
772
|
+
const days = Math.floor((Date.now() - Date.parse(iso)) / 86_400_000)
|
|
773
|
+
return days > 0 ? `${days}d` : "today"
|
|
774
|
+
}
|
|
775
|
+
for (const entry of entries) {
|
|
776
|
+
const via = entry.claim ? ` (claimed: ${entry.claim.branch})` : ""
|
|
777
|
+
console.log(`\n${entry.task.id} ${entry.task.title}${via}`)
|
|
778
|
+
for (const ask of entry.asks) {
|
|
779
|
+
const who = ask.author ? ` · ${ask.author}` : ""
|
|
780
|
+
console.log(` [${ask.ordinal}] ${ask.text.split("\n")[0]} (${age(ask.createdAt)}${who})`)
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
631
785
|
function cmdComment(args: Args): void {
|
|
632
786
|
const [ref, ...rest] = args.positional
|
|
633
787
|
const body = rest.join(" ").trim()
|
|
@@ -657,9 +811,9 @@ function cmdLink(args: Args, action: "link" | "unlink"): void {
|
|
|
657
811
|
const store = openStoreFor(ref)
|
|
658
812
|
const relation = blocks ? "blocks" : "blocked_by"
|
|
659
813
|
const target = parseRefOn(store, (blocks ?? blockedBy)!)
|
|
660
|
-
const
|
|
814
|
+
const key = store.parseId(ref)
|
|
661
815
|
const task =
|
|
662
|
-
action === "link" ? store.link(
|
|
816
|
+
action === "link" ? store.link(key, relation, target) : store.unlink(key, relation, target)
|
|
663
817
|
if (args.flags.json) {
|
|
664
818
|
console.log(JSON.stringify({ task }, null, 2))
|
|
665
819
|
} else {
|
|
@@ -712,7 +866,7 @@ function cmdClaim(args: Args): void {
|
|
|
712
866
|
}
|
|
713
867
|
if (!ref) fail(CLAIM_USAGE)
|
|
714
868
|
const store = openStoreFor(ref)
|
|
715
|
-
const
|
|
869
|
+
const key = store.parseId(ref)
|
|
716
870
|
if (args.flags.release) {
|
|
717
871
|
// A released claim must leave its failure context behind: the branch
|
|
718
872
|
// (and whatever was tried on it) is about to evaporate, so the reason
|
|
@@ -723,26 +877,26 @@ function cmdClaim(args: Args): void {
|
|
|
723
877
|
'task claim --release requires --comment "<why>" — the next worker inherits what was tried',
|
|
724
878
|
)
|
|
725
879
|
}
|
|
726
|
-
const result = release(store,
|
|
880
|
+
const result = release(store, key)
|
|
727
881
|
const commented = result.remote || result.local
|
|
728
882
|
// After the branch deletion, so the comment survives it: release leaves
|
|
729
883
|
// the checkout on the default branch, and the comment file sits there
|
|
730
884
|
// uncommitted, to ride along with the next commit.
|
|
731
885
|
const comment = commented
|
|
732
|
-
? store.addComment(
|
|
886
|
+
? store.addComment(key, why, resolveAuthor(str(args.flags, "author")).name)
|
|
733
887
|
: null
|
|
734
888
|
if (args.flags.json) {
|
|
735
889
|
console.log(JSON.stringify({ released: result, comment }, null, 2))
|
|
736
890
|
} else if (!commented) {
|
|
737
|
-
console.log(`${store.displayId(
|
|
891
|
+
console.log(`${store.displayId(key)} wasn't claimed — no ${result.branch} to delete`)
|
|
738
892
|
} else {
|
|
739
893
|
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(
|
|
894
|
+
console.log(`released ${store.displayId(key)} — deleted ${result.branch} (${where.join(" and ")})`)
|
|
895
|
+
console.log(`left the reason as a comment on ${store.displayId(key)} — uncommitted, commit it with your next change`)
|
|
742
896
|
}
|
|
743
897
|
return
|
|
744
898
|
}
|
|
745
|
-
printClaim(claim(store,
|
|
899
|
+
printClaim(claim(store, key, options))
|
|
746
900
|
} catch (error) {
|
|
747
901
|
if (error instanceof ClaimError) {
|
|
748
902
|
console.error(`error: ${error.message}`)
|
|
@@ -774,7 +928,10 @@ function cmdOverview(args: Args): void {
|
|
|
774
928
|
overview.claimed === null
|
|
775
929
|
? "claimed ? (origin unreachable)"
|
|
776
930
|
: `claimed ${overview.claimed.length}${overview.claimed.length ? ` (${overview.claimed.join(", ")})` : ""}`
|
|
777
|
-
|
|
931
|
+
const asks = overview.oldestAsk
|
|
932
|
+
? `asks ${overview.openAsks} open (oldest ${overview.oldestAsk.taskId}, ${overview.oldestAsk.createdAt.slice(0, 10)})`
|
|
933
|
+
: "asks 0 open"
|
|
934
|
+
console.log(` ${claimed} ${asks}`)
|
|
778
935
|
if (overview.goals.length) {
|
|
779
936
|
console.log("goals")
|
|
780
937
|
table(
|
|
@@ -967,9 +1124,9 @@ function cmdDelete(args: Args): void {
|
|
|
967
1124
|
const ref = args.positional[0]
|
|
968
1125
|
if (!ref) fail("usage: task delete <id>")
|
|
969
1126
|
const store = openStoreFor(ref)
|
|
970
|
-
const
|
|
971
|
-
store.delete(
|
|
972
|
-
console.log(`deleted ${store.displayId(
|
|
1127
|
+
const key = store.parseId(ref)
|
|
1128
|
+
store.delete(key)
|
|
1129
|
+
console.log(`deleted ${store.displayId(key)}`)
|
|
973
1130
|
}
|
|
974
1131
|
|
|
975
1132
|
/**
|
|
@@ -1165,11 +1322,14 @@ subdirectory (it walks up to find .task/, like git).
|
|
|
1165
1322
|
Usage
|
|
1166
1323
|
task init [--name <name>] [--prefix <PREFIX>]
|
|
1167
1324
|
task add <title> [--description <text> | --description-file <path|->]
|
|
1168
|
-
[--status <s>] [--tags <a,b>] [--goal <slug>]
|
|
1325
|
+
[--status <s>] [--tags <a,b>] [--goal <slug>]
|
|
1326
|
+
[--ask "<text>"]…
|
|
1169
1327
|
new tickets land in backlog (the dump list)
|
|
1170
1328
|
unless --status says otherwise;
|
|
1171
1329
|
--description-file reads markdown from a file
|
|
1172
|
-
(or stdin with -) — no shell-quoting fights
|
|
1330
|
+
(or stdin with -) — no shell-quoting fights;
|
|
1331
|
+
--ask (repeatable) files what a person owes
|
|
1332
|
+
the ticket, right at creation
|
|
1173
1333
|
task list [--status <s1,s2>] [--tag <a,b>] [--goal <slug>]
|
|
1174
1334
|
[--needs-human] [--all] [--archived] [--claimable]
|
|
1175
1335
|
task search <query> [--status <s1,s2>] [--tag <a,b>] [--goal <slug>]
|
|
@@ -1183,18 +1343,38 @@ Usage
|
|
|
1183
1343
|
the shared context without chasing it
|
|
1184
1344
|
task update <id> [--title <t>] [--description <text> | --description-file <path|->]
|
|
1185
1345
|
[--status <s>] [--tags <a,b>] [--goal <slug>]
|
|
1186
|
-
[--needs-human | --no-needs-human]
|
|
1187
1346
|
[--pr <url>] [--prs <url1,url2>]
|
|
1188
1347
|
task move <id> <status> change a ticket's status — the one general
|
|
1189
1348
|
status API (backlog todo in_progress done
|
|
1190
|
-
canceled).
|
|
1191
|
-
|
|
1192
|
-
|
|
1349
|
+
canceled). Moving to done is refused while
|
|
1350
|
+
the ticket has open asks: done means nobody
|
|
1351
|
+
owes anything. The named verbs below exist
|
|
1352
|
+
only where a transition does more than write
|
|
1353
|
+
the status field
|
|
1354
|
+
task ask <id> "<text>" file an ask: one thing a person owes this
|
|
1355
|
+
ticket before it can be done. Asks derive the
|
|
1356
|
+
needs-human badge, feed \`task inbox\`, and
|
|
1357
|
+
gate \`done\` — they never block claiming (an
|
|
1358
|
+
agent works the code while a person mints the
|
|
1359
|
+
token; a true precondition is a blocked_by on
|
|
1360
|
+
a thin human ticket instead)
|
|
1361
|
+
task resolve <id> <n> [--comment "<note>"] [--reopen]
|
|
1362
|
+
check ask <n> off (\`task show\` prints the
|
|
1363
|
+
ordinals); the note lands as an attributed
|
|
1364
|
+
comment. --reopen puts a resolved ask back
|
|
1365
|
+
task inbox [--board <P>] what needs YOU, now: every open ask on
|
|
1366
|
+
effectively in-progress tickets — file status
|
|
1367
|
+
in_progress, or a live claim branch on origin
|
|
1368
|
+
(a claimed ticket is in progress whatever the
|
|
1369
|
+
merged file says, and its asks are read from
|
|
1370
|
+
the branch). Cross-board like claim --next;
|
|
1371
|
+
oldest owed first. Backlog/todo asks are out
|
|
1372
|
+
by design — claiming is what makes them yours
|
|
1193
1373
|
task promote <id> backlog → todo, the human "build this" call.
|
|
1194
1374
|
No quality gate — planning happens at claim
|
|
1195
|
-
time. Blocked
|
|
1196
|
-
|
|
1197
|
-
|
|
1375
|
+
time. Blocked tickets still promote —
|
|
1376
|
+
blockers are claim-time gates, reported as a
|
|
1377
|
+
note — so promoting can queue work behind
|
|
1198
1378
|
its blockers. Exit codes: 0 promoted, 2
|
|
1199
1379
|
refused (not a backlog ticket)
|
|
1200
1380
|
task link <id> --blocked-by <id>
|
|
@@ -1202,7 +1382,7 @@ Usage
|
|
|
1202
1382
|
both tasks (A blocked by B ⇔ B blocks A)
|
|
1203
1383
|
task unlink <id> (--blocks <id> | --blocked-by <id>)
|
|
1204
1384
|
task claim <id> [--force] claim a ticket before working it: branch
|
|
1205
|
-
<branchPrefix><prefix>-<
|
|
1385
|
+
<branchPrefix><prefix>-<key> off origin's
|
|
1206
1386
|
default branch, ticket → in_progress as its
|
|
1207
1387
|
first commit, pushed. The namespace defaults
|
|
1208
1388
|
to task/claim/ — set claims.branchPrefix in
|
|
@@ -1215,8 +1395,7 @@ Usage
|
|
|
1215
1395
|
many claim branches exist on origin, claiming
|
|
1216
1396
|
is refused (--force overrides). Exit codes:
|
|
1217
1397
|
0 claimed, 1 already claimed, 2 not claimable
|
|
1218
|
-
(not todo, blocked,
|
|
1219
|
-
at the cap)
|
|
1398
|
+
(not todo, blocked, dirty tree, at the cap)
|
|
1220
1399
|
task claim --next [--board <P>] [--force]
|
|
1221
1400
|
claim the top claimable ticket in one call,
|
|
1222
1401
|
retrying past lost races internally — the
|
|
@@ -1235,15 +1414,15 @@ Usage
|
|
|
1235
1414
|
so the next worker inherits what was tried
|
|
1236
1415
|
task list --claimable [--board <P>]
|
|
1237
1416
|
the claim queue: todo tickets in position
|
|
1238
|
-
order, minus blocked /
|
|
1239
|
-
|
|
1240
|
-
|
|
1417
|
+
order, minus blocked / already claimed on
|
|
1418
|
+
origin. Top entry is next up. Same board
|
|
1419
|
+
scope as claim --next
|
|
1241
1420
|
task instructions print the shipped agent conventions
|
|
1242
1421
|
(skill/SKILL.md) — agents without the skill
|
|
1243
1422
|
installed self-serve the house rules
|
|
1244
1423
|
task overview [--json] one screen of board health: counts per
|
|
1245
1424
|
status, per-goal progress, claimed and
|
|
1246
|
-
|
|
1425
|
+
open-ask counts, stale tickets (untouched
|
|
1247
1426
|
${STALE_DAYS}+ days), oldest open ticket
|
|
1248
1427
|
task comment <id> <text> [--author <who>]
|
|
1249
1428
|
task delete <id>
|
|
@@ -1265,7 +1444,7 @@ Usage
|
|
|
1265
1444
|
task archive <id> move a done/canceled ticket to .task/archive/,
|
|
1266
1445
|
out of the board and off the hot path — still
|
|
1267
1446
|
readable via show and list --archived, and its
|
|
1268
|
-
|
|
1447
|
+
key stays reserved
|
|
1269
1448
|
task archive --all archive everything done or canceled
|
|
1270
1449
|
task unarchive <id> put an archived ticket back on the board
|
|
1271
1450
|
task whoami who your comments are attributed to
|
|
@@ -1297,15 +1476,18 @@ Usage
|
|
|
1297
1476
|
tasks — those are in .task/ either way
|
|
1298
1477
|
|
|
1299
1478
|
Values
|
|
1300
|
-
<id> TAS-
|
|
1301
|
-
|
|
1302
|
-
|
|
1479
|
+
<id> TAS-x7k4m, x7k4m, or any unique prefix of a key (task show x7
|
|
1480
|
+
works, git-style — ambiguity is an error naming the matches).
|
|
1481
|
+
Ids are random 5-character keys, minted branch-safely at
|
|
1482
|
+
\`task add\`. A bare key means the nearest board; a prefixed id
|
|
1483
|
+
routes to whichever board in the repo owns that prefix, so
|
|
1484
|
+
TAS-x7k4m works from anywhere in a monorepo
|
|
1303
1485
|
status ${STATUSES.join(" ")}
|
|
1304
1486
|
--tag a,b matches a task carrying *either* tag
|
|
1305
1487
|
--goal <slug> a task belongs to at most one goal. Goals answer "where is
|
|
1306
1488
|
this going" (one); tags answer "what kind" (many) — something
|
|
1307
1489
|
that wants two goals is a tag
|
|
1308
|
-
--needs-human
|
|
1490
|
+
--needs-human filter: tickets with open asks — something a person owes
|
|
1309
1491
|
--pr <url> attach a pull request (appends); --prs replaces the whole list
|
|
1310
1492
|
clearing --tags "" drops all tags, --goal "" clears it, --prs ""
|
|
1311
1493
|
detaches all PRs
|
|
@@ -1356,6 +1538,12 @@ function main(): void | Promise<void> {
|
|
|
1356
1538
|
return cmdClaim(args)
|
|
1357
1539
|
case "instructions":
|
|
1358
1540
|
return cmdInstructions()
|
|
1541
|
+
case "ask":
|
|
1542
|
+
return cmdAsk(args)
|
|
1543
|
+
case "resolve":
|
|
1544
|
+
return cmdResolve(args)
|
|
1545
|
+
case "inbox":
|
|
1546
|
+
return cmdInbox(args)
|
|
1359
1547
|
case "overview":
|
|
1360
1548
|
return cmdOverview(args)
|
|
1361
1549
|
case "goal":
|