@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.
Files changed (114) hide show
  1. package/README.md +65 -22
  2. package/dist/asks.test.d.ts +17 -0
  3. package/dist/asks.test.d.ts.map +1 -0
  4. package/dist/asks.test.js +278 -0
  5. package/dist/asks.test.js.map +1 -0
  6. package/dist/check.d.ts +3 -2
  7. package/dist/check.d.ts.map +1 -1
  8. package/dist/check.js +99 -60
  9. package/dist/check.js.map +1 -1
  10. package/dist/check.test.js +99 -60
  11. package/dist/check.test.js.map +1 -1
  12. package/dist/claim-io.d.ts +73 -0
  13. package/dist/claim-io.d.ts.map +1 -0
  14. package/dist/claim-io.js +344 -0
  15. package/dist/claim-io.js.map +1 -0
  16. package/dist/claim.d.ts +21 -22
  17. package/dist/claim.d.ts.map +1 -1
  18. package/dist/claim.js +58 -83
  19. package/dist/claim.js.map +1 -1
  20. package/dist/claim.test.d.ts +2 -2
  21. package/dist/claim.test.js +111 -94
  22. package/dist/claim.test.js.map +1 -1
  23. package/dist/cli.js +241 -64
  24. package/dist/cli.js.map +1 -1
  25. package/dist/file-store.d.ts +70 -24
  26. package/dist/file-store.d.ts.map +1 -1
  27. package/dist/file-store.js +319 -141
  28. package/dist/file-store.js.map +1 -1
  29. package/dist/git-serve.d.ts +183 -0
  30. package/dist/git-serve.d.ts.map +1 -0
  31. package/dist/git-serve.js +503 -0
  32. package/dist/git-serve.js.map +1 -0
  33. package/dist/git-serve.test.d.ts +16 -0
  34. package/dist/git-serve.test.d.ts.map +1 -0
  35. package/dist/git-serve.test.js +183 -0
  36. package/dist/git-serve.test.js.map +1 -0
  37. package/dist/git.d.ts +65 -0
  38. package/dist/git.d.ts.map +1 -0
  39. package/dist/git.js +114 -0
  40. package/dist/git.js.map +1 -0
  41. package/dist/id.d.ts +39 -0
  42. package/dist/id.d.ts.map +1 -0
  43. package/dist/id.js +67 -0
  44. package/dist/id.js.map +1 -0
  45. package/dist/inbox.d.ts +41 -0
  46. package/dist/inbox.d.ts.map +1 -0
  47. package/dist/inbox.js +56 -0
  48. package/dist/inbox.js.map +1 -0
  49. package/dist/overview.d.ts +9 -0
  50. package/dist/overview.d.ts.map +1 -1
  51. package/dist/overview.js +19 -11
  52. package/dist/overview.js.map +1 -1
  53. package/dist/overview.test.js +6 -5
  54. package/dist/overview.test.js.map +1 -1
  55. package/dist/promote.test.d.ts +3 -2
  56. package/dist/promote.test.d.ts.map +1 -1
  57. package/dist/promote.test.js +29 -31
  58. package/dist/promote.test.js.map +1 -1
  59. package/dist/publish.d.ts +2 -17
  60. package/dist/publish.d.ts.map +1 -1
  61. package/dist/publish.js +4 -49
  62. package/dist/publish.js.map +1 -1
  63. package/dist/search.d.ts +3 -1
  64. package/dist/search.d.ts.map +1 -1
  65. package/dist/search.js +15 -2
  66. package/dist/search.js.map +1 -1
  67. package/dist/search.test.js +9 -9
  68. package/dist/search.test.js.map +1 -1
  69. package/dist/server.d.ts.map +1 -1
  70. package/dist/server.js +218 -21
  71. package/dist/server.js.map +1 -1
  72. package/dist/store.d.ts +28 -14
  73. package/dist/store.d.ts.map +1 -1
  74. package/dist/store.js.map +1 -1
  75. package/dist/store.test.js +88 -75
  76. package/dist/store.test.js.map +1 -1
  77. package/dist/ticket-doc.d.ts +26 -10
  78. package/dist/ticket-doc.d.ts.map +1 -1
  79. package/dist/ticket-doc.js +71 -27
  80. package/dist/ticket-doc.js.map +1 -1
  81. package/dist/types.d.ts +51 -17
  82. package/dist/types.d.ts.map +1 -1
  83. package/dist/types.js.map +1 -1
  84. package/package.json +1 -1
  85. package/skill/SKILL.md +70 -23
  86. package/src/asks.test.ts +355 -0
  87. package/src/check.test.ts +129 -72
  88. package/src/check.ts +101 -58
  89. package/src/claim-io.ts +401 -0
  90. package/src/claim.test.ts +141 -99
  91. package/src/claim.ts +69 -96
  92. package/src/cli.ts +251 -63
  93. package/src/file-store.ts +357 -145
  94. package/src/git-serve.test.ts +240 -0
  95. package/src/git-serve.ts +595 -0
  96. package/src/git.ts +141 -0
  97. package/src/id.ts +68 -0
  98. package/src/inbox.ts +77 -0
  99. package/src/overview.test.ts +6 -5
  100. package/src/overview.ts +25 -10
  101. package/src/promote.test.ts +40 -28
  102. package/src/publish.ts +6 -53
  103. package/src/search.test.ts +11 -11
  104. package/src/search.ts +17 -3
  105. package/src/server.ts +221 -19
  106. package/src/store.test.ts +95 -77
  107. package/src/store.ts +28 -13
  108. package/src/ticket-doc.ts +93 -30
  109. package/src/types.ts +52 -17
  110. package/ui/dist/assets/index-BjsorZOU.js +229 -0
  111. package/ui/dist/assets/index-CoKCUYic.css +1 -0
  112. package/ui/dist/index.html +2 -2
  113. package/ui/dist/assets/index-BJmOsOdR.js +0 -229
  114. 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-3 done
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-12" → "TAS", "12" → null. */
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]+)-\d+$/.exec(ref.trim());
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 number means the nearest board, as ever —
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-12` works from
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 number.
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>] [--needs-human]`);
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
- const task = store.create({ title, ...patch });
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.number));
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 number = store.parseId(ref);
494
- const task = store.get(number);
512
+ const key = store.parseId(ref);
513
+ const task = store.get(key);
495
514
  if (!task)
