@nickmeriano/task 0.7.0 → 0.8.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 +12 -7
- package/dist/check.d.ts +42 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +364 -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 +209 -0
- package/dist/check.test.js.map +1 -0
- package/dist/claim.d.ts +56 -3
- package/dist/claim.d.ts.map +1 -1
- package/dist/claim.js +164 -9
- package/dist/claim.js.map +1 -1
- package/dist/claim.test.js +168 -14
- package/dist/claim.test.js.map +1 -1
- package/dist/cli.js +504 -93
- package/dist/cli.js.map +1 -1
- package/dist/file-store.d.ts +41 -15
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +197 -99
- package/dist/file-store.js.map +1 -1
- 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 +43 -0
- package/dist/overview.d.ts.map +1 -0
- package/dist/overview.js +53 -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 +47 -0
- package/dist/overview.test.js.map +1 -0
- package/dist/promote.test.d.ts +14 -0
- package/dist/promote.test.d.ts.map +1 -0
- package/dist/promote.test.js +106 -0
- package/dist/promote.test.js.map +1 -0
- package/dist/search.d.ts +32 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +66 -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 +10 -2
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +16 -50
- 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 +77 -48
- package/dist/store.test.js.map +1 -1
- package/dist/ticket-doc.d.ts +55 -2
- package/dist/ticket-doc.d.ts.map +1 -1
- package/dist/ticket-doc.js +177 -7
- package/dist/ticket-doc.js.map +1 -1
- package/dist/types.d.ts +67 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +97 -31
- package/src/check.test.ts +271 -0
- package/src/check.ts +454 -0
- package/src/claim.test.ts +202 -14
- package/src/claim.ts +194 -10
- package/src/cli.ts +510 -89
- package/src/file-store.ts +217 -111
- package/src/index.ts +4 -2
- package/src/overview.test.ts +51 -0
- package/src/overview.ts +90 -0
- package/src/promote.test.ts +131 -0
- package/src/search.test.ts +64 -0
- package/src/search.ts +91 -0
- package/src/server.ts +11 -2
- package/src/store.test.ts +89 -57
- package/src/store.ts +18 -431
- package/src/ticket-doc.ts +210 -10
- package/src/types.ts +71 -14
- package/ui/dist/assets/{index-oJzomUDL.js → index-BJmOsOdR.js} +76 -76
- package/ui/dist/assets/index-BoqQlqSU.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-CXW8uT5f.css +0 -1
package/src/ticket-doc.ts
CHANGED
|
@@ -23,7 +23,8 @@ export interface TicketDoc {
|
|
|
23
23
|
description: string
|
|
24
24
|
status: Status
|
|
25
25
|
tags: string[]
|
|
26
|
-
|
|
26
|
+
/** Slug of the goal this ticket belongs to — at most one, by design. */
|
|
27
|
+
goal: string | null
|
|
27
28
|
needsHuman: boolean
|
|
28
29
|
/** Task numbers blocking this one — the single stored side of the relation. */
|
|
29
30
|
blockedBy: number[]
|
|
@@ -31,8 +32,37 @@ export interface TicketDoc {
|
|
|
31
32
|
position: number
|
|
32
33
|
createdAt: string
|
|
33
34
|
updatedAt: string
|
|
35
|
+
/**
|
|
36
|
+
* Frontmatter keys this version doesn't know, as raw `[key, value]` pairs in
|
|
37
|
+
* file order. Carried so a rewrite can't silently drop them — a hand-added
|
|
38
|
+
* field (or one written by a newer CLI) survives every update, re-emitted
|
|
39
|
+
* after the known fields. Present only when the file actually has any.
|
|
40
|
+
*/
|
|
41
|
+
extras?: [string, string][]
|
|
34
42
|
}
|
|
35
43
|
|
|
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
|
+
*/
|
|
50
|
+
export const TICKET_FIELDS = [
|
|
51
|
+
"status",
|
|
52
|
+
"tags",
|
|
53
|
+
"goal",
|
|
54
|
+
"milestone",
|
|
55
|
+
"needs_human",
|
|
56
|
+
"blocked_by",
|
|
57
|
+
"prs",
|
|
58
|
+
"position",
|
|
59
|
+
"created",
|
|
60
|
+
"updated",
|
|
61
|
+
] as const
|
|
62
|
+
|
|
63
|
+
/** The frontmatter keys a comment file may carry. */
|
|
64
|
+
export const COMMENT_FIELDS = ["author", "created"] as const
|
|
65
|
+
|
|
36
66
|
export interface CommentDoc {
|
|
37
67
|
author: string
|
|
38
68
|
createdAt: string
|
|
@@ -64,13 +94,21 @@ function parseScalar(raw: string): unknown {
|
|
|
64
94
|
}
|
|
65
95
|
}
|
|
66
96
|
|
|
67
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Split a file into frontmatter fields and body. Two callers, two stances:
|
|
99
|
+
* with a `where` the CLI is parsing and every malformation throws, naming the
|
|
100
|
+
* file; with `where: null` the hosted board is rendering and a file with no
|
|
101
|
+
* frontmatter returns null while a merely-odd line is skipped.
|
|
102
|
+
*/
|
|
103
|
+
function splitRaw(text: string, where: string | null): RawDoc | null {
|
|
68
104
|
const lines = text.replace(/\r\n/g, "\n").split("\n")
|
|
69
105
|
if (lines[0] !== "---") {
|
|
106
|
+
if (where === null) return null
|
|
70
107
|
throw new TicketParseError(`${where}: expected the file to start with a --- frontmatter block`)
|
|
71
108
|
}
|
|
72
109
|
const end = lines.indexOf("---", 1)
|
|
73
110
|
if (end < 0) {
|
|
111
|
+
if (where === null) return null
|
|
74
112
|
throw new TicketParseError(`${where}: unterminated frontmatter — no closing ---`)
|
|
75
113
|
}
|
|
76
114
|
const fields = new Map<string, string>()
|
|
@@ -78,6 +116,7 @@ function splitRaw(text: string, where: string): RawDoc {
|
|
|
78
116
|
if (!line.trim()) continue
|
|
79
117
|
const colon = line.indexOf(":")
|
|
80
118
|
if (colon < 0) {
|
|
119
|
+
if (where === null) continue
|
|
81
120
|
throw new TicketParseError(`${where}: not a "key: value" frontmatter line: ${line}`)
|
|
82
121
|
}
|
|
83
122
|
fields.set(line.slice(0, colon).trim(), line.slice(colon + 1))
|
|
@@ -131,16 +170,16 @@ function splitTitle(body: string): { title: string | null; description: string }
|
|
|
131
170
|
}
|
|
132
171
|
|
|
133
172
|
export function parseTicket(text: string, where: string): TicketDoc {
|
|
134
|
-
const raw = splitRaw(text, where)
|
|
173
|
+
const raw = splitRaw(text, where)!
|
|
135
174
|
const status = stringField(raw, "status", where, "todo")
|
|
136
175
|
if (!isStatus(status)) {
|
|
137
176
|
throw new TicketParseError(
|
|
138
177
|
`${where}: invalid status "${status}" — one of: ${STATUSES.join(", ")}`,
|
|
139
178
|
)
|
|
140
179
|
}
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
143
|
-
throw new TicketParseError(`${where}:
|
|
180
|
+
const goal = field(raw, "goal") ?? field(raw, "milestone")
|
|
181
|
+
if (goal !== undefined && goal !== null && typeof goal !== "string") {
|
|
182
|
+
throw new TicketParseError(`${where}: goal should be a string`)
|
|
144
183
|
}
|
|
145
184
|
const needsHuman = field(raw, "needs_human")
|
|
146
185
|
const position = field(raw, "position")
|
|
@@ -151,18 +190,24 @@ export function parseTicket(text: string, where: string): TicketDoc {
|
|
|
151
190
|
if (title === null) {
|
|
152
191
|
throw new TicketParseError(`${where}: expected the body to start with "# <title>"`)
|
|
153
192
|
}
|
|
193
|
+
const known = new Set<string>(TICKET_FIELDS)
|
|
194
|
+
const extras: [string, string][] = []
|
|
195
|
+
for (const [key, value] of raw.fields) {
|
|
196
|
+
if (!known.has(key)) extras.push([key, value.trim()])
|
|
197
|
+
}
|
|
154
198
|
return {
|
|
155
199
|
title,
|
|
156
200
|
description,
|
|
157
201
|
status,
|
|
158
202
|
tags: stringList(raw, "tags", where),
|
|
159
|
-
|
|
203
|
+
goal: typeof goal === "string" && goal !== "" ? goal : null,
|
|
160
204
|
needsHuman: needsHuman === true,
|
|
161
205
|
blockedBy: numberList(raw, "blocked_by", where),
|
|
162
206
|
prs: stringList(raw, "prs", where),
|
|
163
207
|
position: typeof position === "number" ? position : 0,
|
|
164
208
|
createdAt: stringField(raw, "created", where, ""),
|
|
165
209
|
updatedAt: stringField(raw, "updated", where, ""),
|
|
210
|
+
...(extras.length ? { extras } : {}),
|
|
166
211
|
}
|
|
167
212
|
}
|
|
168
213
|
|
|
@@ -174,27 +219,112 @@ function list(values: (string | number)[]): string {
|
|
|
174
219
|
/**
|
|
175
220
|
* The canonical bytes for a ticket. Field order is fixed and empty fields are
|
|
176
221
|
* omitted, so an unchanged ticket always serializes identically — diffs show
|
|
177
|
-
* edits, never churn.
|
|
222
|
+
* edits, never churn. Unknown fields the parse carried through are re-emitted
|
|
223
|
+
* after the known ones, in their original order.
|
|
178
224
|
*/
|
|
179
225
|
export function serializeTicket(doc: TicketDoc): string {
|
|
180
226
|
const lines = ["---", `status: ${doc.status}`]
|
|
181
227
|
if (doc.tags.length) lines.push(`tags: ${list(doc.tags)}`)
|
|
182
|
-
if (doc.
|
|
228
|
+
if (doc.goal) lines.push(`goal: ${JSON.stringify(doc.goal)}`)
|
|
183
229
|
if (doc.needsHuman) lines.push(`needs_human: true`)
|
|
184
230
|
if (doc.blockedBy.length) lines.push(`blocked_by: ${list(doc.blockedBy)}`)
|
|
185
231
|
if (doc.prs.length) lines.push(`prs: ${list(doc.prs)}`)
|
|
186
232
|
lines.push(`position: ${doc.position}`)
|
|
187
233
|
lines.push(`created: ${doc.createdAt}`)
|
|
188
234
|
lines.push(`updated: ${doc.updatedAt}`)
|
|
235
|
+
for (const [key, value] of doc.extras ?? []) lines.push(`${key}: ${value}`)
|
|
236
|
+
lines.push("---", "", `# ${doc.title}`)
|
|
237
|
+
if (doc.description) lines.push("", doc.description)
|
|
238
|
+
return `${lines.join("\n")}\n`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Goals ────────────────────────────────────────────────────────────────────
|
|
242
|
+
//
|
|
243
|
+
// A goal is one file — `.task/goals/<slug>.md` — in the ticket format minus
|
|
244
|
+
// the workflow fields: no status (progress is derived from its tasks), no
|
|
245
|
+
// position (goals aren't a column). The slug is the filename stem and the
|
|
246
|
+
// identity tickets reference via `goal:`; the title inside can change freely.
|
|
247
|
+
|
|
248
|
+
/** Everything `goals/<slug>.md` stores. The slug is the filename, not a field. */
|
|
249
|
+
export interface GoalDoc {
|
|
250
|
+
title: string
|
|
251
|
+
description: string
|
|
252
|
+
createdAt: string
|
|
253
|
+
updatedAt: string
|
|
254
|
+
/** Unknown frontmatter, carried through rewrites — same deal as tickets. */
|
|
255
|
+
extras?: [string, string][]
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The frontmatter keys a goal file may carry. */
|
|
259
|
+
export const GOAL_FIELDS = ["created", "updated"] as const
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* What a goal slug may look like — same alphabet as the claim-branch-safe
|
|
263
|
+
* ticket ids, because the slug ends up in frontmatter, URLs and prompts.
|
|
264
|
+
* "archive" is reserved: `goals/archive/` is where archived goals live.
|
|
265
|
+
*/
|
|
266
|
+
export const GOAL_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/
|
|
267
|
+
|
|
268
|
+
export function isGoalSlug(value: string): boolean {
|
|
269
|
+
return GOAL_SLUG_PATTERN.test(value) && value !== "archive"
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Derive a slug from a title: "Trust & guardrail layer" → "trust-guardrail-layer". */
|
|
273
|
+
export function slugifyGoal(title: string): string {
|
|
274
|
+
return title
|
|
275
|
+
.toLowerCase()
|
|
276
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
277
|
+
.replace(/^-+|-+$/g, "")
|
|
278
|
+
.slice(0, 64)
|
|
279
|
+
.replace(/-+$/g, "")
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function parseGoal(text: string, where: string): GoalDoc {
|
|
283
|
+
const raw = splitRaw(text, where)!
|
|
284
|
+
const { title, description } = splitTitle(raw.body)
|
|
285
|
+
if (title === null) {
|
|
286
|
+
throw new TicketParseError(`${where}: expected the body to start with "# <title>"`)
|
|
287
|
+
}
|
|
288
|
+
const known = new Set<string>(GOAL_FIELDS)
|
|
289
|
+
const extras: [string, string][] = []
|
|
290
|
+
for (const [key, value] of raw.fields) {
|
|
291
|
+
if (!known.has(key)) extras.push([key, value.trim()])
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
title,
|
|
295
|
+
description,
|
|
296
|
+
createdAt: stringField(raw, "created", where, ""),
|
|
297
|
+
updatedAt: stringField(raw, "updated", where, ""),
|
|
298
|
+
...(extras.length ? { extras } : {}),
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Canonical bytes for a goal — fixed field order, like tickets. */
|
|
303
|
+
export function serializeGoal(doc: GoalDoc): string {
|
|
304
|
+
const lines = ["---", `created: ${doc.createdAt}`, `updated: ${doc.updatedAt}`]
|
|
305
|
+
for (const [key, value] of doc.extras ?? []) lines.push(`${key}: ${value}`)
|
|
189
306
|
lines.push("---", "", `# ${doc.title}`)
|
|
190
307
|
if (doc.description) lines.push("", doc.description)
|
|
191
308
|
return `${lines.join("\n")}\n`
|
|
192
309
|
}
|
|
193
310
|
|
|
311
|
+
/** A goal, forgivingly — null when there's no frontmatter at all. */
|
|
312
|
+
export function parseGoalLenient(text: string): GoalDoc | null {
|
|
313
|
+
const raw = splitRaw(text, null)
|
|
314
|
+
if (!raw) return null
|
|
315
|
+
const { title, description } = splitTitle(raw.body)
|
|
316
|
+
return {
|
|
317
|
+
title: title ?? "",
|
|
318
|
+
description,
|
|
319
|
+
createdAt: lenientString(field(raw, "created")),
|
|
320
|
+
updatedAt: lenientString(field(raw, "updated")),
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
194
324
|
// ── Comments ─────────────────────────────────────────────────────────────────
|
|
195
325
|
|
|
196
326
|
export function parseComment(text: string, where: string): CommentDoc {
|
|
197
|
-
const raw = splitRaw(text, where)
|
|
327
|
+
const raw = splitRaw(text, where)!
|
|
198
328
|
return {
|
|
199
329
|
author: stringField(raw, "author", where, ""),
|
|
200
330
|
createdAt: stringField(raw, "created", where, ""),
|
|
@@ -209,6 +339,76 @@ export function serializeComment(doc: CommentDoc): string {
|
|
|
209
339
|
return `${lines.join("\n")}\n`
|
|
210
340
|
}
|
|
211
341
|
|
|
342
|
+
// ── Lenient parsing ──────────────────────────────────────────────────────────
|
|
343
|
+
//
|
|
344
|
+
// The hosted board's read of the same format. The CLI is strict — it wrote the
|
|
345
|
+
// file, so a malformation is a bug worth halting on — but a renderer is looking
|
|
346
|
+
// at whatever a hand-edit or a bad merge left behind, and it must degrade
|
|
347
|
+
// rather than throw: a broken ticket becomes a degraded ticket (unknown status
|
|
348
|
+
// → backlog, missing heading → empty title), never a broken board. Living here
|
|
349
|
+
// rather than restated in the worker keeps one definition of the format.
|
|
350
|
+
|
|
351
|
+
function lenientStrings(value: unknown): string[] {
|
|
352
|
+
return Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : []
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function lenientNumbers(value: unknown): number[] {
|
|
356
|
+
return Array.isArray(value)
|
|
357
|
+
? value.filter((v): v is number => typeof v === "number" && Number.isInteger(v))
|
|
358
|
+
: []
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function lenientString(value: unknown): string {
|
|
362
|
+
return typeof value === "string" ? value : ""
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** A ticket, forgivingly — or null when there's no frontmatter to read at all. */
|
|
366
|
+
export function parseTicketLenient(text: string): TicketDoc | null {
|
|
367
|
+
const raw = splitRaw(text, null)
|
|
368
|
+
if (!raw) return null
|
|
369
|
+
const status = field(raw, "status")
|
|
370
|
+
// The pre-0.8 `milestone` key still reads as the goal — the lenient parser
|
|
371
|
+
// exists precisely for old bytes (hand-edits, and here: old branches the
|
|
372
|
+
// hosted board previews).
|
|
373
|
+
const goal = field(raw, "goal") ?? field(raw, "milestone")
|
|
374
|
+
const position = field(raw, "position")
|
|
375
|
+
const { title, description } = splitTitle(raw.body)
|
|
376
|
+
return {
|
|
377
|
+
title: title ?? "",
|
|
378
|
+
description,
|
|
379
|
+
status: typeof status === "string" && isStatus(status) ? status : "backlog",
|
|
380
|
+
tags: lenientStrings(field(raw, "tags")),
|
|
381
|
+
goal: typeof goal === "string" && goal !== "" ? goal : null,
|
|
382
|
+
needsHuman: field(raw, "needs_human") === true,
|
|
383
|
+
blockedBy: [...new Set(lenientNumbers(field(raw, "blocked_by")))].sort((a, b) => a - b),
|
|
384
|
+
prs: lenientStrings(field(raw, "prs")),
|
|
385
|
+
position: typeof position === "number" && Number.isFinite(position) ? position : 0,
|
|
386
|
+
createdAt: lenientString(field(raw, "created")),
|
|
387
|
+
updatedAt: lenientString(field(raw, "updated")),
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* The frontmatter keys a file carries, leniently — null when there's no
|
|
393
|
+
* frontmatter at all. `task check` uses this to spot unknown keys in files
|
|
394
|
+
* whose parsed shape (comments) doesn't carry them through.
|
|
395
|
+
*/
|
|
396
|
+
export function frontmatterKeys(text: string): string[] | null {
|
|
397
|
+
const raw = splitRaw(text, null)
|
|
398
|
+
return raw ? [...raw.fields.keys()] : null
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** A comment, forgivingly — or null when there's no frontmatter at all. */
|
|
402
|
+
export function parseCommentLenient(text: string): CommentDoc | null {
|
|
403
|
+
const raw = splitRaw(text, null)
|
|
404
|
+
if (!raw) return null
|
|
405
|
+
return {
|
|
406
|
+
author: lenientString(field(raw, "author")),
|
|
407
|
+
createdAt: lenientString(field(raw, "created")),
|
|
408
|
+
body: raw.body,
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
212
412
|
/**
|
|
213
413
|
* A comment's filename stem — readable, sortable, and unique enough that two
|
|
214
414
|
* branches almost never mint the same one: `2026-08-15T165943-claude`. The
|
package/src/types.ts
CHANGED
|
@@ -17,7 +17,12 @@ export interface Task {
|
|
|
17
17
|
description: string
|
|
18
18
|
status: Status
|
|
19
19
|
tags: string[]
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Slug of the goal this task belongs to — at most one. Tags answer "what
|
|
22
|
+
* kind" (many); the goal answers "where is this going" (one). Something that
|
|
23
|
+
* wants to live in two goals is a tag.
|
|
24
|
+
*/
|
|
25
|
+
goal: string | null
|
|
21
26
|
/** This ticket can't be finished by an agent alone. */
|
|
22
27
|
needsHuman: boolean
|
|
23
28
|
/**
|
|
@@ -42,11 +47,7 @@ export interface Task {
|
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
export interface Comment {
|
|
45
|
-
/**
|
|
46
|
-
* Stable opaque id. On file-backed boards it's the comment's filename stem
|
|
47
|
-
* (e.g. "2026-08-15T165943-claude"); on legacy SQLite boards, the rowid as a
|
|
48
|
-
* string.
|
|
49
|
-
*/
|
|
50
|
+
/** Stable opaque id — the comment's filename stem (e.g. "2026-08-15T165943-claude"). */
|
|
50
51
|
id: string
|
|
51
52
|
taskId: string
|
|
52
53
|
author: string
|
|
@@ -54,26 +55,82 @@ export interface Comment {
|
|
|
54
55
|
createdAt: string
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/** How this board's tickets get claimed — see `claim.ts` for the mechanics. */
|
|
59
|
+
export interface ClaimsConfig {
|
|
60
|
+
/**
|
|
61
|
+
* Branch namespace `task claim` creates claim branches under — committed
|
|
62
|
+
* with the board on purpose, so every worker and every clone agree on what
|
|
63
|
+
* "claimed" looks like. Defaults to "task/claim/"; repos whose workers are
|
|
64
|
+
* Claude Code cloud sessions set "claude/task/", the one prefix that
|
|
65
|
+
* runtime can push without extra ceremony. Must agree across a repo's
|
|
66
|
+
* boards (`task check` lints it) — workflows match claim branches by
|
|
67
|
+
* pattern, and one repo with two patterns matches neither reliably.
|
|
68
|
+
*/
|
|
69
|
+
branchPrefix?: string
|
|
70
|
+
/**
|
|
71
|
+
* Repo-wide cap on open claims: `task claim` refuses (exit 2) while this
|
|
72
|
+
* many claim branches exist on origin, `--force` overrides. Read from the
|
|
73
|
+
* repo's *root* board only — a WIP cap per board is no cap at all.
|
|
74
|
+
*/
|
|
75
|
+
maxOpenCount?: number
|
|
76
|
+
}
|
|
77
|
+
|
|
57
78
|
export interface ProjectConfig {
|
|
58
79
|
name: string
|
|
59
80
|
/** Uppercase id prefix, e.g. "PHO" → PHO-1, PHO-2, … */
|
|
60
81
|
prefix: string
|
|
61
82
|
version: number
|
|
62
83
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* Claude Code cloud sessions set "claude/task/", the one prefix that
|
|
67
|
-
* runtime can push without extra ceremony.
|
|
84
|
+
* Pre-0.8 spelling of `claims.branchPrefix`, still read (the shared lock
|
|
85
|
+
* convention must not strand in-flight claims across a version bump).
|
|
86
|
+
* `task check` nags about it; `--fix` migrates it.
|
|
68
87
|
*/
|
|
69
88
|
claimPrefix?: string
|
|
89
|
+
claims?: ClaimsConfig
|
|
90
|
+
/**
|
|
91
|
+
* Root board only: the repo's automation-visible boards, by id prefix, in
|
|
92
|
+
* priority order. This is *selection scope* — where `claim --next` and
|
|
93
|
+
* `list --claimable` look — not permission: explicit-id claims work on any
|
|
94
|
+
* board regardless. Absent → automation sees the nearest board only, so a
|
|
95
|
+
* vendored board can't opt itself in.
|
|
96
|
+
*/
|
|
97
|
+
boards?: string[]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A goal: a titled, described destination that tasks belong to. Progress is
|
|
102
|
+
* always derived from its tasks — a goal has no stored status on purpose,
|
|
103
|
+
* because a second state machine is a second thing that can lie.
|
|
104
|
+
*/
|
|
105
|
+
export interface Goal {
|
|
106
|
+
/** Filename stem under `.task/goals/` — the stable identity tickets reference. */
|
|
107
|
+
slug: string
|
|
108
|
+
title: string
|
|
109
|
+
/** The strategic why. Tickets keep the how; this is the context they share. */
|
|
110
|
+
description: string
|
|
111
|
+
createdAt: string
|
|
112
|
+
updatedAt: string
|
|
113
|
+
/** Set (to true) only on goals living in `.task/goals/archive/`. */
|
|
114
|
+
archived?: boolean
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface GoalInput {
|
|
118
|
+
title: string
|
|
119
|
+
/** Derived from the title when omitted. */
|
|
120
|
+
slug?: string
|
|
121
|
+
description?: string
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface GoalPatch {
|
|
125
|
+
title?: string
|
|
126
|
+
description?: string
|
|
70
127
|
}
|
|
71
128
|
|
|
72
129
|
export interface TaskFilter {
|
|
73
130
|
statuses?: Status[]
|
|
74
131
|
/** Any-of: a task matches if it carries at least one of these tags. */
|
|
75
132
|
tags?: string[]
|
|
76
|
-
|
|
133
|
+
goal?: string
|
|
77
134
|
/** undefined = don't filter on it at all. */
|
|
78
135
|
needsHuman?: boolean
|
|
79
136
|
/** true = list the archive instead of the board. */
|
|
@@ -85,7 +142,7 @@ export interface TaskInput {
|
|
|
85
142
|
description?: string
|
|
86
143
|
status?: Status
|
|
87
144
|
tags?: string[]
|
|
88
|
-
|
|
145
|
+
goal?: string | null
|
|
89
146
|
needsHuman?: boolean
|
|
90
147
|
blocks?: number[]
|
|
91
148
|
blockedBy?: number[]
|
|
@@ -97,7 +154,7 @@ export interface TaskPatch {
|
|
|
97
154
|
description?: string
|
|
98
155
|
status?: Status
|
|
99
156
|
tags?: string[]
|
|
100
|
-
|
|
157
|
+
goal?: string | null
|
|
101
158
|
needsHuman?: boolean
|
|
102
159
|
/**
|
|
103
160
|
* Full replacement, like `tags` — the store reconciles the relation table
|