@nickmeriano/task 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -22
- package/dist/asks.test.d.ts +17 -0
- package/dist/asks.test.d.ts.map +1 -0
- package/dist/asks.test.js +278 -0
- package/dist/asks.test.js.map +1 -0
- package/dist/check.d.ts +3 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +99 -60
- package/dist/check.js.map +1 -1
- package/dist/check.test.js +99 -60
- package/dist/check.test.js.map +1 -1
- package/dist/claim-io.d.ts +73 -0
- package/dist/claim-io.d.ts.map +1 -0
- package/dist/claim-io.js +344 -0
- package/dist/claim-io.js.map +1 -0
- package/dist/claim.d.ts +21 -22
- package/dist/claim.d.ts.map +1 -1
- package/dist/claim.js +58 -83
- package/dist/claim.js.map +1 -1
- package/dist/claim.test.d.ts +2 -2
- package/dist/claim.test.js +111 -94
- package/dist/claim.test.js.map +1 -1
- package/dist/cli.js +241 -64
- package/dist/cli.js.map +1 -1
- package/dist/file-store.d.ts +70 -24
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +319 -141
- package/dist/file-store.js.map +1 -1
- package/dist/git-serve.d.ts +183 -0
- package/dist/git-serve.d.ts.map +1 -0
- package/dist/git-serve.js +503 -0
- package/dist/git-serve.js.map +1 -0
- package/dist/git-serve.test.d.ts +16 -0
- package/dist/git-serve.test.d.ts.map +1 -0
- package/dist/git-serve.test.js +183 -0
- package/dist/git-serve.test.js.map +1 -0
- package/dist/git.d.ts +65 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +114 -0
- package/dist/git.js.map +1 -0
- package/dist/id.d.ts +39 -0
- package/dist/id.d.ts.map +1 -0
- package/dist/id.js +67 -0
- package/dist/id.js.map +1 -0
- package/dist/inbox.d.ts +41 -0
- package/dist/inbox.d.ts.map +1 -0
- package/dist/inbox.js +56 -0
- package/dist/inbox.js.map +1 -0
- package/dist/overview.d.ts +9 -0
- package/dist/overview.d.ts.map +1 -1
- package/dist/overview.js +19 -11
- package/dist/overview.js.map +1 -1
- package/dist/overview.test.js +6 -5
- package/dist/overview.test.js.map +1 -1
- package/dist/promote.test.d.ts +3 -2
- package/dist/promote.test.d.ts.map +1 -1
- package/dist/promote.test.js +29 -31
- package/dist/promote.test.js.map +1 -1
- package/dist/publish.d.ts +2 -17
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +4 -49
- package/dist/publish.js.map +1 -1
- package/dist/search.d.ts +3 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +15 -2
- package/dist/search.js.map +1 -1
- package/dist/search.test.js +9 -9
- package/dist/search.test.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +218 -21
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +28 -14
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js.map +1 -1
- package/dist/store.test.js +88 -75
- package/dist/store.test.js.map +1 -1
- package/dist/ticket-doc.d.ts +26 -10
- package/dist/ticket-doc.d.ts.map +1 -1
- package/dist/ticket-doc.js +71 -27
- package/dist/ticket-doc.js.map +1 -1
- package/dist/types.d.ts +51 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +70 -23
- package/src/asks.test.ts +355 -0
- package/src/check.test.ts +129 -72
- package/src/check.ts +101 -58
- package/src/claim-io.ts +401 -0
- package/src/claim.test.ts +141 -99
- package/src/claim.ts +69 -96
- package/src/cli.ts +251 -63
- package/src/file-store.ts +357 -145
- package/src/git-serve.test.ts +240 -0
- package/src/git-serve.ts +595 -0
- package/src/git.ts +141 -0
- package/src/id.ts +68 -0
- package/src/inbox.ts +77 -0
- package/src/overview.test.ts +6 -5
- package/src/overview.ts +25 -10
- package/src/promote.test.ts +40 -28
- package/src/publish.ts +6 -53
- package/src/search.test.ts +11 -11
- package/src/search.ts +17 -3
- package/src/server.ts +221 -19
- package/src/store.test.ts +95 -77
- package/src/store.ts +28 -13
- package/src/ticket-doc.ts +93 -30
- package/src/types.ts +52 -17
- package/ui/dist/assets/index-BjsorZOU.js +229 -0
- package/ui/dist/assets/index-CoKCUYic.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-BJmOsOdR.js +0 -229
- package/ui/dist/assets/index-BoqQlqSU.css +0 -1
package/src/store.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
2
2
|
import { dirname, join, relative, sep } from "node:path"
|
|
3
3
|
import type {
|
|
4
|
+
Ask,
|
|
4
5
|
Comment,
|
|
5
6
|
Goal,
|
|
6
7
|
GoalInput,
|
|
@@ -29,18 +30,24 @@ export interface Store {
|
|
|
29
30
|
readonly taskDir: string
|
|
30
31
|
readonly config: ProjectConfig
|
|
31
32
|
close(): void
|
|
32
|
-
displayId(
|
|
33
|
-
|
|
33
|
+
displayId(key: string): string
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a ref — "TAS-x7k4m", "x7k4m", or a unique key prefix like "x7" —
|
|
36
|
+
* to a ticket key, git-style: any unambiguous prefix of an existing key
|
|
37
|
+
* works, an ambiguous one throws naming the candidates, and an unknown one
|
|
38
|
+
* comes back verbatim so the caller's lookup produces the "no such task".
|
|
39
|
+
*/
|
|
40
|
+
parseId(ref: string): string
|
|
34
41
|
list(filter?: TaskFilter): Task[]
|
|
35
|
-
get(
|
|
42
|
+
get(key: string): Task | null
|
|
36
43
|
create(input: TaskInput): Task
|
|
37
|
-
update(
|
|
38
|
-
link(
|
|
39
|
-
unlink(
|
|
40
|
-
delete(
|
|
44
|
+
update(key: string, patch: TaskPatch): Task
|
|
45
|
+
link(key: string, relation: "blocks" | "blocked_by", target: string): Task
|
|
46
|
+
unlink(key: string, relation: "blocks" | "blocked_by", target: string): Task
|
|
47
|
+
delete(key: string): void
|
|
41
48
|
/** Move a done/canceled ticket to `.task/archive/`, out of the board's hot path. */
|
|
42
|
-
archive(
|
|
43
|
-
unarchive(
|
|
49
|
+
archive(key: string): Task
|
|
50
|
+
unarchive(key: string): Task
|
|
44
51
|
/** Live goals, in slug order — pass `true` to list `.task/goals/archive/` instead. */
|
|
45
52
|
goals(archived?: boolean): Goal[]
|
|
46
53
|
/** Live or archived — a ticket's `goal:` ref should resolve either way. */
|
|
@@ -52,10 +59,18 @@ export interface Store {
|
|
|
52
59
|
unarchiveGoal(slug: string): Goal
|
|
53
60
|
/** Refused while any live task references the goal, whatever its status. */
|
|
54
61
|
deleteGoal(slug: string): void
|
|
55
|
-
comments(
|
|
56
|
-
addComment(
|
|
57
|
-
deleteComment(
|
|
58
|
-
commentCounts(): Map<
|
|
62
|
+
comments(key: string): Comment[]
|
|
63
|
+
addComment(key: string, body: string, author?: string): Comment
|
|
64
|
+
deleteComment(key: string, commentId: string): Comment
|
|
65
|
+
commentCounts(): Map<string, number>
|
|
66
|
+
/** Every ask on a ticket, open and resolved, in creation (= ordinal) order. */
|
|
67
|
+
asks(key: string): Ask[]
|
|
68
|
+
/** Refused on done/canceled tickets — a finished ticket can't start owing again. */
|
|
69
|
+
addAsk(key: string, text: string, author?: string): Ask
|
|
70
|
+
/** `ref` is an ordinal ("2") or the ask's id. */
|
|
71
|
+
resolveAsk(key: string, ref: string, resolvedBy?: string): Ask
|
|
72
|
+
reopenAsk(key: string, ref: string): Ask
|
|
73
|
+
deleteAsk(key: string, ref: string): Ask
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
/**
|
package/src/ticket-doc.ts
CHANGED
|
@@ -25,9 +25,8 @@ export interface TicketDoc {
|
|
|
25
25
|
tags: string[]
|
|
26
26
|
/** Slug of the goal this ticket belongs to — at most one, by design. */
|
|
27
27
|
goal: string | null
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
blockedBy: number[]
|
|
28
|
+
/** Keys of tasks blocking this one — the single stored side of the relation. */
|
|
29
|
+
blockedBy: string[]
|
|
31
30
|
prs: string[]
|
|
32
31
|
position: number
|
|
33
32
|
createdAt: string
|
|
@@ -41,18 +40,11 @@ export interface TicketDoc {
|
|
|
41
40
|
extras?: [string, string][]
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
/**
|
|
45
|
-
* The frontmatter keys a ticket file may carry; anything else is an extra.
|
|
46
|
-
* `milestone` is the pre-0.8 spelling of `goal`: still *read* (as the goal, so
|
|
47
|
-
* old boards keep rendering) but never written — `task check` flags it and
|
|
48
|
-
* `--fix` rewrites the file, which is the whole migration.
|
|
49
|
-
*/
|
|
43
|
+
/** The frontmatter keys a ticket file may carry; anything else is an extra. */
|
|
50
44
|
export const TICKET_FIELDS = [
|
|
51
45
|
"status",
|
|
52
46
|
"tags",
|
|
53
47
|
"goal",
|
|
54
|
-
"milestone",
|
|
55
|
-
"needs_human",
|
|
56
48
|
"blocked_by",
|
|
57
49
|
"prs",
|
|
58
50
|
"position",
|
|
@@ -69,6 +61,22 @@ export interface CommentDoc {
|
|
|
69
61
|
body: string
|
|
70
62
|
}
|
|
71
63
|
|
|
64
|
+
/** The frontmatter keys an ask file may carry. */
|
|
65
|
+
export const ASK_FIELDS = ["author", "created", "resolved", "resolved_by"] as const
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* One ask on disk — `tickets/<n>/asks/<stem>.md`, the comment layout plus a
|
|
69
|
+
* resolution: `resolved`/`resolved_by` appear when the ask is closed and are
|
|
70
|
+
* simply absent while it's open, so an open ask's bytes never churn.
|
|
71
|
+
*/
|
|
72
|
+
export interface AskDoc {
|
|
73
|
+
author: string
|
|
74
|
+
createdAt: string
|
|
75
|
+
resolvedAt: string | null
|
|
76
|
+
resolvedBy: string | null
|
|
77
|
+
body: string
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
/** A parse that names the file it failed in — these files invite hand-editing. */
|
|
73
81
|
export class TicketParseError extends Error {}
|
|
74
82
|
|
|
@@ -144,13 +152,17 @@ function stringList(raw: RawDoc, key: string, where: string): string[] {
|
|
|
144
152
|
throw new TicketParseError(`${where}: ${key} should be a list of strings, e.g. ["a", "b"]`)
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
function
|
|
155
|
+
function keyList(raw: RawDoc, key: string, where: string): string[] {
|
|
148
156
|
const value = field(raw, key)
|
|
149
157
|
if (value === undefined) return []
|
|
150
|
-
if (Array.isArray(value) && value.every((v) => typeof v === "
|
|
151
|
-
return value as
|
|
158
|
+
if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
|
|
159
|
+
return value as string[]
|
|
152
160
|
}
|
|
153
|
-
|
|
161
|
+
// A list of bare numbers is a pre-migration file (ids were sequential
|
|
162
|
+
// integers before TAS-42) — name the one edit that fixes it.
|
|
163
|
+
throw new TicketParseError(
|
|
164
|
+
`${where}: ${key} should be a list of ticket keys, e.g. ["x7k4m", "q3v8d"] — numeric entries are the pre-TAS-42 id format, which is no longer read`,
|
|
165
|
+
)
|
|
154
166
|
}
|
|
155
167
|
|
|
156
168
|
// ── Tickets ──────────────────────────────────────────────────────────────────
|
|
@@ -177,11 +189,18 @@ export function parseTicket(text: string, where: string): TicketDoc {
|
|
|
177
189
|
`${where}: invalid status "${status}" — one of: ${STATUSES.join(", ")}`,
|
|
178
190
|
)
|
|
179
191
|
}
|
|
180
|
-
|
|
192
|
+
// The pre-0.8 `milestone:` spelling is not honored (TAS-41: no compat), and
|
|
193
|
+
// reading past it would silently drop the ticket's goal — refuse by name
|
|
194
|
+
// instead, so the one edit that fixes the file is in the error.
|
|
195
|
+
if (raw.fields.has("milestone")) {
|
|
196
|
+
throw new TicketParseError(
|
|
197
|
+
`${where}: \`milestone:\` is the pre-0.8 spelling and is no longer read — rename the key to \`goal:\``,
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
const goal = field(raw, "goal")
|
|
181
201
|
if (goal !== undefined && goal !== null && typeof goal !== "string") {
|
|
182
202
|
throw new TicketParseError(`${where}: goal should be a string`)
|
|
183
203
|
}
|
|
184
|
-
const needsHuman = field(raw, "needs_human")
|
|
185
204
|
const position = field(raw, "position")
|
|
186
205
|
if (position !== undefined && typeof position !== "number") {
|
|
187
206
|
throw new TicketParseError(`${where}: position should be a number`)
|
|
@@ -201,8 +220,7 @@ export function parseTicket(text: string, where: string): TicketDoc {
|
|
|
201
220
|
status,
|
|
202
221
|
tags: stringList(raw, "tags", where),
|
|
203
222
|
goal: typeof goal === "string" && goal !== "" ? goal : null,
|
|
204
|
-
|
|
205
|
-
blockedBy: numberList(raw, "blocked_by", where),
|
|
223
|
+
blockedBy: keyList(raw, "blocked_by", where),
|
|
206
224
|
prs: stringList(raw, "prs", where),
|
|
207
225
|
position: typeof position === "number" ? position : 0,
|
|
208
226
|
createdAt: stringField(raw, "created", where, ""),
|
|
@@ -226,7 +244,6 @@ export function serializeTicket(doc: TicketDoc): string {
|
|
|
226
244
|
const lines = ["---", `status: ${doc.status}`]
|
|
227
245
|
if (doc.tags.length) lines.push(`tags: ${list(doc.tags)}`)
|
|
228
246
|
if (doc.goal) lines.push(`goal: ${JSON.stringify(doc.goal)}`)
|
|
229
|
-
if (doc.needsHuman) lines.push(`needs_human: true`)
|
|
230
247
|
if (doc.blockedBy.length) lines.push(`blocked_by: ${list(doc.blockedBy)}`)
|
|
231
248
|
if (doc.prs.length) lines.push(`prs: ${list(doc.prs)}`)
|
|
232
249
|
lines.push(`position: ${doc.position}`)
|
|
@@ -339,6 +356,53 @@ export function serializeComment(doc: CommentDoc): string {
|
|
|
339
356
|
return `${lines.join("\n")}\n`
|
|
340
357
|
}
|
|
341
358
|
|
|
359
|
+
// ── Asks ─────────────────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
export function parseAsk(text: string, where: string): AskDoc {
|
|
362
|
+
const raw = splitRaw(text, where)!
|
|
363
|
+
const resolvedAt = field(raw, "resolved")
|
|
364
|
+
if (resolvedAt !== undefined && typeof resolvedAt !== "string") {
|
|
365
|
+
throw new TicketParseError(`${where}: resolved should be a timestamp string`)
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
author: stringField(raw, "author", where, ""),
|
|
369
|
+
createdAt: stringField(raw, "created", where, ""),
|
|
370
|
+
resolvedAt: resolvedAt ?? null,
|
|
371
|
+
resolvedBy: resolvedAt === undefined ? null : stringField(raw, "resolved_by", where, ""),
|
|
372
|
+
body: raw.body,
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function serializeAsk(doc: AskDoc): string {
|
|
377
|
+
const lines = ["---"]
|
|
378
|
+
if (doc.author) lines.push(`author: ${JSON.stringify(doc.author)}`)
|
|
379
|
+
lines.push(`created: ${doc.createdAt}`)
|
|
380
|
+
if (doc.resolvedAt) {
|
|
381
|
+
lines.push(`resolved: ${doc.resolvedAt}`)
|
|
382
|
+
if (doc.resolvedBy) lines.push(`resolved_by: ${JSON.stringify(doc.resolvedBy)}`)
|
|
383
|
+
}
|
|
384
|
+
lines.push("---", "", doc.body)
|
|
385
|
+
return `${lines.join("\n")}\n`
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* An ask, forgivingly — null when there's no frontmatter at all. The lenient
|
|
390
|
+
* read matters more here than for comments: the inbox reads ask files straight
|
|
391
|
+
* off claim branches, which are exactly the bytes a strict parser can't refuse.
|
|
392
|
+
*/
|
|
393
|
+
export function parseAskLenient(text: string): AskDoc | null {
|
|
394
|
+
const raw = splitRaw(text, null)
|
|
395
|
+
if (!raw) return null
|
|
396
|
+
const resolvedAt = field(raw, "resolved")
|
|
397
|
+
return {
|
|
398
|
+
author: lenientString(field(raw, "author")),
|
|
399
|
+
createdAt: lenientString(field(raw, "created")),
|
|
400
|
+
resolvedAt: typeof resolvedAt === "string" && resolvedAt !== "" ? resolvedAt : null,
|
|
401
|
+
resolvedBy: lenientString(field(raw, "resolved_by")) || null,
|
|
402
|
+
body: raw.body,
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
342
406
|
// ── Lenient parsing ──────────────────────────────────────────────────────────
|
|
343
407
|
//
|
|
344
408
|
// The hosted board's read of the same format. The CLI is strict — it wrote the
|
|
@@ -352,10 +416,10 @@ function lenientStrings(value: unknown): string[] {
|
|
|
352
416
|
return Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : []
|
|
353
417
|
}
|
|
354
418
|
|
|
355
|
-
function
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
419
|
+
function lenientKeys(value: unknown): string[] {
|
|
420
|
+
// Numeric entries (the pre-TAS-42 id format) are dropped, not coerced: a
|
|
421
|
+
// rendered old branch degrades to unlinked tickets, never to wrong links.
|
|
422
|
+
return Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : []
|
|
359
423
|
}
|
|
360
424
|
|
|
361
425
|
function lenientString(value: unknown): string {
|
|
@@ -367,10 +431,10 @@ export function parseTicketLenient(text: string): TicketDoc | null {
|
|
|
367
431
|
const raw = splitRaw(text, null)
|
|
368
432
|
if (!raw) return null
|
|
369
433
|
const status = field(raw, "status")
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
const goal = field(raw, "goal")
|
|
434
|
+
// No `milestone:` read here either — on the old branches the hosted board
|
|
435
|
+
// previews, a pre-0.8 ticket degrades to goalless, the same as any other
|
|
436
|
+
// retired field. History renders; it doesn't get reinterpreted.
|
|
437
|
+
const goal = field(raw, "goal")
|
|
374
438
|
const position = field(raw, "position")
|
|
375
439
|
const { title, description } = splitTitle(raw.body)
|
|
376
440
|
return {
|
|
@@ -379,8 +443,7 @@ export function parseTicketLenient(text: string): TicketDoc | null {
|
|
|
379
443
|
status: typeof status === "string" && isStatus(status) ? status : "backlog",
|
|
380
444
|
tags: lenientStrings(field(raw, "tags")),
|
|
381
445
|
goal: typeof goal === "string" && goal !== "" ? goal : null,
|
|
382
|
-
|
|
383
|
-
blockedBy: [...new Set(lenientNumbers(field(raw, "blocked_by")))].sort((a, b) => a - b),
|
|
446
|
+
blockedBy: [...new Set(lenientKeys(field(raw, "blocked_by")))].sort(),
|
|
384
447
|
prs: lenientStrings(field(raw, "prs")),
|
|
385
448
|
position: typeof position === "number" && Number.isFinite(position) ? position : 0,
|
|
386
449
|
createdAt: lenientString(field(raw, "created")),
|
package/src/types.ts
CHANGED
|
@@ -9,10 +9,42 @@ export const STATUSES = [
|
|
|
9
9
|
] as const
|
|
10
10
|
export type Status = (typeof STATUSES)[number]
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* An ask: one thing a person owes this ticket before it can be done. Stored
|
|
14
|
+
* like a comment — one file per ask under `tickets/<n>/asks/` — so a worker on
|
|
15
|
+
* a claim branch can add one without ever conflicting with main. The ask is
|
|
16
|
+
* the primitive everything human-owed funnels through: `needsHuman` is derived
|
|
17
|
+
* from open asks, `task inbox` is the list of them, and `done` is gated on
|
|
18
|
+
* zero remaining.
|
|
19
|
+
*/
|
|
20
|
+
export interface Ask {
|
|
21
|
+
/** Stable opaque id — the ask's filename stem (e.g. "2026-08-27T104501-claude"). */
|
|
22
|
+
id: string
|
|
23
|
+
taskId: string
|
|
24
|
+
/**
|
|
25
|
+
* 1-based position in creation order across the ticket's asks, resolved and
|
|
26
|
+
* open alike — the short handle `task resolve <id> <ordinal>` takes.
|
|
27
|
+
*/
|
|
28
|
+
ordinal: number
|
|
29
|
+
text: string
|
|
30
|
+
author: string
|
|
31
|
+
createdAt: string
|
|
32
|
+
/** Both null while the ask is open. */
|
|
33
|
+
resolvedAt: string | null
|
|
34
|
+
resolvedBy: string | null
|
|
35
|
+
}
|
|
36
|
+
|
|
12
37
|
export interface Task {
|
|
13
|
-
/** Display id — `<prefix>-<
|
|
38
|
+
/** Display id — `<prefix>-<key>`, e.g. "PHO-x7k4m". */
|
|
14
39
|
id: string
|
|
15
|
-
|
|
40
|
+
/**
|
|
41
|
+
* The ticket's key: 5 random lowercase characters minted at creation (see
|
|
42
|
+
* id.ts), doubling as its directory name under `.task/tickets/`. Random on
|
|
43
|
+
* purpose — allocation must be safe across branches that have never seen
|
|
44
|
+
* each other — and opaque on purpose: creation order lives in `createdAt`,
|
|
45
|
+
* board order in `position`.
|
|
46
|
+
*/
|
|
47
|
+
key: string
|
|
16
48
|
title: string
|
|
17
49
|
description: string
|
|
18
50
|
status: Status
|
|
@@ -23,15 +55,20 @@ export interface Task {
|
|
|
23
55
|
* wants to live in two goals is a tag.
|
|
24
56
|
*/
|
|
25
57
|
goal: string | null
|
|
26
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Derived, never stored: true while any ask is open. There is no flag to
|
|
60
|
+
* set — a flag with no stated reason is the failure mode asks exist to end.
|
|
61
|
+
*/
|
|
27
62
|
needsHuman: boolean
|
|
63
|
+
/** Every ask on this ticket, open and resolved, in creation order. */
|
|
64
|
+
asks: Ask[]
|
|
28
65
|
/**
|
|
29
|
-
*
|
|
66
|
+
* Keys of tasks this one blocks / is blocked by. One relation, two views:
|
|
30
67
|
* a single `(blocker, blocked)` row shows up in the blocker's `blocks` and
|
|
31
68
|
* the blocked task's `blockedBy`, so the mirror can never drift.
|
|
32
69
|
*/
|
|
33
|
-
blocks:
|
|
34
|
-
blockedBy:
|
|
70
|
+
blocks: string[]
|
|
71
|
+
blockedBy: string[]
|
|
35
72
|
/** Pull request URLs attached to this task. */
|
|
36
73
|
prs: string[]
|
|
37
74
|
/** Sort key within a status column; smaller sorts first. */
|
|
@@ -77,13 +114,13 @@ export interface ClaimsConfig {
|
|
|
77
114
|
|
|
78
115
|
export interface ProjectConfig {
|
|
79
116
|
name: string
|
|
80
|
-
/** Uppercase id prefix, e.g. "PHO" → PHO-
|
|
117
|
+
/** Uppercase id prefix, e.g. "PHO" → PHO-x7k4m, PHO-q3v8d, … */
|
|
81
118
|
prefix: string
|
|
82
119
|
version: number
|
|
83
120
|
/**
|
|
84
|
-
* Pre-0.8 spelling of `claims.branchPrefix
|
|
85
|
-
*
|
|
86
|
-
*
|
|
121
|
+
* Pre-0.8 spelling of `claims.branchPrefix`. Never honored — typed only so
|
|
122
|
+
* `claimNamespace` can refuse it by name instead of silently defaulting the
|
|
123
|
+
* board's committed lock convention out from under its workers.
|
|
87
124
|
*/
|
|
88
125
|
claimPrefix?: string
|
|
89
126
|
claims?: ClaimsConfig
|
|
@@ -131,7 +168,7 @@ export interface TaskFilter {
|
|
|
131
168
|
/** Any-of: a task matches if it carries at least one of these tags. */
|
|
132
169
|
tags?: string[]
|
|
133
170
|
goal?: string
|
|
134
|
-
/** undefined = don't filter
|
|
171
|
+
/** Matches the *derived* flag — open asks, nothing else. undefined = don't filter. */
|
|
135
172
|
needsHuman?: boolean
|
|
136
173
|
/** true = list the archive instead of the board. */
|
|
137
174
|
archived?: boolean
|
|
@@ -143,9 +180,8 @@ export interface TaskInput {
|
|
|
143
180
|
status?: Status
|
|
144
181
|
tags?: string[]
|
|
145
182
|
goal?: string | null
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
blockedBy?: number[]
|
|
183
|
+
blocks?: string[]
|
|
184
|
+
blockedBy?: string[]
|
|
149
185
|
prs?: string[]
|
|
150
186
|
}
|
|
151
187
|
|
|
@@ -155,13 +191,12 @@ export interface TaskPatch {
|
|
|
155
191
|
status?: Status
|
|
156
192
|
tags?: string[]
|
|
157
193
|
goal?: string | null
|
|
158
|
-
needsHuman?: boolean
|
|
159
194
|
/**
|
|
160
195
|
* Full replacement, like `tags` — the store reconciles the relation table
|
|
161
196
|
* to match. Writing either side updates the other task's mirror view.
|
|
162
197
|
*/
|
|
163
|
-
blocks?:
|
|
164
|
-
blockedBy?:
|
|
198
|
+
blocks?: string[]
|
|
199
|
+
blockedBy?: string[]
|
|
165
200
|
prs?: string[]
|
|
166
201
|
/** Explicit board position (used by drag-and-drop in the UI). */
|
|
167
202
|
position?: number
|