496
- fail(`no such task: ${store.displayId(number)}`);
497
- const comments = store.comments(number);
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 = (numbers) => numbers
508
- .map((n) => {
509
- const other = store.get(n);
510
- return other ? `${other.id} (${other.status})` : store.displayId(n);
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
- if (task.needsHuman)
518
- console.log(`needs a human`);
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 number = store.parseId(ref);
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(number)?.prs ?? [];
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(number, patch);
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 number = store.parseId(ref);
636
- const task = action === "link" ? store.link(number, relation, target) : store.unlink(number, relation, target);
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 number = store.parseId(ref);
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, number);
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(number, why, resolveAuthor(str(args.flags, "author")).name)
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(number)} wasn't claimed — no ${result.branch} to delete`);
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(number)} — deleted ${result.branch} (${where.join(" and ")})`);
717
- console.log(`left the reason as a comment on ${store.displayId(number)} — uncommitted, commit it with your next change`);
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, number, options));
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
- console.log(` ${claimed} needs-human ${overview.needsHuman}`);
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 number = store.parseId(ref);
950
- store.delete(number);
951
- console.log(`deleted ${store.displayId(number)}`);
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>] [--needs-human]
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). The named verbs below exist only
1153
- where a transition does more than write the
1154
- status field
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 / needs-human tickets still
1158
- promote — those are claim-time gates, reported
1159
- as a note — so promoting can queue work behind
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>-<n> off origin's
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, needs-human, dirty tree,
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 / needs-human / already
1201
- claimed on origin. Top entry is next up. Same
1202
- board scope as claim --next
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
- needs-human counts, stale tickets (untouched
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
- number stays reserved
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-12, or just 12. A bare number means the nearest board; a
1263
- prefixed id routes to whichever board in the repo owns that
1264
- prefix, so TAS-12 works from anywhere in a monorepo
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 this can't be finished by an agent alone
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":