@wirecat/cli-core 0.511.0 → 0.513.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/calendar/mcp/index.d.ts +46 -10
  3. package/dist/calendar/mcp/index.js +60 -25
  4. package/dist/cli/list-query.d.ts +4 -2
  5. package/dist/cli/list-query.js +3 -3
  6. package/dist/cli/messenger/tasks-command.js +4 -5
  7. package/dist/entities/index.d.ts +35 -8
  8. package/dist/entities/task-results.d.ts +22 -7
  9. package/dist/entities/task-results.js +4 -1
  10. package/dist/entities/task.d.ts +18 -1
  11. package/dist/entities/task.js +12 -1
  12. package/dist/errors/reasons.js +1 -0
  13. package/dist/services/task-rules.js +2 -2
  14. package/dist/services/tasks.d.ts +1 -1
  15. package/dist/services/tasks.js +7 -5
  16. package/dist/store/index.d.ts +4 -1
  17. package/dist/store/index.js +2 -0
  18. package/dist/store/sqlite/knowledge.js +3 -2
  19. package/dist/store/sqlite/manifest.js +1 -0
  20. package/dist/store/sqlite/migrations.generated.js +12 -0
  21. package/dist/store/sqlite/person-preparation.js +1 -1
  22. package/dist/store/sqlite/schema.d.ts +48 -0
  23. package/dist/store/sqlite/schema.js +3 -0
  24. package/dist/store/sqlite/tags.d.ts +1 -2
  25. package/dist/store/sqlite/tags.js +18 -3
  26. package/dist/store/sqlite/task-board.d.ts +16 -0
  27. package/dist/store/sqlite/task-board.js +192 -0
  28. package/dist/store/sqlite/task-candidates.js +1 -1
  29. package/dist/store/sqlite/task-category-settings.js +1 -1
  30. package/dist/store/sqlite/task-close-store.js +6 -4
  31. package/dist/store/sqlite/task-pairs.js +4 -3
  32. package/dist/store/sqlite/task-relations.js +3 -2
  33. package/dist/store/sqlite/tasks.js +16 -5
  34. package/dist/store/sqlite/work-tasks.d.ts +96 -9
  35. package/dist/store/sqlite/work-tasks.js +236 -32
  36. package/dist/tasks/index.d.ts +2 -1
  37. package/dist/tasks/index.js +2 -1
  38. package/dist/tasks/model.d.ts +5 -2
  39. package/dist/tasks/model.js +5 -2
  40. package/dist/tasks/priority.d.ts +18 -0
  41. package/dist/tasks/priority.js +37 -0
  42. package/dist/tasks/service.js +2 -2
  43. package/package.json +1 -1
