@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/dist/cli.js
CHANGED
|
@@ -6,15 +6,17 @@
|
|
|
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
|
import { spawn } from "node:child_process";
|
|
12
12
|
import { readFileSync } from "node:fs";
|
|
13
|
-
import { basename, join } from "node:path";
|
|
13
|
+
import { basename, join, relative, sep } from "node:path";
|
|
14
14
|
import process from "node:process";
|
|
15
15
|
import { resolveAuthor } from "./author.js";
|
|
16
16
|
import { checkBoards, checkRoot } from "./check.js";
|
|
17
17
|
import { ClaimError, claim, claimNext, claimableTasks, promote, release, selectionBoards, } from "./claim.js";
|
|
18
|
+
import { addAskRouted, setAskResolvedRouted } from "./claim-io.js";
|
|
19
|
+
import { buildInbox } from "./inbox.js";
|
|
18
20
|
import { STALE_DAYS, buildOverview } from "./overview.js";
|
|
19
21
|
import { searchStore } from "./search.js";
|
|
20
22
|
import { detectRepo, parseSlug, publish, resolveHost } from "./publish.js";
|
|
@@ -45,7 +47,6 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
45
47
|
"version",
|
|
46
48
|
"yes",
|
|
47
49
|
"needs-human",
|
|
48
|
-
"no-needs-human",
|
|
49
50
|
"archived",
|
|
50
51
|
"claimable",
|
|
51
52
|
"release",
|
|
@@ -58,6 +59,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
58
59
|
"fix",
|
|
59
60
|
"next",
|
|
60
61
|
"force",
|
|
62
|
+
"reopen",
|
|
61
63
|
]);
|
|
62
64
|
/**
|
|
63
65
|
* Value flags that may repeat — `--board NIC --board TAS` accumulates in
|
|
@@ -65,14 +67,26 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
65
67
|
* last-one-wins.
|
|
66
68
|
*/
|
|
67
69
|
const REPEATABLE_FLAGS = new Set(["board"]);
|
|
70
|
+
/**
|
|
71
|
+
* Repeatable flags whose values are free text — collected verbatim into a
|
|
72
|
+
* list, never comma-joined, because an ask like "check A, then B" must
|
|
73
|
+
* survive as one ask.
|
|
74
|
+
*/
|
|
75
|
+
const MULTI_FLAGS = new Set(["ask"]);
|
|
68
76
|
function parseArgs(argv) {
|
|
69
77
|
const positional = [];
|
|
70
78
|
const flags = {};
|
|
79
|
+
const multi = {};
|
|
71
80
|
for (let i = 0; i < argv.length; i++) {
|
|
72
81
|
const arg = argv[i];
|
|
73
82
|
if (arg.startsWith("--")) {
|
|
74
83
|
const name = arg.slice(2);
|
|
75
84
|
const set = (key, value) => {
|
|
85
|
+
if (MULTI_FLAGS.has(key)) {
|
|
86
|
+
;
|
|
87
|
+
(multi[key] ??= []).push(value);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
76
90
|
const prior = flags[key];
|
|
77
91
|
flags[key] =
|
|
78
92
|
REPEATABLE_FLAGS.has(key) && typeof prior === "string" ? `${prior},${value}` : value;
|
|
@@ -92,7 +106,7 @@ function parseArgs(argv) {
|
|
|
92
106
|
positional.push(arg);
|
|
93
107
|
}
|
|
94
108
|
}
|
|
95
|
-
return { positional, flags };
|
|
109
|
+
return { positional, flags, multi };
|
|
96
110
|
}
|
|
97
111
|
function str(flags, name) {
|
|
98
112
|
const v = flags[name];
|
|
@@ -108,18 +122,18 @@ function openStore() {
|
|
|
108
122
|
fail("no .task directory found in this directory or any parent — run `task init` first");
|
|
109
123
|
return openBoard(root);
|
|
110
124
|
}
|
|
111
|
-
/** The prefix a ref carries, if any: "TAS-
|
|
125
|
+
/** The prefix a ref carries, if any: "TAS-x7k4m" → "TAS", "x7k4m" → null. */
|
|
112
126
|
function refPrefix(ref) {
|
|
113
|
-
const match = /^([A-Za-z0-9]+)
|
|
127
|
+
const match = /^([A-Za-z0-9]+)-[A-Za-z0-9]+$/.exec(ref.trim());
|
|
114
128
|
return match ? match[1].toUpperCase() : null;
|
|
115
129
|
}
|
|
116
130
|
/**
|
|
117
|
-
* The board a ref belongs to. A bare
|
|
131
|
+
* The board a ref belongs to. A bare key means the nearest board, as ever —
|
|
118
132
|
* but a prefixed id is an address, and it routes: if the prefix isn't the
|
|
119
133
|
* nearest board's, every board in the repo (walking up to the outermost board
|
|
120
|
-
* root, then down) is searched for it, so `task show TAS-
|
|
134
|
+
* root, then down) is searched for it, so `task show TAS-x7k4m` works from
|
|
121
135
|
* anywhere in a monorepo. What this must never do is what it used to: silently
|
|
122
|
-
* strip a foreign prefix and act on the nearest board's ticket of that
|
|
136
|
+
* strip a foreign prefix and act on the nearest board's ticket of that key.
|
|
123
137
|
*/
|
|
124
138
|
function openStoreFor(ref) {
|
|
125
139
|
const prefix = ref ? refPrefix(ref) : null;
|
|
@@ -205,10 +219,6 @@ function patchFromFlags(flags) {
|
|
|
205
219
|
const goal = str(flags, "goal");
|
|
206
220
|
if (goal !== undefined)
|
|
207
221
|
patch.goal = goal.trim() || null;
|
|
208
|
-
if (flags["needs-human"])
|
|
209
|
-
patch.needsHuman = true;
|
|
210
|
-
if (flags["no-needs-human"])
|
|
211
|
-
patch.needsHuman = false;
|
|
212
222
|
// Replace semantics like --tags; `--pr` (append one) is handled per-command
|
|
213
223
|
// because appending needs the task's current list.
|
|
214
224
|
const prs = str(flags, "prs");
|
|
@@ -268,18 +278,27 @@ function cmdInit(args) {
|
|
|
268
278
|
function cmdAdd(args) {
|
|
269
279
|
const title = args.positional.join(" ").trim();
|
|
270
280
|
if (!title)
|
|
271
|
-
fail(`usage: task add <title> [--description …] [--status …] [--tags a,b] [--goal <slug>] [--
|
|
281
|
+
fail(`usage: task add <title> [--description …] [--status …] [--tags a,b] [--goal <slug>] [--ask "<text>"]…`);
|
|
272
282
|
const store = openStore();
|
|
273
283
|
const patch = patchFromFlags(args.flags);
|
|
274
284
|
const pr = str(args.flags, "pr");
|
|
275
285
|
if (pr)
|
|
276
286
|
patch.prs = [...(patch.prs ?? []), pr];
|
|
277
|
-
|
|
287
|
+
let task = store.create({ title, ...patch });
|
|
288
|
+
const asks = (args.multi.ask ?? []).map((a) => a.trim()).filter(Boolean);
|
|
289
|
+
if (asks.length > 0) {
|
|
290
|
+
const author = resolveAuthor(str(args.flags, "author")).name;
|
|
291
|
+
for (const text of asks)
|
|
292
|
+
store.addAsk(task.key, text, author);
|
|
293
|
+
task = store.get(task.key);
|
|
294
|
+
}
|
|
278
295
|
if (args.flags.json) {
|
|
279
296
|
console.log(JSON.stringify({ task }, null, 2));
|
|
280
297
|
}
|
|
281
298
|
else {
|
|
282
299
|
printTask(task);
|
|
300
|
+
for (const ask of task.asks)
|
|
301
|
+
console.log(` ask [${ask.ordinal}] ${ask.text.split("\n")[0]}`);
|
|
283
302
|
}
|
|
284
303
|
}
|
|
285
304
|
/** `--board NIC --board TAS` (or `--board NIC,TAS`) → ["NIC", "TAS"]. */
|
|
@@ -460,7 +479,7 @@ function cmdArchive(args) {
|
|
|
460
479
|
}
|
|
461
480
|
const store = openStore();
|
|
462
481
|
const finished = store.list({ statuses: ["done", "canceled"] });
|
|
463
|
-
const archived = finished.map((t) => store.archive(t.
|
|
482
|
+
const archived = finished.map((t) => store.archive(t.key));
|
|
464
483
|
if (args.flags.json) {
|
|
465
484
|
console.log(JSON.stringify({ archived }, null, 2));
|
|
466
485
|
}
|
|
@@ -490,11 +509,11 @@ function cmdShow(args) {
|
|
|
490
509
|
if (!ref)
|
|
491
510
|
fail("usage: task show <id>");
|
|
492
511
|
const store = openStoreFor(ref);
|
|
493
|
-
const
|
|
494
|
-
const task = store.get(
|
|
512
|
+
const key = store.parseId(ref);
|
|
513
|
+
const task = store.get(key);
|
|
495
514
|
if (!task)
|
|
496
|
-
fail(`no such task: ${store.displayId(
|
|
497
|
-
const comments = store.comments(
|
|
515
|
+
fail(`no such task: ${store.displayId(key)}`);
|
|
516
|
+
const comments = store.comments(key);
|
|
498
517
|
// The goal rides along in full — title *and* description — so an agent
|
|
499
518
|
// picking the ticket up cold inherits the shared context without it being
|
|
500
519
|
// pasted into every ticket. null when unset or when the ref dangles.
|
|
@@ -504,18 +523,27 @@ function cmdShow(args) {
|
|
|
504
523
|
return;
|
|
505
524
|
}
|
|
506
525
|
// A linked task is only as useful as knowing whether it's still in the way.
|
|
507
|
-
const describeLinks = (
|
|
508
|
-
.map((
|
|
509
|
-
const other = store.get(
|
|
510
|
-
return other ? `${other.id} (${other.status})` : store.displayId(
|
|
526
|
+
const describeLinks = (keys) => keys
|
|
527
|
+
.map((k) => {
|
|
528
|
+
const other = store.get(k);
|
|
529
|
+
return other ? `${other.id} (${other.status})` : store.displayId(k);
|
|
511
530
|
})
|
|
512
531
|
.join(", ");
|
|
513
532
|
console.log(`${task.id} ${task.title}`);
|
|
514
533
|
console.log(`status ${task.status}`);
|
|
515
534
|
if (task.archived)
|
|
516
535
|
console.log(`archived yes — \`task unarchive ${task.id}\` to edit`);
|
|
517
|
-
|
|
518
|
-
|
|
536
|
+
const openAsks = task.asks.filter((a) => !a.resolvedAt);
|
|
537
|
+
if (openAsks.length) {
|
|
538
|
+
console.log(`needs a human — ${openAsks.length} open ask${openAsks.length === 1 ? "" : "s"}`);
|
|
539
|
+
}
|
|
540
|
+
for (const ask of task.asks) {
|
|
541
|
+
const state = ask.resolvedAt
|
|
542
|
+
? `✓ resolved${ask.resolvedBy ? ` by ${ask.resolvedBy}` : ""}`
|
|
543
|
+
: `○ open${ask.author ? ` · ${ask.author}` : ""} · ${ask.createdAt.slice(0, 10)}`;
|
|
544
|
+
console.log(`ask [${ask.ordinal}] ${ask.text.split("\n").join("\n ")}`);
|
|
545
|
+
console.log(` ${state}`);
|
|
546
|
+
}
|
|
519
547
|
if (task.tags.length)
|
|
520
548
|
console.log(`tags ${task.tags.join(", ")}`);
|
|
521
549
|
if (task.goal) {
|
|
@@ -548,17 +576,17 @@ function cmdUpdate(args) {
|
|
|
548
576
|
if (!ref)
|
|
549
577
|
fail("usage: task update <id> [--status …] [--title …] …");
|
|
550
578
|
const store = openStoreFor(ref);
|
|
551
|
-
const
|
|
579
|
+
const key = store.parseId(ref);
|
|
552
580
|
const patch = patchFromFlags(args.flags);
|
|
553
581
|
const pr = str(args.flags, "pr");
|
|
554
582
|
if (pr) {
|
|
555
583
|
// Append, dedup — `--pr <url>` is "attach this PR", not "replace the list".
|
|
556
|
-
const current = patch.prs ?? store.get(
|
|
584
|
+
const current = patch.prs ?? store.get(key)?.prs ?? [];
|
|
557
585
|
patch.prs = current.includes(pr) ? current : [...current, pr];
|
|
558
586
|
}
|
|
559
587
|
if (Object.keys(patch).length === 0)
|
|
560
588
|
fail("nothing to update — pass at least one flag");
|
|
561
|
-
const task = store.update(
|
|
589
|
+
const task = store.update(key, patch);
|
|
562
590
|
if (args.flags.json) {
|
|
563
591
|
console.log(JSON.stringify({ task }, null, 2));
|
|
564
592
|
}
|
|
@@ -570,7 +598,7 @@ function cmdMove(args) {
|
|
|
570
598
|
const [ref, status] = args.positional;
|
|
571
599
|
if (!ref || !status)
|
|
572
600
|
fail("usage: task move <id> <status>");
|
|
573
|
-
cmdUpdate({ positional: [ref], flags: { ...args.flags, status } });
|
|
601
|
+
cmdUpdate({ positional: [ref], flags: { ...args.flags, status }, multi: {} });
|
|
574
602
|
}
|
|
575
603
|
/**
|
|
576
604
|
* `task promote <id>` — backlog → todo, the human "build this" call; see
|
|
@@ -602,6 +630,121 @@ function cmdPromote(args) {
|
|
|
602
630
|
throw error;
|
|
603
631
|
}
|
|
604
632
|
}
|
|
633
|
+
/**
|
|
634
|
+
* `task ask <id> <text>` — file one thing a person owes this ticket. The ask
|
|
635
|
+
* is the primitive every human requirement funnels through: it derives the
|
|
636
|
+
* needs-human badge, feeds `task inbox`, and gates `done` until resolved.
|
|
637
|
+
* On a claimed ticket the write lands on the claim branch as a pushed commit
|
|
638
|
+
* (see claim-io.ts) — the worker's done gate has to see debt filed mid-claim.
|
|
639
|
+
*/
|
|
640
|
+
function cmdAsk(args) {
|
|
641
|
+
const [ref, ...rest] = args.positional;
|
|
642
|
+
const text = rest.join(" ").trim();
|
|
643
|
+
if (!ref || !text)
|
|
644
|
+
fail(`usage: task ask <id> "<what a person needs to do>" [--author <who>]`);
|
|
645
|
+
const store = openStoreFor(ref);
|
|
646
|
+
const author = resolveAuthor(str(args.flags, "author")).name;
|
|
647
|
+
const { ask, branch } = addAskRouted(store, store.parseId(ref), text, author);
|
|
648
|
+
if (args.flags.json) {
|
|
649
|
+
console.log(JSON.stringify({ ask, branch }, null, 2));
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
console.log(`asked on ${ask.taskId} [${ask.ordinal}] ${ask.text.split("\n")[0]}`);
|
|
653
|
+
if (branch)
|
|
654
|
+
console.log(`committed and pushed to ${branch} — the claim owns this ticket's asks`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* `task resolve <id> <n>` — check an ask off (`--reopen` puts it back). The
|
|
659
|
+
* ordinal is the one `task show` prints; the optional --comment lands as an
|
|
660
|
+
* attributed comment so the answer travels with the ticket, not just the
|
|
661
|
+
* checkbox. Resolving the last open ask is the moment a ticket usually moves
|
|
662
|
+
* to done — the hint below says so rather than doing it, because finishing a
|
|
663
|
+
* ticket is a statement about the *work*, not the asks.
|
|
664
|
+
*/
|
|
665
|
+
function cmdResolve(args) {
|
|
666
|
+
const [ref, askRef] = args.positional;
|
|
667
|
+
if (!ref || !askRef) {
|
|
668
|
+
fail('usage: task resolve <id> <ordinal> [--comment "<note>"] [--reopen] [--author <who>]');
|
|
669
|
+
}
|
|
670
|
+
const store = openStoreFor(ref);
|
|
671
|
+
const key = store.parseId(ref);
|
|
672
|
+
const author = resolveAuthor(str(args.flags, "author")).name;
|
|
673
|
+
const { ask, branch } = setAskResolvedRouted(store, key, askRef, !args.flags.reopen, author);
|
|
674
|
+
const note = str(args.flags, "comment")?.trim();
|
|
675
|
+
const comment = note
|
|
676
|
+
? store.addComment(key, `${args.flags.reopen ? "Reopened" : "Resolved"} ask [${ask.ordinal}] (${ask.text.split("\n")[0]}): ${note}`, author)
|
|
677
|
+
: null;
|
|
678
|
+
const task = store.get(key);
|
|
679
|
+
// Branch-resolved asks aren't in the local files — count what's still open
|
|
680
|
+
// on the side that was just written.
|
|
681
|
+
const remaining = branch
|
|
682
|
+
? undefined
|
|
683
|
+
: task.asks.filter((a) => !a.resolvedAt).length;
|
|
684
|
+
if (args.flags.json) {
|
|
685
|
+
console.log(JSON.stringify({ ask, branch, comment, openAsks: remaining ?? null }, null, 2));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const verb = args.flags.reopen ? "reopened" : "resolved";
|
|
689
|
+
console.log(`${verb} ${ask.taskId} [${ask.ordinal}] ${ask.text.split("\n")[0]}`);
|
|
690
|
+
if (branch) {
|
|
691
|
+
console.log(`committed and pushed to ${branch} — the worker sees it on its next pull`);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (args.flags.reopen)
|
|
695
|
+
return;
|
|
696
|
+
if (remaining !== undefined && remaining > 0) {
|
|
697
|
+
console.log(`${remaining} ask${remaining === 1 ? "" : "s"} still open on ${ask.taskId}`);
|
|
698
|
+
}
|
|
699
|
+
else if (task.status === "in_progress") {
|
|
700
|
+
console.log(`that was the last open ask — \`task move ${ask.taskId} done\` if the work is finished too`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* `task inbox` — what do I need to do NOW to unblock work: every open ask on
|
|
705
|
+
* effectively-in-progress tickets (file status, or a live claim branch —
|
|
706
|
+
* see inbox.ts), across the repo's boards, oldest owed first. Deliberately
|
|
707
|
+
* cross-board, like `claim --next`: the queue's attention is per-board, a
|
|
708
|
+
* person's isn't.
|
|
709
|
+
*/
|
|
710
|
+
function cmdInbox(args) {
|
|
711
|
+
let entries;
|
|
712
|
+
try {
|
|
713
|
+
const scope = findScopeRoot(process.cwd());
|
|
714
|
+
const boards = selectionBoards(process.cwd(), boardFlag(args)).map((store) => {
|
|
715
|
+
const rel = relative(scope, store.root).split(sep).join("/");
|
|
716
|
+
return { id: rel === "" ? "." : rel, store };
|
|
717
|
+
});
|
|
718
|
+
entries = buildInbox(boards);
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
if (error instanceof ClaimError)
|
|
722
|
+
fail(error.message);
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
if (args.flags.json) {
|
|
726
|
+
console.log(JSON.stringify({ inbox: entries }, null, 2));
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (entries.length === 0) {
|
|
730
|
+
console.log("inbox zero — no open asks on in-progress work");
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const open = entries.reduce((sum, e) => sum + e.asks.length, 0);
|
|
734
|
+
console.log(`${open} open ask${open === 1 ? "" : "s"} across ${entries.length} ticket${entries.length === 1 ? "" : "s"}`);
|
|
735
|
+
const age = (iso) => {
|
|
736
|
+
const days = Math.floor((Date.now() - Date.parse(iso)) / 86_400_000);
|
|
737
|
+
return days > 0 ? `${days}d` : "today";
|
|
738
|
+
};
|
|
739
|
+
for (const entry of entries) {
|
|
740
|
+
const via = entry.claim ? ` (claimed: ${entry.claim.branch})` : "";
|
|
741
|
+
console.log(`\n${entry.task.id} ${entry.task.title}${via}`);
|
|
742
|
+
for (const ask of entry.asks) {
|
|
743
|
+
const who = ask.author ? ` · ${ask.author}` : "";
|
|
744
|
+
console.log(` [${ask.ordinal}] ${ask.text.split("\n")[0]} (${age(ask.createdAt)}${who})`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
605
748
|
function cmdComment(args) {
|
|
606
749
|
const [ref, ...rest] = args.positional;
|
|
607
750
|
const body = rest.join(" ").trim();
|
|
@@ -632,8 +775,8 @@ function cmdLink(args, action) {
|
|
|
632
775
|
const store = openStoreFor(ref);
|
|
633
776
|
const relation = blocks ? "blocks" : "blocked_by";
|
|
634
777
|
const target = parseRefOn(store, (blocks ?? blockedBy));
|
|
635
|
-
const
|
|
636
|
-
const task = action === "link" ? store.link(
|
|
778
|
+
const key = store.parseId(ref);
|
|
779
|
+
const task = action === "link" ? store.link(key, relation, target) : store.unlink(key, relation, target);
|
|
637
780
|
if (args.flags.json) {
|
|
638
781
|
console.log(JSON.stringify({ task }, null, 2));
|
|
639
782
|
}
|
|
@@ -688,7 +831,7 @@ function cmdClaim(args) {
|
|
|
688
831
|
if (!ref)
|
|
689
832
|
fail(CLAIM_USAGE);
|
|
690
833
|
const store = openStoreFor(ref);
|
|
691
|
-
const
|
|
834
|
+
const key = store.parseId(ref);
|
|
692
835
|
if (args.flags.release) {
|
|
693
836
|
// A released claim must leave its failure context behind: the branch
|
|
694
837
|
// (and whatever was tried on it) is about to evaporate, so the reason
|
|
@@ -697,28 +840,28 @@ function cmdClaim(args) {
|
|
|
697
840
|
if (!why) {
|
|
698
841
|
fail('task claim --release requires --comment "<why>" — the next worker inherits what was tried');
|
|
699
842
|
}
|
|
700
|
-
const result = release(store,
|
|
843
|
+
const result = release(store, key);
|
|
701
844
|
const commented = result.remote || result.local;
|
|
702
845
|
// After the branch deletion, so the comment survives it: release leaves
|
|
703
846
|
// the checkout on the default branch, and the comment file sits there
|
|
704
847
|
// uncommitted, to ride along with the next commit.
|
|
705
848
|
const comment = commented
|
|
706
|
-
? store.addComment(
|
|
849
|
+
? store.addComment(key, why, resolveAuthor(str(args.flags, "author")).name)
|
|
707
850
|
: null;
|
|
708
851
|
if (args.flags.json) {
|
|
709
852
|
console.log(JSON.stringify({ released: result, comment }, null, 2));
|
|
710
853
|
}
|
|
711
854
|
else if (!commented) {
|
|
712
|
-
console.log(`${store.displayId(
|
|
855
|
+
console.log(`${store.displayId(key)} wasn't claimed — no ${result.branch} to delete`);
|
|
713
856
|
}
|
|
714
857
|
else {
|
|
715
858
|
const where = [result.remote && "origin", result.local && "local"].filter(Boolean);
|
|
716
|
-
console.log(`released ${store.displayId(
|
|
717
|
-
console.log(`left the reason as a comment on ${store.displayId(
|
|
859
|
+
console.log(`released ${store.displayId(key)} — deleted ${result.branch} (${where.join(" and ")})`);
|
|
860
|
+
console.log(`left the reason as a comment on ${store.displayId(key)} — uncommitted, commit it with your next change`);
|
|
718
861
|
}
|
|
719
862
|
return;
|
|
720
863
|
}
|
|
721
|
-
printClaim(claim(store,
|
|
864
|
+
printClaim(claim(store, key, options));
|
|
722
865
|
}
|
|
723
866
|
catch (error) {
|
|
724
867
|
if (error instanceof ClaimError) {
|
|
@@ -746,7 +889,10 @@ function cmdOverview(args) {
|
|
|
746
889
|
const claimed = overview.claimed === null
|
|
747
890
|
? "claimed ? (origin unreachable)"
|
|
748
891
|
: `claimed ${overview.claimed.length}${overview.claimed.length ? ` (${overview.claimed.join(", ")})` : ""}`;
|
|
749
|
-
|
|
892
|
+
const asks = overview.oldestAsk
|
|
893
|
+
? `asks ${overview.openAsks} open (oldest ${overview.oldestAsk.taskId}, ${overview.oldestAsk.createdAt.slice(0, 10)})`
|
|
894
|
+
: "asks 0 open";
|
|
895
|
+
console.log(` ${claimed} ${asks}`);
|
|
750
896
|
if (overview.goals.length) {
|
|
751
897
|
console.log("goals");
|
|
752
898
|
table(overview.goals.map((g) => [
|
|
@@ -946,9 +1092,9 @@ function cmdDelete(args) {
|
|
|
946
1092
|
if (!ref)
|
|
947
1093
|
fail("usage: task delete <id>");
|
|
948
1094
|
const store = openStoreFor(ref);
|
|
949
|
-
const
|
|
950
|
-
store.delete(
|
|
951
|
-
console.log(`deleted ${store.displayId(
|
|
1095
|
+
const key = store.parseId(ref);
|
|
1096
|
+
store.delete(key);
|
|
1097
|
+
console.log(`deleted ${store.displayId(key)}`);
|
|
952
1098
|
}
|
|
953
1099
|
/**
|
|
954
1100
|
* Bind the first free port at or after `first`, the way dev servers do: a board
|
|
@@ -1127,11 +1273,14 @@ subdirectory (it walks up to find .task/, like git).
|
|
|
1127
1273
|
Usage
|
|
1128
1274
|
task init [--name <name>] [--prefix <PREFIX>]
|
|
1129
1275
|
task add <title> [--description <text> | --description-file <path|->]
|
|
1130
|
-
[--status <s>] [--tags <a,b>] [--goal <slug>]
|
|
1276
|
+
[--status <s>] [--tags <a,b>] [--goal <slug>]
|
|
1277
|
+
[--ask "<text>"]…
|
|
1131
1278
|
new tickets land in backlog (the dump list)
|
|
1132
1279
|
unless --status says otherwise;
|
|
1133
1280
|
--description-file reads markdown from a file
|
|
1134
|
-
(or stdin with -) — no shell-quoting fights
|
|
1281
|
+
(or stdin with -) — no shell-quoting fights;
|
|
1282
|
+
--ask (repeatable) files what a person owes
|
|
1283
|
+
the ticket, right at creation
|
|
1135
1284
|
task list [--status <s1,s2>] [--tag <a,b>] [--goal <slug>]
|
|
1136
1285
|
[--needs-human] [--all] [--archived] [--claimable]
|
|
1137
1286
|
task search <query> [--status <s1,s2>] [--tag <a,b>] [--goal <slug>]
|
|
@@ -1145,18 +1294,38 @@ Usage
|
|
|
1145
1294
|
the shared context without chasing it
|
|
1146
1295
|
task update <id> [--title <t>] [--description <text> | --description-file <path|->]
|
|
1147
1296
|
[--status <s>] [--tags <a,b>] [--goal <slug>]
|
|
1148
|
-
[--needs-human | --no-needs-human]
|
|
1149
1297
|
[--pr <url>] [--prs <url1,url2>]
|
|
1150
1298
|
task move <id> <status> change a ticket's status — the one general
|
|
1151
1299
|
status API (backlog todo in_progress done
|
|
1152
|
-
canceled).
|
|
1153
|
-
|
|
1154
|
-
|
|
1300
|
+
canceled). Moving to done is refused while
|
|
1301
|
+
the ticket has open asks: done means nobody
|
|
1302
|
+
owes anything. The named verbs below exist
|
|
1303
|
+
only where a transition does more than write
|
|
1304
|
+
the status field
|
|
1305
|
+
task ask <id> "<text>" file an ask: one thing a person owes this
|
|
1306
|
+
ticket before it can be done. Asks derive the
|
|
1307
|
+
needs-human badge, feed \`task inbox\`, and
|
|
1308
|
+
gate \`done\` — they never block claiming (an
|
|
1309
|
+
agent works the code while a person mints the
|
|
1310
|
+
token; a true precondition is a blocked_by on
|
|
1311
|
+
a thin human ticket instead)
|
|
1312
|
+
task resolve <id> <n> [--comment "<note>"] [--reopen]
|
|
1313
|
+
check ask <n> off (\`task show\` prints the
|
|
1314
|
+
ordinals); the note lands as an attributed
|
|
1315
|
+
comment. --reopen puts a resolved ask back
|
|
1316
|
+
task inbox [--board <P>] what needs YOU, now: every open ask on
|
|
1317
|
+
effectively in-progress tickets — file status
|
|
1318
|
+
in_progress, or a live claim branch on origin
|
|
1319
|
+
(a claimed ticket is in progress whatever the
|
|
1320
|
+
merged file says, and its asks are read from
|
|
1321
|
+
the branch). Cross-board like claim --next;
|
|
1322
|
+
oldest owed first. Backlog/todo asks are out
|
|
1323
|
+
by design — claiming is what makes them yours
|
|
1155
1324
|
task promote <id> backlog → todo, the human "build this" call.
|
|
1156
1325
|
No quality gate — planning happens at claim
|
|
1157
|
-
time. Blocked
|
|
1158
|
-
|
|
1159
|
-
|
|
1326
|
+
time. Blocked tickets still promote —
|
|
1327
|
+
blockers are claim-time gates, reported as a
|
|
1328
|
+
note — so promoting can queue work behind
|
|
1160
1329
|
its blockers. Exit codes: 0 promoted, 2
|
|
1161
1330
|
refused (not a backlog ticket)
|
|
1162
1331
|
task link <id> --blocked-by <id>
|
|
@@ -1164,7 +1333,7 @@ Usage
|
|
|
1164
1333
|
both tasks (A blocked by B ⇔ B blocks A)
|
|
1165
1334
|
task unlink <id> (--blocks <id> | --blocked-by <id>)
|
|
1166
1335
|
task claim <id> [--force] claim a ticket before working it: branch
|
|
1167
|
-
<branchPrefix><prefix>-<
|
|
1336
|
+
<branchPrefix><prefix>-<key> off origin's
|
|
1168
1337
|
default branch, ticket → in_progress as its
|
|
1169
1338
|
first commit, pushed. The namespace defaults
|
|
1170
1339
|
to task/claim/ — set claims.branchPrefix in
|
|
@@ -1177,8 +1346,7 @@ Usage
|
|
|
1177
1346
|
many claim branches exist on origin, claiming
|
|
1178
1347
|
is refused (--force overrides). Exit codes:
|
|
1179
1348
|
0 claimed, 1 already claimed, 2 not claimable
|
|
1180
|
-
(not todo, blocked,
|
|
1181
|
-
at the cap)
|
|
1349
|
+
(not todo, blocked, dirty tree, at the cap)
|
|
1182
1350
|
task claim --next [--board <P>] [--force]
|
|
1183
1351
|
claim the top claimable ticket in one call,
|
|
1184
1352
|
retrying past lost races internally — the
|
|
@@ -1197,15 +1365,15 @@ Usage
|
|
|
1197
1365
|
so the next worker inherits what was tried
|
|
1198
1366
|
task list --claimable [--board <P>]
|
|
1199
1367
|
the claim queue: todo tickets in position
|
|
1200
|
-
order, minus blocked /
|
|
1201
|
-
|
|
1202
|
-
|
|
1368
|
+
order, minus blocked / already claimed on
|
|
1369
|
+
origin. Top entry is next up. Same board
|
|
1370
|
+
scope as claim --next
|
|
1203
1371
|
task instructions print the shipped agent conventions
|
|
1204
1372
|
(skill/SKILL.md) — agents without the skill
|
|
1205
1373
|
installed self-serve the house rules
|
|
1206
1374
|
task overview [--json] one screen of board health: counts per
|
|
1207
1375
|
status, per-goal progress, claimed and
|
|
1208
|
-
|
|
1376
|
+
open-ask counts, stale tickets (untouched
|
|
1209
1377
|
${STALE_DAYS}+ days), oldest open ticket
|
|
1210
1378
|
task comment <id> <text> [--author <who>]
|
|
1211
1379
|
task delete <id>
|
|
@@ -1227,7 +1395,7 @@ Usage
|
|
|
1227
1395
|
task archive <id> move a done/canceled ticket to .task/archive/,
|
|
1228
1396
|
out of the board and off the hot path — still
|
|
1229
1397
|
readable via show and list --archived, and its
|
|
1230
|
-
|
|
1398
|
+
key stays reserved
|
|
1231
1399
|
task archive --all archive everything done or canceled
|
|
1232
1400
|
task unarchive <id> put an archived ticket back on the board
|
|
1233
1401
|
task whoami who your comments are attributed to
|
|
@@ -1259,15 +1427,18 @@ Usage
|
|
|
1259
1427
|
tasks — those are in .task/ either way
|
|
1260
1428
|
|
|
1261
1429
|
Values
|
|
1262
|
-
<id> TAS-
|
|
1263
|
-
|
|
1264
|
-
|
|
1430
|
+
<id> TAS-x7k4m, x7k4m, or any unique prefix of a key (task show x7
|
|
1431
|
+
works, git-style — ambiguity is an error naming the matches).
|
|
1432
|
+
Ids are random 5-character keys, minted branch-safely at
|
|
1433
|
+
\`task add\`. A bare key means the nearest board; a prefixed id
|
|
1434
|
+
routes to whichever board in the repo owns that prefix, so
|
|
1435
|
+
TAS-x7k4m works from anywhere in a monorepo
|
|
1265
1436
|
status ${STATUSES.join(" ")}
|
|
1266
1437
|
--tag a,b matches a task carrying *either* tag
|
|
1267
1438
|
--goal <slug> a task belongs to at most one goal. Goals answer "where is
|
|
1268
1439
|
this going" (one); tags answer "what kind" (many) — something
|
|
1269
1440
|
that wants two goals is a tag
|
|
1270
|
-
--needs-human
|
|
1441
|
+
--needs-human filter: tickets with open asks — something a person owes
|
|
1271
1442
|
--pr <url> attach a pull request (appends); --prs replaces the whole list
|
|
1272
1443
|
clearing --tags "" drops all tags, --goal "" clears it, --prs ""
|
|
1273
1444
|
detaches all PRs
|
|
@@ -1315,6 +1486,12 @@ function main() {
|
|
|
1315
1486
|
return cmdClaim(args);
|
|
1316
1487
|
case "instructions":
|
|
1317
1488
|
return cmdInstructions();
|
|
1489
|
+
case "ask":
|
|
1490
|
+
return cmdAsk(args);
|
|
1491
|
+
case "resolve":
|
|
1492
|
+
return cmdResolve(args);
|
|
1493
|
+
case "inbox":
|
|
1494
|
+
return cmdInbox(args);
|
|
1318
1495
|
case "overview":
|
|
1319
1496
|
return cmdOverview(args);
|
|
1320
1497
|
case "goal":
|