@nickmeriano/task 0.7.1 → 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 +73 -25
- 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 +43 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +403 -0
- package/dist/check.js.map +1 -0
- package/dist/check.test.d.ts +9 -0
- package/dist/check.test.d.ts.map +1 -0
- package/dist/check.test.js +248 -0
- package/dist/check.test.js.map +1 -0
- 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 +61 -9
- package/dist/claim.d.ts.map +1 -1
- package/dist/claim.js +197 -67
- package/dist/claim.js.map +1 -1
- package/dist/claim.test.d.ts +2 -2
- package/dist/claim.test.js +235 -64
- package/dist/claim.test.js.map +1 -1
- package/dist/cli.js +724 -136
- package/dist/cli.js.map +1 -1
- package/dist/file-store.d.ts +110 -38
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +514 -238
- 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/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/overview.d.ts +52 -0
- package/dist/overview.d.ts.map +1 -0
- package/dist/overview.js +61 -0
- package/dist/overview.js.map +1 -0
- package/dist/overview.test.d.ts +8 -0
- package/dist/overview.test.d.ts.map +1 -0
- package/dist/overview.test.js +48 -0
- package/dist/overview.test.js.map +1 -0
- package/dist/promote.test.d.ts +15 -0
- package/dist/promote.test.d.ts.map +1 -0
- package/dist/promote.test.js +104 -0
- package/dist/promote.test.js.map +1 -0
- 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 +34 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +79 -0
- package/dist/search.js.map +1 -0
- package/dist/search.test.d.ts +2 -0
- package/dist/search.test.d.ts.map +1 -0
- package/dist/search.test.js +53 -0
- package/dist/search.test.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +228 -23
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +43 -63
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +0 -368
- package/dist/store.js.map +1 -1
- package/dist/store.test.d.ts +1 -2
- package/dist/store.test.d.ts.map +1 -1
- package/dist/store.test.js +148 -106
- package/dist/store.test.js.map +1 -1
- package/dist/ticket-doc.d.ts +74 -5
- package/dist/ticket-doc.d.ts.map +1 -1
- package/dist/ticket-doc.js +229 -15
- package/dist/ticket-doc.js.map +1 -1
- package/dist/types.d.ts +115 -28
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +153 -40
- package/src/asks.test.ts +355 -0
- package/src/check.test.ts +328 -0
- package/src/check.ts +497 -0
- package/src/claim-io.ts +401 -0
- package/src/claim.test.ts +301 -71
- package/src/claim.ts +238 -81
- package/src/cli.ts +740 -131
- package/src/file-store.ts +572 -254
- 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/index.ts +4 -2
- package/src/overview.test.ts +52 -0
- package/src/overview.ts +105 -0
- package/src/promote.test.ts +143 -0
- package/src/publish.ts +6 -53
- package/src/search.test.ts +64 -0
- package/src/search.ts +105 -0
- package/src/server.ts +232 -21
- package/src/store.test.ts +166 -116
- package/src/store.ts +46 -444
- package/src/ticket-doc.ts +284 -21
- package/src/types.ts +120 -28
- 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-COunM-QN.css +0 -1
- package/ui/dist/assets/index-D4homvrQ.js +0 -229
package/dist/cli.js
CHANGED
|
@@ -1,32 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// The `task` CLI — tasks that live in your repo. Zero dependencies: hand-rolled
|
|
3
|
-
// flag parsing, plain-text tickets in .task/tickets/ for storage
|
|
4
|
-
//
|
|
5
|
-
// agents).
|
|
3
|
+
// flag parsing, plain-text tickets in .task/tickets/ for storage, plain text
|
|
4
|
+
// out (--json for agents).
|
|
6
5
|
//
|
|
7
6
|
// task init
|
|
8
|
-
// task add "Wire up webhooks" --tags api,infra --
|
|
7
|
+
// task add "Wire up webhooks" --tags api,infra --goal launch
|
|
9
8
|
// task list --status todo,in_progress
|
|
10
|
-
// task
|
|
9
|
+
// task move TAS-x7k4m done
|
|
11
10
|
// task serve
|
|
12
11
|
import { spawn } from "node:child_process";
|
|
13
12
|
import { readFileSync } from "node:fs";
|
|
14
|
-
import { basename, join } from "node:path";
|
|
13
|
+
import { basename, join, relative, sep } from "node:path";
|
|
15
14
|
import process from "node:process";
|
|
16
15
|
import { resolveAuthor } from "./author.js";
|
|
17
|
-
import {
|
|
16
|
+
import { checkBoards, checkRoot } from "./check.js";
|
|
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";
|
|
20
|
+
import { STALE_DAYS, buildOverview } from "./overview.js";
|
|
21
|
+
import { searchStore } from "./search.js";
|
|
18
22
|
import { detectRepo, parseSlug, publish, resolveHost } from "./publish.js";
|
|
19
23
|
import { createTaskServer } from "./server.js";
|
|
20
|
-
import { initProject,
|
|
24
|
+
import { initProject, openBoard } from "./file-store.js";
|
|
21
25
|
import { CONFIG_FILE, TASK_DIR, boardConfig, findBoards, findBoardsByPrefix, findRoot, findScopeRoot, } from "./store.js";
|
|
22
26
|
import { STATUSES, isStatus } from "./types.js";
|
|
23
|
-
// node:sqlite still emits an ExperimentalWarning on Node 22 — noise in a CLI
|
|
24
|
-
// that runs it on every invocation. Filter that one warning, keep the rest.
|
|
25
|
-
process.removeAllListeners("warning");
|
|
26
|
-
process.on("warning", (warning) => {
|
|
27
|
-
if (warning.name !== "ExperimentalWarning")
|
|
28
|
-
console.error(warning);
|
|
29
|
-
});
|
|
30
27
|
/**
|
|
31
28
|
* Read from package.json rather than duplicated as a literal. Releases here are
|
|
32
29
|
* cut by changesets, which bumps package.json and nothing else — so a hardcoded
|
|
@@ -50,7 +47,6 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
50
47
|
"version",
|
|
51
48
|
"yes",
|
|
52
49
|
"needs-human",
|
|
53
|
-
"no-needs-human",
|
|
54
50
|
"archived",
|
|
55
51
|
"claimable",
|
|
56
52
|
"release",
|
|
@@ -60,30 +56,57 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
60
56
|
"public",
|
|
61
57
|
"private",
|
|
62
58
|
"no-wait",
|
|
59
|
+
"fix",
|
|
60
|
+
"next",
|
|
61
|
+
"force",
|
|
62
|
+
"reopen",
|
|
63
63
|
]);
|
|
64
|
+
/**
|
|
65
|
+
* Value flags that may repeat — `--board NIC --board TAS` accumulates in
|
|
66
|
+
* given order, equivalent to `--board NIC,TAS`. Everything else keeps
|
|
67
|
+
* last-one-wins.
|
|
68
|
+
*/
|
|
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"]);
|
|
64
76
|
function parseArgs(argv) {
|
|
65
77
|
const positional = [];
|
|
66
78
|
const flags = {};
|
|
79
|
+
const multi = {};
|
|
67
80
|
for (let i = 0; i < argv.length; i++) {
|
|
68
81
|
const arg = argv[i];
|
|
69
82
|
if (arg.startsWith("--")) {
|
|
70
83
|
const name = arg.slice(2);
|
|
84
|
+
const set = (key, value) => {
|
|
85
|
+
if (MULTI_FLAGS.has(key)) {
|
|
86
|
+
;
|
|
87
|
+
(multi[key] ??= []).push(value);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const prior = flags[key];
|
|
91
|
+
flags[key] =
|
|
92
|
+
REPEATABLE_FLAGS.has(key) && typeof prior === "string" ? `${prior},${value}` : value;
|
|
93
|
+
};
|
|
71
94
|
const eq = name.indexOf("=");
|
|
72
95
|
if (eq >= 0) {
|
|
73
|
-
|
|
96
|
+
set(name.slice(0, eq), name.slice(eq + 1));
|
|
74
97
|
}
|
|
75
98
|
else if (BOOLEAN_FLAGS.has(name) || i + 1 >= argv.length || argv[i + 1].startsWith("--")) {
|
|
76
99
|
flags[name] = true;
|
|
77
100
|
}
|
|
78
101
|
else {
|
|
79
|
-
|
|
102
|
+
set(name, argv[++i]);
|
|
80
103
|
}
|
|
81
104
|
}
|
|
82
105
|
else {
|
|
83
106
|
positional.push(arg);
|
|
84
107
|
}
|
|
85
108
|
}
|
|
86
|
-
return { positional, flags };
|
|
109
|
+
return { positional, flags, multi };
|
|
87
110
|
}
|
|
88
111
|
function str(flags, name) {
|
|
89
112
|
const v = flags[name];
|
|
@@ -99,18 +122,18 @@ function openStore() {
|
|
|
99
122
|
fail("no .task directory found in this directory or any parent — run `task init` first");
|
|
100
123
|
return openBoard(root);
|
|
101
124
|
}
|
|
102
|
-
/** The prefix a ref carries, if any: "TAS-
|
|
125
|
+
/** The prefix a ref carries, if any: "TAS-x7k4m" → "TAS", "x7k4m" → null. */
|
|
103
126
|
function refPrefix(ref) {
|
|
104
|
-
const match = /^([A-Za-z0-9]+)
|
|
127
|
+
const match = /^([A-Za-z0-9]+)-[A-Za-z0-9]+$/.exec(ref.trim());
|
|
105
128
|
return match ? match[1].toUpperCase() : null;
|
|
106
129
|
}
|
|
107
130
|
/**
|
|
108
|
-
* The board a ref belongs to. A bare
|
|
131
|
+
* The board a ref belongs to. A bare key means the nearest board, as ever —
|
|
109
132
|
* but a prefixed id is an address, and it routes: if the prefix isn't the
|
|
110
133
|
* nearest board's, every board in the repo (walking up to the outermost board
|
|
111
|
-
* 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
|
|
112
135
|
* anywhere in a monorepo. What this must never do is what it used to: silently
|
|
113
|
-
* 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.
|
|
114
137
|
*/
|
|
115
138
|
function openStoreFor(ref) {
|
|
116
139
|
const prefix = ref ? refPrefix(ref) : null;
|
|
@@ -170,21 +193,32 @@ function patchFromFlags(flags) {
|
|
|
170
193
|
if (title !== undefined)
|
|
171
194
|
patch.title = title;
|
|
172
195
|
const description = str(flags, "description") ?? str(flags, "desc");
|
|
196
|
+
// Long markdown doesn't survive shell quoting — `--description-file` keeps
|
|
197
|
+
// agents writing plans and specs on the CLI path instead of hand-editing
|
|
198
|
+
// ticket files. `-` reads stdin.
|
|
199
|
+
const descriptionFile = str(flags, "description-file");
|
|
200
|
+
if (description !== undefined && descriptionFile !== undefined) {
|
|
201
|
+
fail("pass --description or --description-file, not both");
|
|
202
|
+
}
|
|
173
203
|
if (description !== undefined)
|
|
174
204
|
patch.description = description;
|
|
205
|
+
if (descriptionFile !== undefined) {
|
|
206
|
+
try {
|
|
207
|
+
patch.description = readFileSync(descriptionFile === "-" ? 0 : descriptionFile, "utf8");
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
fail(`--description-file: can't read ${descriptionFile === "-" ? "stdin" : descriptionFile}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
175
213
|
const status = str(flags, "status");
|
|
176
214
|
if (status !== undefined)
|
|
177
215
|
patch.status = parseStatus(status);
|
|
178
216
|
const tags = str(flags, "tags") ?? str(flags, "tag");
|
|
179
217
|
if (tags !== undefined)
|
|
180
218
|
patch.tags = parseTags(tags);
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
patch.
|
|
184
|
-
if (flags["needs-human"])
|
|
185
|
-
patch.needsHuman = true;
|
|
186
|
-
if (flags["no-needs-human"])
|
|
187
|
-
patch.needsHuman = false;
|
|
219
|
+
const goal = str(flags, "goal");
|
|
220
|
+
if (goal !== undefined)
|
|
221
|
+
patch.goal = goal.trim() || null;
|
|
188
222
|
// Replace semantics like --tags; `--pr` (append one) is handled per-command
|
|
189
223
|
// because appending needs the task's current list.
|
|
190
224
|
const prs = str(flags, "prs");
|
|
@@ -216,7 +250,7 @@ function taskRow(t) {
|
|
|
216
250
|
t.needsHuman ? "⚑" : "",
|
|
217
251
|
t.title,
|
|
218
252
|
t.tags.join(","),
|
|
219
|
-
t.
|
|
253
|
+
t.goal ?? "",
|
|
220
254
|
];
|
|
221
255
|
}
|
|
222
256
|
function printTask(t) {
|
|
@@ -244,22 +278,40 @@ function cmdInit(args) {
|
|
|
244
278
|
function cmdAdd(args) {
|
|
245
279
|
const title = args.positional.join(" ").trim();
|
|
246
280
|
if (!title)
|
|
247
|
-
fail(`usage: task add <title> [--description …] [--status …] [--tags a,b] [--
|
|
281
|
+
fail(`usage: task add <title> [--description …] [--status …] [--tags a,b] [--goal <slug>] [--ask "<text>"]…`);
|
|
248
282
|
const store = openStore();
|
|
249
283
|
const patch = patchFromFlags(args.flags);
|
|
250
284
|
const pr = str(args.flags, "pr");
|
|
251
285
|
if (pr)
|
|
252
286
|
patch.prs = [...(patch.prs ?? []), pr];
|
|
253
|
-
|
|
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
|
+
}
|
|
254
295
|
if (args.flags.json) {
|
|
255
296
|
console.log(JSON.stringify({ task }, null, 2));
|
|
256
297
|
}
|
|
257
298
|
else {
|
|
258
299
|
printTask(task);
|
|
300
|
+
for (const ask of task.asks)
|
|
301
|
+
console.log(` ask [${ask.ordinal}] ${ask.text.split("\n")[0]}`);
|
|
259
302
|
}
|
|
260
303
|
}
|
|
304
|
+
/** `--board NIC --board TAS` (or `--board NIC,TAS`) → ["NIC", "TAS"]. */
|
|
305
|
+
function boardFlag(args) {
|
|
306
|
+
const raw = str(args.flags, "board");
|
|
307
|
+
if (raw === undefined)
|
|
308
|
+
return undefined;
|
|
309
|
+
const prefixes = raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
310
|
+
if (prefixes.length === 0)
|
|
311
|
+
fail("--board needs a prefix, e.g. --board TAS");
|
|
312
|
+
return prefixes;
|
|
313
|
+
}
|
|
261
314
|
function cmdList(args) {
|
|
262
|
-
const store = openStore();
|
|
263
315
|
if (args.flags.claimable) {
|
|
264
316
|
// The dispatcher's queue view (TAS-21): claimable already pins the status
|
|
265
317
|
// set and consults origin, so the board-shaping flags don't compose.
|
|
@@ -267,7 +319,17 @@ function cmdList(args) {
|
|
|
267
319
|
if (args.flags[flag] !== undefined)
|
|
268
320
|
fail(`--claimable can't be combined with --${flag}`);
|
|
269
321
|
}
|
|
270
|
-
|
|
322
|
+
// Selection scope, same resolution as `claim --next`: --board flag > root
|
|
323
|
+
// board's `boards` list > nearest board. Board order is priority order.
|
|
324
|
+
let tasks;
|
|
325
|
+
try {
|
|
326
|
+
tasks = selectionBoards(process.cwd(), boardFlag(args)).flatMap((s) => claimableTasks(s));
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
if (error instanceof ClaimError)
|
|
330
|
+
fail(error.message);
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
271
333
|
if (args.flags.json) {
|
|
272
334
|
console.log(JSON.stringify({ tasks }, null, 2));
|
|
273
335
|
}
|
|
@@ -279,6 +341,9 @@ function cmdList(args) {
|
|
|
279
341
|
}
|
|
280
342
|
return;
|
|
281
343
|
}
|
|
344
|
+
if (args.flags.board !== undefined)
|
|
345
|
+
fail("--board only applies to list --claimable");
|
|
346
|
+
const store = openStore();
|
|
282
347
|
const archived = Boolean(args.flags.archived);
|
|
283
348
|
const statusFlag = str(args.flags, "status");
|
|
284
349
|
const statuses = statusFlag
|
|
@@ -292,7 +357,7 @@ function cmdList(args) {
|
|
|
292
357
|
const tasks = store.list({
|
|
293
358
|
statuses,
|
|
294
359
|
tags: tagFlag ? parseTags(tagFlag) : undefined,
|
|
295
|
-
|
|
360
|
+
goal: str(args.flags, "goal"),
|
|
296
361
|
needsHuman: args.flags["needs-human"] ? true : undefined,
|
|
297
362
|
archived: archived || undefined,
|
|
298
363
|
});
|
|
@@ -313,6 +378,43 @@ function cmdList(args) {
|
|
|
313
378
|
table(tasks.map(taskRow));
|
|
314
379
|
}
|
|
315
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* `task search <query>` — find tickets by what they say. Case-insensitive
|
|
383
|
+
* substring over titles, descriptions and comments, scoped by the usual list
|
|
384
|
+
* filters; `--archived` searches the archive *in addition to* the board (a
|
|
385
|
+
* search is a question about everything ever written down, unlike
|
|
386
|
+
* `list --archived`, which is a view of one place).
|
|
387
|
+
*/
|
|
388
|
+
function cmdSearch(args) {
|
|
389
|
+
const query = args.positional.join(" ").trim();
|
|
390
|
+
if (!query) {
|
|
391
|
+
fail("usage: task search <query> [--status <s1,s2>] [--tag <a,b>] [--goal <slug>] [--needs-human] [--archived]");
|
|
392
|
+
}
|
|
393
|
+
const store = openStore();
|
|
394
|
+
const statusFlag = str(args.flags, "status");
|
|
395
|
+
const tagFlag = str(args.flags, "tags") ?? str(args.flags, "tag");
|
|
396
|
+
const results = searchStore(store, query, {
|
|
397
|
+
statuses: statusFlag ? statusFlag.split(",").map(parseStatus) : undefined,
|
|
398
|
+
tags: tagFlag ? parseTags(tagFlag) : undefined,
|
|
399
|
+
goal: str(args.flags, "goal"),
|
|
400
|
+
needsHuman: args.flags["needs-human"] ? true : undefined,
|
|
401
|
+
}, Boolean(args.flags.archived));
|
|
402
|
+
if (args.flags.json) {
|
|
403
|
+
console.log(JSON.stringify({ query, results }, null, 2));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (results.length === 0) {
|
|
407
|
+
console.log(`no matches for "${query}"${args.flags.archived ? "" : " (--archived searches the archive too)"}`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
for (const { task, matches } of results) {
|
|
411
|
+
console.log(`${task.id} ${STATUS_GLYPH[task.status]} ${task.status}${task.archived ? " (archived)" : ""} ${task.title}`);
|
|
412
|
+
for (const match of matches) {
|
|
413
|
+
const label = match.field === "comment" ? `comment${match.author ? ` by ${match.author}` : ""}` : match.field;
|
|
414
|
+
console.log(` ${label}: ${match.snippet}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
316
418
|
/**
|
|
317
419
|
* `task boards` — every board in this repo, found the way `task serve` finds
|
|
318
420
|
* them but anchored at the *outermost* board root, so it answers from anywhere
|
|
@@ -377,7 +479,7 @@ function cmdArchive(args) {
|
|
|
377
479
|
}
|
|
378
480
|
const store = openStore();
|
|
379
481
|
const finished = store.list({ statuses: ["done", "canceled"] });
|
|
380
|
-
const archived = finished.map((t) => store.archive(t.
|
|
482
|
+
const archived = finished.map((t) => store.archive(t.key));
|
|
381
483
|
if (args.flags.json) {
|
|
382
484
|
console.log(JSON.stringify({ archived }, null, 2));
|
|
383
485
|
}
|
|
@@ -407,32 +509,46 @@ function cmdShow(args) {
|
|
|
407
509
|
if (!ref)
|
|
408
510
|
fail("usage: task show <id>");
|
|
409
511
|
const store = openStoreFor(ref);
|
|
410
|
-
const
|
|
411
|
-
const task = store.get(
|
|
512
|
+
const key = store.parseId(ref);
|
|
513
|
+
const task = store.get(key);
|
|
412
514
|
if (!task)
|
|
413
|
-
fail(`no such task: ${store.displayId(
|
|
414
|
-
const comments = store.comments(
|
|
515
|
+
fail(`no such task: ${store.displayId(key)}`);
|
|
516
|
+
const comments = store.comments(key);
|
|
517
|
+
// The goal rides along in full — title *and* description — so an agent
|
|
518
|
+
// picking the ticket up cold inherits the shared context without it being
|
|
519
|
+
// pasted into every ticket. null when unset or when the ref dangles.
|
|
520
|
+
const goal = task.goal ? store.getGoal(task.goal) : null;
|
|
415
521
|
if (args.flags.json) {
|
|
416
|
-
console.log(JSON.stringify({ task, comments }, null, 2));
|
|
522
|
+
console.log(JSON.stringify({ task, comments, goal }, null, 2));
|
|
417
523
|
return;
|
|
418
524
|
}
|
|
419
525
|
// A linked task is only as useful as knowing whether it's still in the way.
|
|
420
|
-
const describeLinks = (
|
|
421
|
-
.map((
|
|
422
|
-
const other = store.get(
|
|
423
|
-
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);
|
|
424
530
|
})
|
|
425
531
|
.join(", ");
|
|
426
532
|
console.log(`${task.id} ${task.title}`);
|
|
427
533
|
console.log(`status ${task.status}`);
|
|
428
534
|
if (task.archived)
|
|
429
535
|
console.log(`archived yes — \`task unarchive ${task.id}\` to edit`);
|
|
430
|
-
|
|
431
|
-
|
|
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
|
+
}
|
|
432
547
|
if (task.tags.length)
|
|
433
548
|
console.log(`tags ${task.tags.join(", ")}`);
|
|
434
|
-
if (task.
|
|
435
|
-
console.log(`
|
|
549
|
+
if (task.goal) {
|
|
550
|
+
console.log(`goal ${task.goal}${goal ? ` — ${goal.title}${goal.archived ? " (archived)" : ""}` : ""}`);
|
|
551
|
+
}
|
|
436
552
|
if (task.blockedBy.length)
|
|
437
553
|
console.log(`blocked by ${describeLinks(task.blockedBy)}`);
|
|
438
554
|
if (task.blocks.length)
|
|
@@ -443,6 +559,10 @@ function cmdShow(args) {
|
|
|
443
559
|
console.log(`updated ${task.updatedAt}`);
|
|
444
560
|
if (task.description)
|
|
445
561
|
console.log(`\n${task.description}`);
|
|
562
|
+
if (goal?.description) {
|
|
563
|
+
console.log(`\n── goal: ${goal.title} (${goal.slug})`);
|
|
564
|
+
console.log(goal.description.split("\n").map((line) => ` ${line}`).join("\n"));
|
|
565
|
+
}
|
|
446
566
|
if (comments.length) {
|
|
447
567
|
console.log(``);
|
|
448
568
|
for (const c of comments) {
|
|
@@ -451,24 +571,22 @@ function cmdShow(args) {
|
|
|
451
571
|
}
|
|
452
572
|
}
|
|
453
573
|
}
|
|
454
|
-
function cmdUpdate(args
|
|
574
|
+
function cmdUpdate(args) {
|
|
455
575
|
const ref = args.positional[0];
|
|
456
576
|
if (!ref)
|
|
457
577
|
fail("usage: task update <id> [--status …] [--title …] …");
|
|
458
578
|
const store = openStoreFor(ref);
|
|
459
|
-
const
|
|
579
|
+
const key = store.parseId(ref);
|
|
460
580
|
const patch = patchFromFlags(args.flags);
|
|
461
|
-
if (forcedStatus)
|
|
462
|
-
patch.status = forcedStatus;
|
|
463
581
|
const pr = str(args.flags, "pr");
|
|
464
582
|
if (pr) {
|
|
465
583
|
// Append, dedup — `--pr <url>` is "attach this PR", not "replace the list".
|
|
466
|
-
const current = patch.prs ?? store.get(
|
|
584
|
+
const current = patch.prs ?? store.get(key)?.prs ?? [];
|
|
467
585
|
patch.prs = current.includes(pr) ? current : [...current, pr];
|
|
468
586
|
}
|
|
469
587
|
if (Object.keys(patch).length === 0)
|
|
470
588
|
fail("nothing to update — pass at least one flag");
|
|
471
|
-
const task = store.update(
|
|
589
|
+
const task = store.update(key, patch);
|
|
472
590
|
if (args.flags.json) {
|
|
473
591
|
console.log(JSON.stringify({ task }, null, 2));
|
|
474
592
|
}
|
|
@@ -480,7 +598,152 @@ function cmdMove(args) {
|
|
|
480
598
|
const [ref, status] = args.positional;
|
|
481
599
|
if (!ref || !status)
|
|
482
600
|
fail("usage: task move <id> <status>");
|
|
483
|
-
cmdUpdate({ positional: [ref], flags: { ...args.flags, status } });
|
|
601
|
+
cmdUpdate({ positional: [ref], flags: { ...args.flags, status }, multi: {} });
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* `task promote <id>` — backlog → todo, the human "build this" call; see
|
|
605
|
+
* claim.ts for why blocked / needs-human are holds (reported), not refusals.
|
|
606
|
+
* Exit codes follow claim's contract: 0 promoted, 2 refused (fix something).
|
|
607
|
+
*/
|
|
608
|
+
function cmdPromote(args) {
|
|
609
|
+
const ref = args.positional[0];
|
|
610
|
+
if (!ref)
|
|
611
|
+
fail("usage: task promote <id>");
|
|
612
|
+
const store = openStoreFor(ref);
|
|
613
|
+
try {
|
|
614
|
+
const result = promote(store, store.parseId(ref));
|
|
615
|
+
if (args.flags.json) {
|
|
616
|
+
console.log(JSON.stringify({ task: result.task, holds: result.holds }, null, 2));
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
printTask(result.task);
|
|
620
|
+
for (const hold of result.holds) {
|
|
621
|
+
console.log(`note: ${hold} — in todo, but not claimable until that clears`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
if (error instanceof ClaimError) {
|
|
627
|
+
console.error(`error: ${error.message}`);
|
|
628
|
+
process.exit(2);
|
|
629
|
+
}
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
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
|
+
}
|
|
484
747
|
}
|
|
485
748
|
function cmdComment(args) {
|
|
486
749
|
const [ref, ...rest] = args.positional;
|
|
@@ -512,8 +775,8 @@ function cmdLink(args, action) {
|
|
|
512
775
|
const store = openStoreFor(ref);
|
|
513
776
|
const relation = blocks ? "blocks" : "blocked_by";
|
|
514
777
|
const target = parseRefOn(store, (blocks ?? blockedBy));
|
|
515
|
-
const
|
|
516
|
-
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);
|
|
517
780
|
if (args.flags.json) {
|
|
518
781
|
console.log(JSON.stringify({ task }, null, 2));
|
|
519
782
|
}
|
|
@@ -525,39 +788,80 @@ function cmdLink(args, action) {
|
|
|
525
788
|
console.log(`${subject} ${verb} blocks ${object}`);
|
|
526
789
|
}
|
|
527
790
|
}
|
|
791
|
+
const CLAIM_USAGE = 'usage: task claim <id> | task claim --next | task claim --release <id> --comment "<why>"';
|
|
528
792
|
/**
|
|
529
|
-
* `task claim <id>` / `task claim --release <id>` — see
|
|
530
|
-
* mechanics. Exit codes are the contract callers script
|
|
531
|
-
* 1 already claimed
|
|
793
|
+
* `task claim <id>` / `task claim --next` / `task claim --release <id>` — see
|
|
794
|
+
* claim.ts for the mechanics. Exit codes are the contract callers script
|
|
795
|
+
* against: 0 claimed, 1 already claimed / queue empty (nothing to do here —
|
|
796
|
+
* a scheduled worker stops cleanly), 2 preconditions failed (fix something).
|
|
532
797
|
*/
|
|
533
798
|
function cmdClaim(args) {
|
|
534
799
|
const ref = args.positional[0];
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
800
|
+
const options = { force: Boolean(args.flags.force) };
|
|
801
|
+
const printClaim = (result) => {
|
|
802
|
+
if (args.flags.json) {
|
|
803
|
+
console.log(JSON.stringify({ task: result.task, branch: result.branch }, null, 2));
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
console.log(`claimed ${result.task.id} — on ${result.branch} (from ${result.base}), status in_progress`);
|
|
807
|
+
}
|
|
808
|
+
};
|
|
539
809
|
try {
|
|
810
|
+
if (args.flags.next) {
|
|
811
|
+
// The first line of every scheduled worker: claim the top claimable
|
|
812
|
+
// ticket across the selection scope (--board > root `boards` > nearest),
|
|
813
|
+
// retrying past lost races internally.
|
|
814
|
+
if (ref)
|
|
815
|
+
fail("pass an id or --next, not both");
|
|
816
|
+
if (args.flags.release)
|
|
817
|
+
fail(CLAIM_USAGE);
|
|
818
|
+
const result = claimNext(selectionBoards(process.cwd(), boardFlag(args)), options);
|
|
819
|
+
if (!result) {
|
|
820
|
+
if (args.flags.json) {
|
|
821
|
+
console.log(JSON.stringify({ task: null }, null, 2));
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
console.log("nothing claimable — no unblocked, unclaimed todo tickets in scope");
|
|
825
|
+
}
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
828
|
+
printClaim(result);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (!ref)
|
|
832
|
+
fail(CLAIM_USAGE);
|
|
833
|
+
const store = openStoreFor(ref);
|
|
834
|
+
const key = store.parseId(ref);
|
|
540
835
|
if (args.flags.release) {
|
|
541
|
-
|
|
836
|
+
// A released claim must leave its failure context behind: the branch
|
|
837
|
+
// (and whatever was tried on it) is about to evaporate, so the reason
|
|
838
|
+
// is required, and lands as an ordinary attributed comment.
|
|
839
|
+
const why = str(args.flags, "comment")?.trim();
|
|
840
|
+
if (!why) {
|
|
841
|
+
fail('task claim --release requires --comment "<why>" — the next worker inherits what was tried');
|
|
842
|
+
}
|
|
843
|
+
const result = release(store, key);
|
|
844
|
+
const commented = result.remote || result.local;
|
|
845
|
+
// After the branch deletion, so the comment survives it: release leaves
|
|
846
|
+
// the checkout on the default branch, and the comment file sits there
|
|
847
|
+
// uncommitted, to ride along with the next commit.
|
|
848
|
+
const comment = commented
|
|
849
|
+
? store.addComment(key, why, resolveAuthor(str(args.flags, "author")).name)
|
|
850
|
+
: null;
|
|
542
851
|
if (args.flags.json) {
|
|
543
|
-
console.log(JSON.stringify({ released: result }, null, 2));
|
|
852
|
+
console.log(JSON.stringify({ released: result, comment }, null, 2));
|
|
544
853
|
}
|
|
545
|
-
else if (!
|
|
546
|
-
console.log(`${store.displayId(
|
|
854
|
+
else if (!commented) {
|
|
855
|
+
console.log(`${store.displayId(key)} wasn't claimed — no ${result.branch} to delete`);
|
|
547
856
|
}
|
|
548
857
|
else {
|
|
549
858
|
const where = [result.remote && "origin", result.local && "local"].filter(Boolean);
|
|
550
|
-
console.log(`released ${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`);
|
|
551
861
|
}
|
|
552
862
|
return;
|
|
553
863
|
}
|
|
554
|
-
|
|
555
|
-
if (args.flags.json) {
|
|
556
|
-
console.log(JSON.stringify({ task: result.task, branch: result.branch }, null, 2));
|
|
557
|
-
}
|
|
558
|
-
else {
|
|
559
|
-
console.log(`claimed ${result.task.id} — on ${result.branch} (from ${result.base}), status in_progress`);
|
|
560
|
-
}
|
|
864
|
+
printClaim(claim(store, key, options));
|
|
561
865
|
}
|
|
562
866
|
catch (error) {
|
|
563
867
|
if (error instanceof ClaimError) {
|
|
@@ -567,6 +871,167 @@ function cmdClaim(args) {
|
|
|
567
871
|
throw error;
|
|
568
872
|
}
|
|
569
873
|
}
|
|
874
|
+
/** `task instructions` — the shipped agent conventions, self-served at runtime. */
|
|
875
|
+
function cmdInstructions() {
|
|
876
|
+
// skill/ ships in the npm package; `../` from both dist/cli.js and
|
|
877
|
+
// src/cli.ts is the package root, same trick VERSION uses.
|
|
878
|
+
process.stdout.write(readFileSync(new URL("../skill/SKILL.md", import.meta.url), "utf8"));
|
|
879
|
+
}
|
|
880
|
+
/** `task overview [--json]` — one screen of board health. See overview.ts. */
|
|
881
|
+
function cmdOverview(args) {
|
|
882
|
+
const overview = buildOverview(openStore());
|
|
883
|
+
if (args.flags.json) {
|
|
884
|
+
console.log(JSON.stringify({ overview }, null, 2));
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
console.log(`${overview.board} (${overview.prefix}) — ${overview.open} open`);
|
|
888
|
+
console.log(` ${STATUSES.map((s) => `${STATUS_GLYPH[s]} ${s} ${overview.statuses[s]}`).join(" ")}`);
|
|
889
|
+
const claimed = overview.claimed === null
|
|
890
|
+
? "claimed ? (origin unreachable)"
|
|
891
|
+
: `claimed ${overview.claimed.length}${overview.claimed.length ? ` (${overview.claimed.join(", ")})` : ""}`;
|
|
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}`);
|
|
896
|
+
if (overview.goals.length) {
|
|
897
|
+
console.log("goals");
|
|
898
|
+
table(overview.goals.map((g) => [
|
|
899
|
+
` ${g.slug}`,
|
|
900
|
+
g.title,
|
|
901
|
+
`${g.done}/${g.total} done${g.total ? ` (${Math.round((g.done / g.total) * 100)}%)` : ""}`,
|
|
902
|
+
]));
|
|
903
|
+
}
|
|
904
|
+
if (overview.stale.length) {
|
|
905
|
+
console.log(`stale (untouched ${STALE_DAYS}+ days)`);
|
|
906
|
+
table(overview.stale.map((t) => [` ${t.id}`, t.title, t.updatedAt.slice(0, 10)]));
|
|
907
|
+
}
|
|
908
|
+
if (overview.oldestOpen) {
|
|
909
|
+
console.log(`oldest open ${overview.oldestOpen.id} ${overview.oldestOpen.title} (created ${overview.oldestOpen.createdAt.slice(0, 10)})`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* `task goal …` — the goals a board's tasks belong to. A goal is a titled,
|
|
914
|
+
* described destination (`.task/goals/<slug>.md`); a task carries at most one.
|
|
915
|
+
* There is no goal status anywhere here on purpose: progress is derived from
|
|
916
|
+
* the tasks, and `archive` is the only lifecycle a goal has.
|
|
917
|
+
*/
|
|
918
|
+
function cmdGoal(args) {
|
|
919
|
+
const [sub, ...rest] = args.positional;
|
|
920
|
+
const usage = "usage: task goal add <title> [--slug <s>] [--description …] | list [--archived] | " +
|
|
921
|
+
"show <slug> | update <slug> [--title …] [--description …] | archive <slug> | " +
|
|
922
|
+
"unarchive <slug> | delete <slug>";
|
|
923
|
+
if (!sub)
|
|
924
|
+
fail(usage);
|
|
925
|
+
const store = openStore();
|
|
926
|
+
const slugArg = () => {
|
|
927
|
+
const slug = rest[0];
|
|
928
|
+
if (!slug)
|
|
929
|
+
fail(`usage: task goal ${sub} <slug>`);
|
|
930
|
+
return slug;
|
|
931
|
+
};
|
|
932
|
+
const printGoal = (goal, verb) => console.log(`${verb} ${goal.slug} ${goal.title}${goal.archived ? " (archived)" : ""}`);
|
|
933
|
+
switch (sub) {
|
|
934
|
+
case "add": {
|
|
935
|
+
const title = rest.join(" ").trim();
|
|
936
|
+
if (!title)
|
|
937
|
+
fail("usage: task goal add <title> [--slug <s>] [--description …]");
|
|
938
|
+
const goal = store.createGoal({
|
|
939
|
+
title,
|
|
940
|
+
slug: str(args.flags, "slug"),
|
|
941
|
+
description: str(args.flags, "description") ?? str(args.flags, "desc"),
|
|
942
|
+
});
|
|
943
|
+
if (args.flags.json)
|
|
944
|
+
console.log(JSON.stringify({ goal }, null, 2));
|
|
945
|
+
else
|
|
946
|
+
printGoal(goal, "created");
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
case "list": {
|
|
950
|
+
const archived = Boolean(args.flags.archived);
|
|
951
|
+
const goals = store.goals(archived);
|
|
952
|
+
// Progress is derived here, at read time — a goal file never stores it.
|
|
953
|
+
const tasks = store.list({ statuses: undefined });
|
|
954
|
+
const rows = goals.map((goal) => {
|
|
955
|
+
const mine = tasks.filter((t) => t.goal === goal.slug);
|
|
956
|
+
const done = mine.filter((t) => t.status === "done" || t.status === "canceled").length;
|
|
957
|
+
return { ...goal, open: mine.length - done, done, total: mine.length };
|
|
958
|
+
});
|
|
959
|
+
if (args.flags.json) {
|
|
960
|
+
console.log(JSON.stringify({ goals: rows }, null, 2));
|
|
961
|
+
}
|
|
962
|
+
else if (rows.length === 0) {
|
|
963
|
+
console.log(archived ? "no archived goals" : "no goals — create one with `task goal add`");
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
table(rows.map((g) => [g.slug, g.title, `${g.done}/${g.total} done`]));
|
|
967
|
+
}
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
case "show": {
|
|
971
|
+
const slug = slugArg();
|
|
972
|
+
const goal = store.getGoal(slug);
|
|
973
|
+
if (!goal)
|
|
974
|
+
fail(`no such goal: "${slug}"`);
|
|
975
|
+
const tasks = store.list({ statuses: undefined, goal: slug });
|
|
976
|
+
if (args.flags.json) {
|
|
977
|
+
console.log(JSON.stringify({ goal, tasks }, null, 2));
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
console.log(`${goal.slug} ${goal.title}`);
|
|
981
|
+
if (goal.archived)
|
|
982
|
+
console.log(`archived yes — \`task goal unarchive ${goal.slug}\` to edit`);
|
|
983
|
+
console.log(`created ${goal.createdAt}`);
|
|
984
|
+
console.log(`updated ${goal.updatedAt}`);
|
|
985
|
+
if (goal.description)
|
|
986
|
+
console.log(`\n${goal.description}`);
|
|
987
|
+
if (tasks.length) {
|
|
988
|
+
console.log("");
|
|
989
|
+
table(tasks.map(taskRow));
|
|
990
|
+
}
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
case "update": {
|
|
994
|
+
const patch = {
|
|
995
|
+
...(str(args.flags, "title") !== undefined && { title: str(args.flags, "title") }),
|
|
996
|
+
...((str(args.flags, "description") ?? str(args.flags, "desc")) !== undefined && {
|
|
997
|
+
description: str(args.flags, "description") ?? str(args.flags, "desc"),
|
|
998
|
+
}),
|
|
999
|
+
};
|
|
1000
|
+
if (Object.keys(patch).length === 0)
|
|
1001
|
+
fail("nothing to update — pass --title or --description");
|
|
1002
|
+
const goal = store.updateGoal(slugArg(), patch);
|
|
1003
|
+
if (args.flags.json)
|
|
1004
|
+
console.log(JSON.stringify({ goal }, null, 2));
|
|
1005
|
+
else
|
|
1006
|
+
printGoal(goal, "updated");
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
case "archive": {
|
|
1010
|
+
const goal = store.archiveGoal(slugArg());
|
|
1011
|
+
if (args.flags.json)
|
|
1012
|
+
console.log(JSON.stringify({ goal }, null, 2));
|
|
1013
|
+
else
|
|
1014
|
+
printGoal(goal, "archived");
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
case "unarchive": {
|
|
1018
|
+
const goal = store.unarchiveGoal(slugArg());
|
|
1019
|
+
if (args.flags.json)
|
|
1020
|
+
console.log(JSON.stringify({ goal }, null, 2));
|
|
1021
|
+
else
|
|
1022
|
+
printGoal(goal, "unarchived");
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
case "delete": {
|
|
1026
|
+
const slug = slugArg();
|
|
1027
|
+
store.deleteGoal(slug);
|
|
1028
|
+
console.log(`deleted goal ${slug}`);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
default:
|
|
1032
|
+
fail(usage);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
570
1035
|
const AUTHOR_SOURCE = {
|
|
571
1036
|
flag: "--author",
|
|
572
1037
|
env: "$TASK_AUTHOR",
|
|
@@ -584,35 +1049,52 @@ function cmdWhoami(args) {
|
|
|
584
1049
|
}
|
|
585
1050
|
}
|
|
586
1051
|
/**
|
|
587
|
-
* `task
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
1052
|
+
* `task check [--fix]` — lint the board files. Git merges and hand-edits are
|
|
1053
|
+
* write paths no CLI code sees, so validation is a command you can run (and
|
|
1054
|
+
* put in pre-commit or CI, where merge-introduced drift gets caught). Covers
|
|
1055
|
+
* every board at or below here, the same set `task serve` would serve. Exits
|
|
1056
|
+
* non-zero while problems remain; `--fix` first applies the mechanical
|
|
1057
|
+
* repairs — pruning dangling references, rewriting files into canonical form.
|
|
591
1058
|
*/
|
|
592
|
-
function
|
|
593
|
-
const root =
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
1059
|
+
function cmdCheck(args) {
|
|
1060
|
+
const root = checkRoot(process.cwd());
|
|
1061
|
+
const fix = Boolean(args.flags.fix);
|
|
1062
|
+
const result = checkBoards(root, fix);
|
|
1063
|
+
if (result.boards === 0) {
|
|
1064
|
+
fail("no .task directory found in this directory, any parent, or below — run `task init` first");
|
|
1065
|
+
}
|
|
1066
|
+
// In fix mode the fixable issues were just repaired; what's left needs a human.
|
|
1067
|
+
const remaining = fix ? result.issues.filter((issue) => !issue.fixable) : result.issues;
|
|
597
1068
|
if (args.flags.json) {
|
|
598
|
-
console.log(JSON.stringify({
|
|
599
|
-
|
|
1069
|
+
console.log(JSON.stringify({ ...result, remaining }, null, 2));
|
|
1070
|
+
}
|
|
1071
|
+
else {
|
|
1072
|
+
for (const file of result.fixed)
|
|
1073
|
+
console.log(`fixed ${file}`);
|
|
1074
|
+
for (const issue of remaining) {
|
|
1075
|
+
console.log(`${issue.fixable ? "fixable" : "problem"} ${issue.file}`);
|
|
1076
|
+
console.log(` ${issue.message}`);
|
|
1077
|
+
}
|
|
1078
|
+
const scope = `${result.files} file${result.files === 1 ? "" : "s"} across ${result.boards} board${result.boards === 1 ? "" : "s"}`;
|
|
1079
|
+
if (remaining.length === 0) {
|
|
1080
|
+
console.log(result.fixed.length ? `fixed ${result.fixed.length}, ${scope} clean` : `ok — ${scope}`);
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
const fixable = remaining.filter((issue) => issue.fixable).length;
|
|
1084
|
+
console.log(`${remaining.length} problem${remaining.length === 1 ? "" : "s"} in ${scope}${fixable ? ` (${fixable} fixable — run \`task check --fix\`)` : ""}`);
|
|
1085
|
+
}
|
|
600
1086
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
console.log(``);
|
|
604
|
-
console.log(`Next:`);
|
|
605
|
-
console.log(` git rm --cached ${join(TASK_DIR, "tasks.db")} stop tracking the database`);
|
|
606
|
-
console.log(` git add ${TASK_DIR} commit the tickets`);
|
|
1087
|
+
if (remaining.length > 0)
|
|
1088
|
+
process.exitCode = 1;
|
|
607
1089
|
}
|
|
608
1090
|
function cmdDelete(args) {
|
|
609
1091
|
const ref = args.positional[0];
|
|
610
1092
|
if (!ref)
|
|
611
1093
|
fail("usage: task delete <id>");
|
|
612
1094
|
const store = openStoreFor(ref);
|
|
613
|
-
const
|
|
614
|
-
store.delete(
|
|
615
|
-
console.log(`deleted ${store.displayId(
|
|
1095
|
+
const key = store.parseId(ref);
|
|
1096
|
+
store.delete(key);
|
|
1097
|
+
console.log(`deleted ${store.displayId(key)}`);
|
|
616
1098
|
}
|
|
617
1099
|
/**
|
|
618
1100
|
* Bind the first free port at or after `first`, the way dev servers do: a board
|
|
@@ -790,51 +1272,142 @@ subdirectory (it walks up to find .task/, like git).
|
|
|
790
1272
|
|
|
791
1273
|
Usage
|
|
792
1274
|
task init [--name <name>] [--prefix <PREFIX>]
|
|
793
|
-
task add <title> [--description <text>
|
|
794
|
-
[--
|
|
795
|
-
|
|
1275
|
+
task add <title> [--description <text> | --description-file <path|->]
|
|
1276
|
+
[--status <s>] [--tags <a,b>] [--goal <slug>]
|
|
1277
|
+
[--ask "<text>"]…
|
|
1278
|
+
new tickets land in backlog (the dump list)
|
|
1279
|
+
unless --status says otherwise;
|
|
1280
|
+
--description-file reads markdown from a file
|
|
1281
|
+
(or stdin with -) — no shell-quoting fights;
|
|
1282
|
+
--ask (repeatable) files what a person owes
|
|
1283
|
+
the ticket, right at creation
|
|
1284
|
+
task list [--status <s1,s2>] [--tag <a,b>] [--goal <slug>]
|
|
796
1285
|
[--needs-human] [--all] [--archived] [--claimable]
|
|
797
|
-
task
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1286
|
+
task search <query> [--status <s1,s2>] [--tag <a,b>] [--goal <slug>]
|
|
1287
|
+
[--needs-human] [--archived]
|
|
1288
|
+
find tickets by what they say — case-insensitive
|
|
1289
|
+
match over titles, descriptions and comments,
|
|
1290
|
+
with a snippet of the matching line. --archived
|
|
1291
|
+
searches the archive too, on top of the board
|
|
1292
|
+
task show <id> the ticket, its comments — and its goal's title
|
|
1293
|
+
and description inline, so a cold pickup gets
|
|
1294
|
+
the shared context without chasing it
|
|
1295
|
+
task update <id> [--title <t>] [--description <text> | --description-file <path|->]
|
|
1296
|
+
[--status <s>] [--tags <a,b>] [--goal <slug>]
|
|
801
1297
|
[--pr <url>] [--prs <url1,url2>]
|
|
802
|
-
task move <id> <status>
|
|
803
|
-
|
|
804
|
-
|
|
1298
|
+
task move <id> <status> change a ticket's status — the one general
|
|
1299
|
+
status API (backlog todo in_progress done
|
|
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
|
|
1324
|
+
task promote <id> backlog → todo, the human "build this" call.
|
|
1325
|
+
No quality gate — planning happens at claim
|
|
1326
|
+
time. Blocked tickets still promote —
|
|
1327
|
+
blockers are claim-time gates, reported as a
|
|
1328
|
+
note — so promoting can queue work behind
|
|
1329
|
+
its blockers. Exit codes: 0 promoted, 2
|
|
1330
|
+
refused (not a backlog ticket)
|
|
805
1331
|
task link <id> --blocked-by <id>
|
|
806
1332
|
task link <id> --blocks <id> mark a dependency — one relation, visible from
|
|
807
1333
|
both tasks (A blocked by B ⇔ B blocks A)
|
|
808
1334
|
task unlink <id> (--blocks <id> | --blocked-by <id>)
|
|
809
|
-
task claim <id>
|
|
810
|
-
<
|
|
811
|
-
branch, ticket → in_progress as its
|
|
812
|
-
commit, pushed. The namespace defaults
|
|
813
|
-
task/claim/ — set
|
|
1335
|
+
task claim <id> [--force] claim a ticket before working it: branch
|
|
1336
|
+
<branchPrefix><prefix>-<key> off origin's
|
|
1337
|
+
default branch, ticket → in_progress as its
|
|
1338
|
+
first commit, pushed. The namespace defaults
|
|
1339
|
+
to task/claim/ — set claims.branchPrefix in
|
|
814
1340
|
.task/config.json to change it (e.g.
|
|
815
1341
|
"claude/task/", which Claude cloud sessions
|
|
816
1342
|
can push). Branch on origin = claimed —
|
|
817
1343
|
git's atomic ref creation is the lock, so two
|
|
818
|
-
concurrent claimers can't both win.
|
|
1344
|
+
concurrent claimers can't both win. While the
|
|
1345
|
+
root board sets claims.maxOpenCount and that
|
|
1346
|
+
many claim branches exist on origin, claiming
|
|
1347
|
+
is refused (--force overrides). Exit codes:
|
|
819
1348
|
0 claimed, 1 already claimed, 2 not claimable
|
|
820
|
-
(not todo, blocked,
|
|
821
|
-
task claim --
|
|
1349
|
+
(not todo, blocked, dirty tree, at the cap)
|
|
1350
|
+
task claim --next [--board <P>] [--force]
|
|
1351
|
+
claim the top claimable ticket in one call,
|
|
1352
|
+
retrying past lost races internally — the
|
|
1353
|
+
first line of every scheduled worker. Scope:
|
|
1354
|
+
--board prefixes (repeatable, given order =
|
|
1355
|
+
priority) > the root board's "boards" list >
|
|
1356
|
+
the nearest board. Exit codes: 0 claimed
|
|
1357
|
+
(prints the ticket), 1 queue empty, 2
|
|
1358
|
+
preconditions failed
|
|
1359
|
+
task claim --release <id> --comment "<why>"
|
|
1360
|
+
abandon a claim: delete the branch on origin
|
|
822
1361
|
and locally — the status flip only lived on
|
|
823
|
-
the branch, so deleting it is the revert
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1362
|
+
the branch, so deleting it is the revert. The
|
|
1363
|
+
comment is required and lands on the ticket
|
|
1364
|
+
(attributed like task comment, uncommitted),
|
|
1365
|
+
so the next worker inherits what was tried
|
|
1366
|
+
task list --claimable [--board <P>]
|
|
1367
|
+
the claim queue: todo tickets in position
|
|
1368
|
+
order, minus blocked / already claimed on
|
|
1369
|
+
origin. Top entry is next up. Same board
|
|
1370
|
+
scope as claim --next
|
|
1371
|
+
task instructions print the shipped agent conventions
|
|
1372
|
+
(skill/SKILL.md) — agents without the skill
|
|
1373
|
+
installed self-serve the house rules
|
|
1374
|
+
task overview [--json] one screen of board health: counts per
|
|
1375
|
+
status, per-goal progress, claimed and
|
|
1376
|
+
open-ask counts, stale tickets (untouched
|
|
1377
|
+
${STALE_DAYS}+ days), oldest open ticket
|
|
827
1378
|
task comment <id> <text> [--author <who>]
|
|
828
1379
|
task delete <id>
|
|
1380
|
+
task goal add <title> [--slug <s>] [--description <text>]
|
|
1381
|
+
create a goal: a titled, described destination
|
|
1382
|
+
tasks belong to (.task/goals/<slug>.md). The
|
|
1383
|
+
description is the strategic why — the context
|
|
1384
|
+
every ticket under it shares
|
|
1385
|
+
task goal list [--archived] every goal with derived progress (done/total —
|
|
1386
|
+
a goal stores no status of its own, ever)
|
|
1387
|
+
task goal show <slug> the goal, its description, and its tasks
|
|
1388
|
+
task goal update <slug> [--title <t>] [--description <text>]
|
|
1389
|
+
task goal archive <slug> shelve a finished/abandoned goal — refused
|
|
1390
|
+
while it still has open tasks
|
|
1391
|
+
task goal unarchive <slug>
|
|
1392
|
+
task goal delete <slug> only when no live task references it
|
|
829
1393
|
task boards every board in this repo — prefix, name, path,
|
|
830
1394
|
open count; * marks the one commands target here
|
|
831
1395
|
task archive <id> move a done/canceled ticket to .task/archive/,
|
|
832
1396
|
out of the board and off the hot path — still
|
|
833
1397
|
readable via show and list --archived, and its
|
|
834
|
-
|
|
1398
|
+
key stays reserved
|
|
835
1399
|
task archive --all archive everything done or canceled
|
|
836
1400
|
task unarchive <id> put an archived ticket back on the board
|
|
837
1401
|
task whoami who your comments are attributed to
|
|
1402
|
+
task check [--fix] lint the board files — merges and hand-edits
|
|
1403
|
+
are write paths no CLI code sees. Reports
|
|
1404
|
+
unparseable files, dangling blocked_by refs,
|
|
1405
|
+
bad timestamps, unknown frontmatter keys and
|
|
1406
|
+
formatting drift, for every board at or below
|
|
1407
|
+
here; exits non-zero while problems remain.
|
|
1408
|
+
--fix applies the mechanical repairs first
|
|
1409
|
+
(prune dangling refs, rewrite to canonical
|
|
1410
|
+
form). Cheap enough for pre-commit and CI
|
|
838
1411
|
task serve [--port <n>] [--no-open] [--strict-port]
|
|
839
1412
|
board + table UI with live updates, opened in
|
|
840
1413
|
your browser (default port ${DEFAULT_PORT}; taken ports
|
|
@@ -852,19 +1425,22 @@ Usage
|
|
|
852
1425
|
task unpublish [--repo <owner/name>]
|
|
853
1426
|
take that URL down. Removes the board, not the
|
|
854
1427
|
tasks — those are in .task/ either way
|
|
855
|
-
task migrate move a pre-0.6 board off its committed SQLite
|
|
856
|
-
database and onto text files in .task/tickets/.
|
|
857
|
-
The database stays on disk as an ignored backup
|
|
858
1428
|
|
|
859
1429
|
Values
|
|
860
|
-
<id> TAS-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
|
863
1436
|
status ${STATUSES.join(" ")}
|
|
864
1437
|
--tag a,b matches a task carrying *either* tag
|
|
865
|
-
--
|
|
1438
|
+
--goal <slug> a task belongs to at most one goal. Goals answer "where is
|
|
1439
|
+
this going" (one); tags answer "what kind" (many) — something
|
|
1440
|
+
that wants two goals is a tag
|
|
1441
|
+
--needs-human filter: tickets with open asks — something a person owes
|
|
866
1442
|
--pr <url> attach a pull request (appends); --prs replaces the whole list
|
|
867
|
-
clearing --tags "" drops all tags, --
|
|
1443
|
+
clearing --tags "" drops all tags, --goal "" clears it, --prs ""
|
|
868
1444
|
detaches all PRs
|
|
869
1445
|
|
|
870
1446
|
Comment authors resolve --author → $TASK_AUTHOR → git config user.name →
|
|
@@ -892,22 +1468,34 @@ function main() {
|
|
|
892
1468
|
return cmdAdd(args);
|
|
893
1469
|
case "list":
|
|
894
1470
|
return cmdList(args);
|
|
1471
|
+
case "search":
|
|
1472
|
+
return cmdSearch(args);
|
|
895
1473
|
case "show":
|
|
896
1474
|
return cmdShow(args);
|
|
897
1475
|
case "update":
|
|
898
1476
|
return cmdUpdate(args);
|
|
899
1477
|
case "move":
|
|
900
1478
|
return cmdMove(args);
|
|
901
|
-
case "
|
|
902
|
-
return
|
|
903
|
-
case "done":
|
|
904
|
-
return cmdUpdate(args, "done");
|
|
1479
|
+
case "promote":
|
|
1480
|
+
return cmdPromote(args);
|
|
905
1481
|
case "link":
|
|
906
1482
|
return cmdLink(args, "link");
|
|
907
1483
|
case "unlink":
|
|
908
1484
|
return cmdLink(args, "unlink");
|
|
909
1485
|
case "claim":
|
|
910
1486
|
return cmdClaim(args);
|
|
1487
|
+
case "instructions":
|
|
1488
|
+
return cmdInstructions();
|
|
1489
|
+
case "ask":
|
|
1490
|
+
return cmdAsk(args);
|
|
1491
|
+
case "resolve":
|
|
1492
|
+
return cmdResolve(args);
|
|
1493
|
+
case "inbox":
|
|
1494
|
+
return cmdInbox(args);
|
|
1495
|
+
case "overview":
|
|
1496
|
+
return cmdOverview(args);
|
|
1497
|
+
case "goal":
|
|
1498
|
+
return cmdGoal(args);
|
|
911
1499
|
case "comment":
|
|
912
1500
|
return cmdComment(args);
|
|
913
1501
|
case "whoami":
|
|
@@ -920,8 +1508,8 @@ function main() {
|
|
|
920
1508
|
return cmdArchive(args);
|
|
921
1509
|
case "unarchive":
|
|
922
1510
|
return cmdUnarchive(args);
|
|
923
|
-
case "
|
|
924
|
-
return
|
|
1511
|
+
case "check":
|
|
1512
|
+
return cmdCheck(args);
|
|
925
1513
|
case "serve":
|
|
926
1514
|
return cmdServe(args);
|
|
927
1515
|
case "publish":
|