@@ -7,8 +7,11 @@ export declare const taskCard: v.StrictObjectSchema<{
7
7
  readonly title: v.StringSchema<undefined>;
8
8
  readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
9
9
  readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
10
- readonly status: v.PicklistSchema<readonly ["open", "done", "dismissed"], undefined>;
10
+ readonly status: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
11
11
  readonly priority: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
12
+ readonly priority_label: v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>;
13
+ readonly agent: v.NullableSchema<v.StringSchema<undefined>, undefined>;
14
+ readonly revision: v.NumberSchema<undefined>;
12
15
  readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
13
16
  readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
14
17
  readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
@@ -69,7 +72,7 @@ export declare const taskCreated: v.StrictObjectSchema<{
69
72
  readonly account: v.StringSchema<undefined>;
70
73
  readonly group: v.StringSchema<undefined>;
71
74
  readonly kind: v.PicklistSchema<readonly ["question", "request", "mention", "promise"], undefined>;
72
- readonly state: v.PicklistSchema<readonly ["open", "done", "dismissed"], undefined>;
75
+ readonly state: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
73
76
  readonly reason: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
74
77
  readonly origin: v.PicklistSchema<readonly ["rule", "agent", "owner"], undefined>;
75
78
  readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
@@ -108,8 +111,11 @@ export declare const taskCreated: v.StrictObjectSchema<{
108
111
  readonly title: v.StringSchema<undefined>;
109
112
  readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
110
113
  readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
111
- readonly status: v.PicklistSchema<readonly ["open", "done", "dismissed"], undefined>;
114
+ readonly status: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
112
115
  readonly priority: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
116
+ readonly priority_label: v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>;
117
+ readonly agent: v.NullableSchema<v.StringSchema<undefined>, undefined>;
118
+ readonly revision: v.NumberSchema<undefined>;
113
119
  readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
114
120
  readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
115
121
  readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
@@ -171,7 +177,7 @@ export declare const taskClosed: v.StrictObjectSchema<{
171
177
  readonly account: v.StringSchema<undefined>;
172
178
  readonly group: v.StringSchema<undefined>;
173
179
  readonly kind: v.PicklistSchema<readonly ["question", "request", "mention", "promise"], undefined>;
174
- readonly state: v.PicklistSchema<readonly ["open", "done", "dismissed"], undefined>;
180
+ readonly state: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
175
181
  readonly reason: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
176
182
  readonly origin: v.PicklistSchema<readonly ["rule", "agent", "owner"], undefined>;
177
183
  readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
@@ -210,8 +216,11 @@ export declare const taskLinks: v.UnionSchema<[v.StrictObjectSchema<{
210
216
  readonly title: v.StringSchema<undefined>;
211
217
  readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
212
218
  readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
213
- readonly status: v.PicklistSchema<readonly ["open", "done", "dismissed"], undefined>;
219
+ readonly status: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
214
220
  readonly priority: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
221
+ readonly priority_label: v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>;
222
+ readonly agent: v.NullableSchema<v.StringSchema<undefined>, undefined>;
223
+ readonly revision: v.NumberSchema<undefined>;
215
224
  readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
216
225
  readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
217
226
  readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
@@ -340,8 +349,11 @@ export declare const taskMerge: v.StrictObjectSchema<{
340
349
  readonly title: v.StringSchema<undefined>;
341
350
  readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
342
351
  readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
343
- readonly status: v.PicklistSchema<readonly ["open", "done", "dismissed"], undefined>;
352
+ readonly status: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
344
353
  readonly priority: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
354
+ readonly priority_label: v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>;
355
+ readonly agent: v.NullableSchema<v.StringSchema<undefined>, undefined>;
356
+ readonly revision: v.NumberSchema<undefined>;
345
357
  readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
346
358
  readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
347
359
  readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
@@ -435,8 +447,11 @@ export declare const taskUnmerge: v.StrictObjectSchema<{
435
447
  readonly title: v.StringSchema<undefined>;
436
448
  readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
437
449
  readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
438
- readonly status: v.PicklistSchema<readonly ["open", "done", "dismissed"], undefined>;
450
+ readonly status: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
439
451
  readonly priority: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
452
+ readonly priority_label: v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>;
453
+ readonly agent: v.NullableSchema<v.StringSchema<undefined>, undefined>;
454
+ readonly revision: v.NumberSchema<undefined>;
440
455
  readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
441
456
  readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
442
457
  readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
@@ -1,7 +1,7 @@
1
1
  import * as v from "valibot";
2
2
  import { TASK_TYPES } from "../domain/knowledge.js";
3
3
  import { LINK_KINDS, LINK_SOURCES } from "../store/sqlite/link-kinds.js";
4
- import { TASK_KINDS, TASK_ORIGINS, TASK_STATES, TASK_VERDICTS } from "../tasks/index.js";
4
+ import { PRIORITY_LABELS, TASK_KINDS, TASK_ORIGINS, TASK_STATES, TASK_VERDICTS } from "../tasks/index.js";
5
5
  import { count, id, reference, text, time } from "./fields.js";
6
6
  import { closeSuggestion } from "./task.js";
7
7
  import { taskPair } from "./task-pair.js";
@@ -30,6 +30,9 @@ export const taskCard = v.strictObject({
30
30
  type: v.picklist(TASK_TYPES),
31
31
  status: v.picklist(TASK_STATES),
32
32
  priority: v.nullable(v.number()),
33
+ priority_label: v.nullable(v.picklist(PRIORITY_LABELS)),
34
+ agent: v.nullable(text),
35
+ revision: v.number(),
33
36
  parent: v.nullable(reference),
34
37
  source: v.nullable(text),
35
38
  due_at: nullableTime,
@@ -16,7 +16,7 @@ export declare const task: v.StrictObjectSchema<{
16
16
  readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
17
17
  readonly description: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
18
18
  readonly kind: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
19
- readonly state: v.PicklistSchema<readonly ["open", "done", "dismissed"], undefined>;
19
+ readonly state: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
20
20
  readonly origin: v.OptionalSchema<v.PicklistSchema<readonly ["rule", "agent", "owner"], undefined>, undefined>;
21
21
  readonly project: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
22
22
  readonly account: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
@@ -24,7 +24,24 @@ export declare const task: v.StrictObjectSchema<{
24
24
  readonly chat: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
25
25
  /** The message it came from. */
26
26
  readonly source_message: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
27
+ /** 0 to 1, higher first; `priority_label` reads it as low, medium, high or critical. */
27
28
  readonly priority: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
29
+ readonly priority_label: v.OptionalSchema<v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>, undefined>;
30
+ /** The agent working on it, apart from its assignees and its claim. */
31
+ readonly agent: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
32
+ /** Give it back with `--revision` to refuse a write made on a stale copy. */
33
+ readonly revision: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
34
+ readonly assignments: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
35
+ readonly to: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
36
+ readonly role: v.PicklistSchema<["assignee", "reviewer", "watcher"], undefined>;
37
+ }, undefined>, undefined>, undefined>;
38
+ /** Open work whose blocked-by tasks are all done. */
39
+ readonly ready: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
40
+ /** The blocked-by tasks not done yet: open (any open state), dismissed, or unavailable. */
41
+ readonly blocked_by: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
42
+ readonly task: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
43
+ readonly state: v.PicklistSchema<["open", "dismissed", "unavailable"], undefined>;
44
+ }, undefined>, undefined>, undefined>;
28
45
  readonly parent: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
29
46
  readonly due_at: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>, undefined>;
30
47
  readonly closed_at: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>, undefined>;
@@ -1,6 +1,6 @@
1
1
  import * as v from "valibot";
2
2
  import { TASK_TYPES } from "../domain/knowledge.js";
3
- import { TASK_ORIGINS, TASK_STATES } from "../tasks/index.js";
3
+ import { PRIORITY_LABELS, TASK_ORIGINS, TASK_STATES } from "../tasks/index.js";
4
4
  import { id, reference, text, time } from "./fields.js";
5
5
  export const closeSuggestion = v.strictObject({
6
6
  as: v.picklist(["done", "dismissed"]),
@@ -27,7 +27,18 @@ export const task = v.strictObject({
27
27
  chat: v.optional(v.nullable(reference)),
28
28
  /** The message it came from. */
29
29
  source_message: v.optional(v.nullable(reference)),
30
+ /** 0 to 1, higher first; `priority_label` reads it as low, medium, high or critical. */
30
31
  priority: v.optional(v.nullable(v.number())),
32
+ priority_label: v.optional(v.nullable(v.picklist(PRIORITY_LABELS))),
33
+ /** The agent working on it, apart from its assignees and its claim. */
34
+ agent: v.optional(v.nullable(text)),
35
+ /** Give it back with `--revision` to refuse a write made on a stale copy. */
36
+ revision: v.optional(v.number()),
37
+ assignments: v.optional(v.array(v.strictObject({ to: reference, role: v.picklist(["assignee", "reviewer", "watcher"]) }))),
38
+ /** Open work whose blocked-by tasks are all done. */
39
+ ready: v.optional(v.boolean()),
40
+ /** The blocked-by tasks not done yet: open (any open state), dismissed, or unavailable. */
41
+ blocked_by: v.optional(v.array(v.strictObject({ task: reference, state: v.picklist(["open", "dismissed", "unavailable"]) }))),
31
42
  parent: v.optional(v.nullable(reference)),
32
43
  due_at: v.optional(v.nullable(time)),
33
44
  closed_at: v.optional(v.nullable(time)),
@@ -120,6 +120,7 @@ const OTHERS = [
120
120
  validation("task.category-unknown", "the category is not in the task category list"),
121
121
  validation("task.claimed", "someone else holds the task"),
122
122
  validation("task.closed", "the task is already closed"),
123
+ ["task.stale", "configuration_error", "the task changed since it was read; read it again"],
123
124
  validation("task.dependency-cycle", "the task would depend on itself"),
124
125
  validation("task.duplicate-candidates", "open tasks may be the same job: attach to one, or add it as new"),
125
126
  validation("task.certainty-required", "an agent closes a task with its certainty, 0 to 1"),
@@ -1,5 +1,5 @@
1
1
  import { formatLocator } from "../domain/locator.js";
2
- import { createTaskService } from "../tasks/index.js";
2
+ import { createTaskService, isClosedState } from "../tasks/index.js";
3
3
  import { questions } from "./questions.js";
4
4
  import { categorize } from "./task-categories.js";
5
5
  export const taskAccount = ({ provider, account }) => `${provider}:${account}`;
@@ -57,7 +57,7 @@ export const applyTaskRules = async (review, { store, account, now, categories,
57
57
  };
58
58
  const close = async (message, kind, answer) => {
59
59
  for (const task of await store.findBySource(owner, sourceOf(message))) {
60
- if (task.kind !== kind || task.state !== "open")
60
+ if (task.kind !== kind || isClosedState(task.state))
61
61
  continue;
62
62
  result.closed += 1;
63
63
  if (dryRun)
@@ -200,7 +200,7 @@ export interface TasksService {
200
200
  export declare const CLAIM_LEASE_MS: number;
201
201
  export declare const PREVIEW = 200;
202
202
  export declare const taskTypeOf: (typed: string) => "mention" | "promise" | "question" | "request";
203
- export declare const taskStateOf: (typed: string) => "dismissed" | "done" | "open";
203
+ export declare const taskStateOf: (typed: string) => "dismissed" | "done" | "in_progress" | "open" | "todo";
204
204
  export declare const closedStateOf: (typed: string) => "dismissed" | "done";
205
205
  export declare const taskLinkKindOf: (typed: string) => "blocked-by" | "created-from" | "discovered-from" | "duplicate-of" | "evidence" | "follow-up-of" | "related-to";
206
206
  export declare const taskTypesOf: (typed: string) => TaskKind[];
@@ -5,7 +5,7 @@ import { formatReference } from "../domain/references.js";
5
5
  import { task as taskEntity } from "../entities/task.js";
6
6
  import { certaintyOf, TASK_RELATION_KINDS, } from "../store/index.js";
7
7
  import { queryFilter } from "../store/store.js";
8
- import { createTaskService, TASK_KINDS, TASK_STATES, } from "../tasks/index.js";
8
+ import { createTaskService, isClosedState, TASK_KINDS, TASK_STATES, } from "../tasks/index.js";
9
9
  import { storedChatId } from "./message-resolution.js";
10
10
  import { categorize, taskCategoryOf } from "./task-categories.js";
11
11
  import { taskAccount } from "./task-rules.js";
@@ -163,7 +163,7 @@ export const tasksService = (deps, categories) => {
163
163
  const planOf = async (store, id, plan, by) => {
164
164
  if (plan.category !== undefined)
165
165
  plan = { ...plan, category: await categoryOf(store, plan.category) };
166
- const open = (await store.tasks.details(id)).status === "open";
166
+ const open = !isClosedState((await store.tasks.details(id)).status);
167
167
  if (!open && (plan.dueAt != null || plan.nextCheckAt != null))
168
168
  throw new CliError("validation_error", "a closed task takes no due date or next check; it stays closed");
169
169
  const { assignees, ...fields } = plan;
@@ -199,7 +199,9 @@ export const tasksService = (deps, categories) => {
199
199
  * A message linked to a task is read by the comparison, so the task may now look like another one: the pairs go to
200
200
  * the review list, never to a merge, and the link stands whatever they score. A closed task is not checked.
201
201
  */
202
- const reviewAfterLink = async (store, id) => (await store.tasks.get(id))?.state === "open" ? store.tasks.reviewDuplicates(id, { linked: true }) : 0;
202
+ const reviewAfterLink = async (store, id) => !isClosedState((await store.tasks.get(id))?.state ?? "done")
203
+ ? store.tasks.reviewDuplicates(id, { linked: true })
204
+ : 0;
203
205
  return {
204
206
  list: ({ query, limit, offset, previews }) => inStore(async (store, account) => {
205
207
  const groups = new Map();
@@ -358,7 +360,7 @@ export const tasksService = (deps, categories) => {
358
360
  },
359
361
  close: (id, { as, reason, by, agent, certainty, suggestOnly }) => inStore(async (store, account) => {
360
362
  const existing = await owned(store, account, id);
361
- const suggested = existing.state === "open" ? await store.tasks.closeSuggestion(id) : null;
363
+ const suggested = !isClosedState(existing.state) ? await store.tasks.closeSuggestion(id) : null;
362
364
  const state = as ?? (by === "owner" ? suggested?.as : undefined);
363
365
  if (state === undefined)
364
366
  throw new CliError("validation_error", "say how the task is closed: --as done, or --as dismissed");
@@ -368,7 +370,7 @@ export const tasksService = (deps, categories) => {
368
370
  const { closeCertainty, closeSuggestion } = await store.tasks.details(task.id);
369
371
  return { ...(await taskView(store, account, task)), closeCertainty, closeSuggestion };
370
372
  };
371
- if (by === "agent" && existing.state === "open") {
373
+ if (by === "agent" && !isClosedState(existing.state)) {
372
374
  if (certainty === undefined)
373
375
  throw new CliError("validation_error", "an agent closes a task with its certainty, 0 to 1: how sure it is the task needs nothing more", { reason: "task.certainty-required" });
374
376
  const { allowed, min } = await store.tasks.agentClose();
@@ -5,6 +5,7 @@ export type { TagRedirectIndex, TagRedirectResolution, TagRedirectSnapshot } fro
5
5
  export { createTagRedirectIndex } from "../domain/tag-redirects.js";
6
6
  export { note } from "../entities/note.js";
7
7
  export type { FindingEvidence, FindingOutcome, MeetingFactFinding, MeetingFinding, MeetingFindingsInput, MeetingFindingsReceipt, MeetingTaskFinding, } from "../meetings/index.js";
8
+ export { CLOSED_STATES, isClosedState, PRIORITY_LABELS, type PriorityLabel, priorityLabel, priorityOf, TASK_STATES, type TaskState, } from "../tasks/index.js";
8
9
  export type { AccountMail, AccountMeetings, AccountStore } from "./account-store.js";
9
10
  export type { DocumentTransferOptions, DocumentTransferPreview, DocumentTransferReceipt, } from "./document-transfer/index.js";
10
11
  export { applyDocumentTransfer, previewDocumentTransfer } from "./document-transfer/index.js";
@@ -54,6 +55,8 @@ export { fillSearchIndex, resetSearchIndex, type SearchIndexFill, type SearchInd
54
55
  export type { SyncStateSave } from "./sqlite/sync.js";
55
56
  export type { TagCreationOptions, TagCreationReview } from "./sqlite/tag-creation.js";
56
57
  export type { TagKind, TagRow } from "./sqlite/tags.js";
58
+ export type { TaskSort } from "./sqlite/task-board.js";
59
+ export { PROJECT_TASK_FIELDS, TASK_SORTS } from "./sqlite/task-board.js";
57
60
  export type { SimilarSubject, SimilarTask, TaskDraft } from "./sqlite/task-candidates.js";
58
61
  export { type AgentCloseSettings, type CloseSuggestion, certaintyOf } from "./sqlite/task-close.js";
59
62
  export type { TaskPair, TaskPairDecision, TaskPairSide } from "./sqlite/task-pairs.js";
@@ -62,6 +65,6 @@ export type { TaskRelation, TaskRelationKind, TaskRelationsStore } from "./sqlit
62
65
  export { TASK_RELATION_KINDS } from "./sqlite/task-relations.js";
63
66
  export type { StoreTaskStore } from "./sqlite/tasks.js";
64
67
  export type { TagPreflight, VocabularyAlias, VocabularyAliasReceipt, VocabularyAudit, VocabularyAuditOptions, VocabularyCatalog, VocabularyEntry, VocabularyKind, VocabularyOptions, VocabularyResolution, VocabularyStore, VocabularyTarget, } from "./sqlite/vocabulary.js";
65
- export type { NewWorkTask, TaskAssignment, TaskDetails, TaskEvent, TaskFields, TaskRole, TaskType, WorkTaskType, } from "./sqlite/work-tasks.js";
68
+ export type { BoardCounts, BoardOptions, NewWorkTask, RevisionCheck, TaskAssignment, TaskDetails, TaskEvent, TaskFields, TaskMove, TaskMoved, TaskRole, TaskType, WorkTaskType, } from "./sqlite/work-tasks.js";
66
69
  export { TASK_EVENT_KINDS, TASK_ROLES } from "./sqlite/work-tasks.js";
67
70
  export { type AccountAccounts, type AccountAttachments, type AccountCounters, type AccountKey, type AccountMentions, type AccountMessageFilter, type AccountMessages, type AccountPeople, type AccountsStore, type AccountVectors, type AttachmentsStore, type AttachmentTextEntry, type AttachmentView, type ChatStats, type ChatsStore, type ConversationsStore, type CountersStore, type DeletionScope, type Delta, type FileAttachment, type IdentityRef, type IngestedVia, type LinkedIdentity, type LinkOptions, MENTION_QUERY, MENTION_STATUSES, type MentionsStore, type MessageFilter, type MessageStore, type MessagesStore, OTHER_SOURCES, openStore, type PeopleStore, type PersonFacts, type PersonRecord, type QueriesStore, type Range, type RankedEvidence, type RankedStoreFound, type RankedStoreRow, type RankingEvidenceItem, type RankingEvidenceRequest, type RankingRequest, type RosterStore, type SearchCommand, type SearchesStore, type SearchIndexStore, type SearchRecord, type StoredAccount, type StoredChatFilter, type StoredHit, type StoredSearch, type StoredTag, type StoreOptions, type SyncStore, type TagFilter, type TagsStore, type TagTarget, type TaskTagTarget, type TextOrigin, type VectorsStore, } from "./store.js";
@@ -1,6 +1,7 @@
1
1
  export { FACT_STATUSES, FACT_TYPES, MEMORY_KINDS, MEMORY_SCOPES, MEMORY_STATUSES, TASK_TYPES, WORK_TASK_TYPES, } from "../domain/knowledge.js";
2
2
  export { createTagRedirectIndex } from "../domain/tag-redirects.js";
3
3
  export { note } from "../entities/note.js";
4
+ export { CLOSED_STATES, isClosedState, PRIORITY_LABELS, priorityLabel, priorityOf, TASK_STATES, } from "../tasks/index.js";
4
5
  export { applyDocumentTransfer, previewDocumentTransfer } from "./document-transfer/index.js";
5
6
  export { PRAGMAS } from "./driver.js";
6
7
  export { EVIDENCE_FINDINGS_KIND, validateEvidenceFindings, } from "./evidence-findings.js";
@@ -22,6 +23,7 @@ export { ORGANIZATION_KINDS, PROJECT_TYPES, SCOPES } from "./sqlite/knowledge.js
22
23
  export { NOTE_FIELDS, } from "./sqlite/note-search.js";
23
24
  export { PROPOSAL_STATUSES } from "./sqlite/proposed-actions.js";
24
25
  export { fillSearchIndex, resetSearchIndex, searchIndexState, } from "./sqlite/search-index.js";
26
+ export { PROJECT_TASK_FIELDS, TASK_SORTS } from "./sqlite/task-board.js";
25
27
  export { certaintyOf } from "./sqlite/task-close.js";
26
28
  export { TASK_PAIR_DECISIONS } from "./sqlite/task-pairs.js";
27
29
  export { TASK_RELATION_KINDS } from "./sqlite/task-relations.js";
@@ -5,6 +5,7 @@ import { formatReference, parseReference } from "../../domain/references.js";
5
5
  import { normalizeTag } from "../../domain/tags.js";
6
6
  import { normalizeVocabularyName } from "../../domain/vocabulary.js";
7
7
  import { formatMeetingReference } from "../../meetings/index.js";
8
+ import { isClosedState } from "../../tasks/index.js";
8
9
  import { fold } from "../normalize.js";
9
10
  import { ulid } from "../ulid.js";
10
11
  import { ownerPerson } from "./actors.js";
@@ -267,7 +268,7 @@ export const knowledgeStoreOver = (context) => {
267
268
  const row = taskRowOf(database, id);
268
269
  const open = row !== undefined &&
269
270
  taskAccountOf(database, row) === taskAccount(key) &&
270
- database.prepare("SELECT status FROM tasks WHERE id = ?").get(row)?.status === "open";
271
+ !isClosedState(String(database.prepare("SELECT status FROM tasks WHERE id = ?").get(row)?.status ?? "done"));
271
272
  if (!open)
272
273
  throw new CliError("validation_error", "a reminder requires an open task in this account");
273
274
  return row;
@@ -622,7 +623,7 @@ export const knowledgeStoreOver = (context) => {
622
623
  throw new CliError("validation_error", "lease takes 1000–3600000 milliseconds");
623
624
  const accountId = account(key), at = now();
624
625
  const rows = database
625
- .prepare("SELECT r.id FROM reminders r JOIN tasks t ON t.id = r.task_id WHERE r.account_id=? AND r.due_at<=? AND t.status = 'open' " +
626
+ .prepare("SELECT r.id FROM reminders r JOIN tasks t ON t.id = r.task_id WHERE r.account_id=? AND r.due_at<=? AND t.status NOT IN ('done','dismissed') " +
626
627
  "AND (r.state='pending' OR (r.state='leased' AND r.lease_until<=?)) ORDER BY r.due_at, r.id LIMIT ?")
627
628
  .all(accountId, at, at, limit);
628
629
  const claimed = [];
@@ -54,6 +54,7 @@ export const MANIFEST = [
54
54
  { name: "20261029224107_version-38-folded-triggers", version: 38, minCompatible: 34 },
55
55
  { name: "20261030114008_version-39-email-locations", version: 39, minCompatible: 34 },
56
56
  { name: "20261030225520_version-40-note-trigrams", version: 40, minCompatible: 34 },
57
+ { name: "20261031230945_version-41-task-fields", version: 41, minCompatible: 34 },
57
58
  ];
58
59
  export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
59
60
  const migrations = [];
@@ -847,5 +847,17 @@ export const GENERATED = [
847
847
  "CREATE TRIGGER document_trigrams_bd BEFORE DELETE ON documents BEGIN\n DELETE FROM document_trigrams WHERE rowid = old.id;\nEND;",
848
848
  "-- The trigrams hold JS-normalized text, which SQL cannot produce: fillTrigrams fills the rows stored before this\n-- version, up to `watermark`, and a search reads the rest of them by their text meanwhile.\nINSERT INTO search_index_state (name, watermark, filled_through, terms_through, normalizer_version)\n SELECT 'note_trigrams', coalesce(max(id), 0), 0, 0, 1 FROM notes\n UNION ALL SELECT 'document_trigrams', coalesce(max(id), 0), 0, 0, 1 FROM documents;"
849
849
  ]
850
+ },
851
+ {
852
+ "name": "20261031230945_version-41-task-fields",
853
+ "statements": [
854
+ "ALTER TABLE `tasks` ADD `priority_score` real;",
855
+ "ALTER TABLE `tasks` ADD `agent` text;",
856
+ "ALTER TABLE `tasks` ADD `revision` integer DEFAULT 1 NOT NULL;",
857
+ "-- 0 urgent … 4 low land inside the critical, high, medium and low quarters, with room on either side for moves.\nUPDATE tasks SET priority_score = CASE priority WHEN 0 THEN 0.875 WHEN 1 THEN 0.625 WHEN 2 THEN 0.375 WHEN 3 THEN 0.1875 WHEN 4 THEN 0.0625 END WHERE priority IS NOT NULL;",
858
+ "-- A build before version 41 writes only `priority`; these keep the score in step with what it wrote.\nCREATE TRIGGER tasks_priority_score_ai AFTER INSERT ON tasks WHEN new.priority IS NOT NULL AND new.priority_score IS NULL BEGIN\n UPDATE tasks SET priority_score = CASE new.priority WHEN 0 THEN 0.875 WHEN 1 THEN 0.625 WHEN 2 THEN 0.375 WHEN 3 THEN 0.1875 WHEN 4 THEN 0.0625 END WHERE id = new.id;\nEND;",
859
+ "CREATE TRIGGER tasks_priority_score_au AFTER UPDATE OF priority ON tasks WHEN new.priority IS NOT old.priority AND new.priority_score IS old.priority_score BEGIN\n UPDATE tasks SET priority_score = CASE new.priority WHEN 0 THEN 0.875 WHEN 1 THEN 0.625 WHEN 2 THEN 0.375 WHEN 3 THEN 0.1875 WHEN 4 THEN 0.0625 END WHERE id = new.id;\nEND;",
860
+ "-- Every build's write bumps it, older ones too, so a stale copy is caught whoever wrote last.\nCREATE TRIGGER tasks_revision_au AFTER UPDATE ON tasks WHEN new.revision IS old.revision BEGIN\n UPDATE tasks SET revision = old.revision + 1 WHERE id = new.id;\nEND;"
861
+ ]
850
862
  }
851
863
  ];
@@ -52,7 +52,7 @@ export const personPreparation = ({ database }, uid, { accounts, limit }) => {
52
52
  const tasks = database
53
53
  .prepare(`SELECT t.id,t.title,t.due_at,t.project_id,t.source_locator,t.created_at,group_concat(DISTINCT ta.role) AS role
54
54
  FROM task_assignments ta JOIN tasks t ON t.id=ta.task_id JOIN main.projects p ON p.id=t.project_id
55
- WHERE ta.assignee_type='person' AND ta.assignee_id=? AND t.deleted_at IS NULL AND t.status='open'
55
+ WHERE ta.assignee_type='person' AND ta.assignee_id=? AND t.deleted_at IS NULL AND t.status NOT IN ('done','dismissed')
56
56
  AND p.deleted_at IS NULL AND (p.account_id IS NULL OR p.account_id IN (SELECT a.id FROM accounts a
57
57
  JOIN json_each(?) k ON a.provider=json_extract(k.value,'$.provider')
58
58
  AND a.external_id=json_extract(k.value,'$.account')))
@@ -12683,6 +12683,54 @@ export declare const tasks: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
12683
12683
  identity: undefined;
12684
12684
  generated: undefined;
12685
12685
  }, {}>;
12686
+ priorityScore: import("drizzle-orm/sqlite-core").SQLiteColumn<{
12687
+ name: string;
12688
+ tableName: "tasks";
12689
+ dataType: "number double";
12690
+ data: number;
12691
+ driverParam: number;
12692
+ notNull: false;
12693
+ hasDefault: false;
12694
+ isPrimaryKey: false;
12695
+ isAutoincrement: false;
12696
+ hasRuntimeDefault: false;
12697
+ enumValues: undefined;
12698
+ baseColumn: never;
12699
+ identity: undefined;
12700
+ generated: undefined;
12701
+ }, {}>;
12702
+ agent: import("drizzle-orm/sqlite-core").SQLiteColumn<{
12703
+ name: string;
12704
+ tableName: "tasks";
12705
+ dataType: "string";
12706
+ data: string;
12707
+ driverParam: string;
12708
+ notNull: false;
12709
+ hasDefault: false;
12710
+ isPrimaryKey: false;
12711
+ isAutoincrement: false;
12712
+ hasRuntimeDefault: false;
12713
+ enumValues: [string, ...string[]];
12714
+ baseColumn: never;
12715
+ identity: undefined;
12716
+ generated: undefined;
12717
+ }, {}>;
12718
+ revision: import("drizzle-orm/sqlite-core").SQLiteColumn<{
12719
+ name: string;
12720
+ tableName: "tasks";
12721
+ dataType: "number int53";
12722
+ data: number;
12723
+ driverParam: number;
12724
+ notNull: true;
12725
+ hasDefault: true;
12726
+ isPrimaryKey: false;
12727
+ isAutoincrement: false;
12728
+ hasRuntimeDefault: false;
12729
+ enumValues: undefined;
12730
+ baseColumn: never;
12731
+ identity: undefined;
12732
+ generated: undefined;
12733
+ }, {}>;
12686
12734
  };
12687
12735
  dialect: 'sqlite';
12688
12736
  isAlias: false;
@@ -1266,6 +1266,9 @@ export const tasks = sqliteTable("tasks", {
1266
1266
  closeSuggestedCertainty: real("close_suggested_certainty"),
1267
1267
  closeSuggestedReason: text("close_suggested_reason"),
1268
1268
  closeSuggestedAt: integer("close_suggested_at"),
1269
+ priorityScore: real("priority_score"),
1270
+ agent: text("agent"),
1271
+ revision: integer("revision").notNull().default(1),
1269
1272
  }, (table) => [
1270
1273
  unique().on(table.projectId, table.number),
1271
1274
  index("tasks_by_title_folded").on(table.titleFolded),
@@ -73,10 +73,9 @@ export declare const canonicalTagNamed: (database: CacheDatabase, name: string)
73
73
  export declare const createTag: (context: StoreContext, name: string, kind: TagKind, by: "owner" | "agent", options?: TagCreationOptions) => TagRow;
74
74
  /** The tag of that name, made as a plain tag when no tag or topic holds the name yet. */
75
75
  export declare const ensureTag: (context: StoreContext, name: string) => TagRow;
76
- /** The tags it did not have before, in the order given; a tag the owner adds becomes the owner's. */
77
76
  export declare const addTags: (context: StoreContext, thing: Thing, tags: string[], source?: TaggingSource, author?: Actor) => string[];
78
77
  /** An explicit removal withdraws the effective label, restricted to the requested origin when supplied. */
79
- export declare const removeTags: ({ database }: StoreContext, thing: Thing, tags: string[], source?: "manual" | "auto") => string[];
78
+ export declare const removeTags: (context: StoreContext, thing: Thing, tags: string[], source?: "manual" | "auto") => string[];
80
79
  export interface Label {
81
80
  tag: string;
82
81
  /** `file` when only a note's file states it; the owner's own label otherwise. */
@@ -111,13 +111,23 @@ export const ensureTag = (context, name) => {
111
111
  return canonicalTagNamed(context.database, name) ?? createTag(context, name, "tag", "owner");
112
112
  };
113
113
  /** The tags it did not have before, in the order given; a tag the owner adds becomes the owner's. */
114
+ /** A task's tag change is part of its history, and of its revision. */
115
+ const taskTagsChanged = (context, thing, changes, author) => {
116
+ if (thing.type !== "task" || [...(changes.added ?? []), ...(changes.removed ?? [])].length === 0)
117
+ return;
118
+ const at = context.now();
119
+ context.database.prepare("UPDATE tasks SET updated_at = ? WHERE id = ?").run(at, thing.id);
120
+ context.database
121
+ .prepare("INSERT INTO task_events (task_id, actor_type, actor_id, kind, changes, created_at) VALUES (?, ?, ?, 'tags', ?, ?)")
122
+ .run(thing.id, author?.type ?? null, author?.id ?? null, JSON.stringify({ tags: [changes.removed ?? [], changes.added ?? []] }), at);
123
+ };
114
124
  export const addTags = (context, thing, tags, source = "owner", author) => {
115
125
  const { database, now } = context;
116
126
  const insert = database.prepare("INSERT INTO taggings (tag_id, taggable_type, taggable_id, main, source, author_type, author_id, created_at, updated_at) " +
117
127
  "VALUES (?, ?, ?, 0, ?, ?, ?, ?, ?) ON CONFLICT (tag_id, taggable_type, taggable_id) DO NOTHING");
118
128
  const claim = database.prepare("UPDATE taggings SET source = 'owner', updated_at = ? WHERE tag_id = ? AND taggable_type = ? AND taggable_id = ? AND source <> 'owner'");
119
129
  const at = now();
120
- return tags.filter((name) => {
130
+ const fresh = tags.filter((name) => {
121
131
  const tag = ensureTag(context, name);
122
132
  const held = database
123
133
  .prepare("SELECT source,author_type,author_id FROM taggings WHERE tag_id IN (SELECT ? UNION ALL SELECT source_id FROM tag_redirects WHERE survivor_id=?) AND taggable_type=? AND taggable_id=?")
@@ -129,11 +139,14 @@ export const addTags = (context, thing, tags, source = "owner", author) => {
129
139
  claim.run(at, tag.id, thing.type, thing.id);
130
140
  return added && held.length === 0;
131
141
  });
142
+ taskTagsChanged(context, thing, { added: fresh }, author);
143
+ return fresh;
132
144
  };
133
145
  /** An explicit removal withdraws the effective label, restricted to the requested origin when supplied. */
134
- export const removeTags = ({ database }, thing, tags, source) => {
146
+ export const removeTags = (context, thing, tags, source) => {
147
+ const { database } = context;
135
148
  const remove = database.prepare("DELETE FROM taggings WHERE tag_id=? AND taggable_type=? AND taggable_id=?");
136
- return tags.filter((name) => {
149
+ const removed = tags.filter((name) => {
137
150
  const tag = canonicalTagNamed(database, name);
138
151
  if (!tag)
139
152
  return false;
@@ -171,6 +184,8 @@ export const removeTags = ({ database }, thing, tags, source) => {
171
184
  }
172
185
  return changed;
173
186
  });
187
+ taskTagsChanged(context, thing, { removed });
188
+ return removed;
174
189
  };
175
190
  /** A thing's tags by name, with whether only a file states each. */
176
191
  /** A tagging of something still there: a deleted document keeps its tags but is not counted or listed by them. */
@@ -0,0 +1,16 @@
1
+ import { type FieldSpec, type QueryAst } from "@wirecat/cli-base/query";
2
+ import type { ColumnMap } from "./query-filter.js";
3
+ export declare const TASK_SORTS: readonly ["priority", "due", "created", "updated", "key", "state"];
4
+ export type TaskSort = (typeof TASK_SORTS)[number];
5
+ /** The query language of the owner's project tasks, every project at once; `text` takes the bare words. */
6
+ export declare const PROJECT_TASK_FIELDS: readonly FieldSpec[];
7
+ /** A `blocked-by` task that is not done holds the task back; so does one deleted or out of reach. */
8
+ export declare const UNFINISHED_BLOCKER: string;
9
+ /** `now` is when overdue is measured; one value per answer, so the list and its counts agree. */
10
+ export declare const projectTaskColumns: (now: number) => ColumnMap;
11
+ /** Live tasks of the owner's projects; an account's inbox is listed by its account, never here. */
12
+ export declare const BOARD_SCOPE = "t.deleted_at IS NULL AND t.project_id IN (SELECT id FROM projects WHERE deleted_at IS NULL AND length(key) <> 10)";
13
+ /** The chosen order first, then priority, age and id, so a page boundary never splits a tie. */
14
+ export declare const boardOrder: (sort: TaskSort, descending: boolean) => string;
15
+ /** The query without its required terms on `name`: a picker counts the choices its own filter hides. */
16
+ export declare const withoutField: (query: QueryAst | undefined, name: string) => QueryAst | undefined;