@wirecat/cli-core 0.511.0 → 0.512.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/CHANGELOG.md +34 -0
- package/dist/calendar/mcp/index.d.ts +46 -10
- package/dist/calendar/mcp/index.js +60 -25
- package/dist/cli/list-query.d.ts +4 -2
- package/dist/cli/list-query.js +3 -3
- package/dist/entities/index.d.ts +8 -8
- package/dist/entities/task-results.d.ts +7 -7
- package/dist/entities/task.d.ts +1 -1
- package/dist/services/task-rules.js +2 -2
- package/dist/services/tasks.d.ts +1 -1
- package/dist/services/tasks.js +7 -5
- package/dist/store/index.d.ts +4 -1
- package/dist/store/index.js +2 -0
- package/dist/store/sqlite/knowledge.js +3 -2
- package/dist/store/sqlite/person-preparation.js +1 -1
- package/dist/store/sqlite/task-board.d.ts +16 -0
- package/dist/store/sqlite/task-board.js +133 -0
- package/dist/store/sqlite/task-candidates.js +1 -1
- package/dist/store/sqlite/task-category-settings.js +1 -1
- package/dist/store/sqlite/task-close-store.js +2 -1
- package/dist/store/sqlite/task-pairs.js +4 -3
- package/dist/store/sqlite/task-relations.js +3 -2
- package/dist/store/sqlite/tasks.js +5 -4
- package/dist/store/sqlite/work-tasks.d.ts +52 -2
- package/dist/store/sqlite/work-tasks.js +103 -10
- package/dist/tasks/index.d.ts +1 -1
- package/dist/tasks/index.js +1 -1
- package/dist/tasks/model.d.ts +5 -2
- package/dist/tasks/model.js +5 -2
- package/dist/tasks/service.js +2 -2
- package/package.json +1 -1
package/dist/services/tasks.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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();
|
package/dist/store/index.d.ts
CHANGED
|
@@ -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, TASK_STATES } 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, TaskAssignment, TaskDetails, TaskEvent, TaskFields, 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";
|
package/dist/store/index.js
CHANGED
|
@@ -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, 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
|
|
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
|
|
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 = [];
|
|
@@ -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
|
|
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')))
|
|
@@ -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;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { OPERATORS_BY_TYPE, walkQuery, } from "@wirecat/cli-base/query";
|
|
3
|
+
import { TASK_TYPES } from "../../domain/knowledge.js";
|
|
4
|
+
import { taskWordsOf } from "../../search/task-similarity.js";
|
|
5
|
+
import { TASK_STATES } from "../../tasks/index.js";
|
|
6
|
+
import { INBOX_KEY_LENGTH } from "./task-keys.js";
|
|
7
|
+
export const TASK_SORTS = ["priority", "due", "created", "updated", "key", "state"];
|
|
8
|
+
const field = (name, type, example, more = {}) => ({
|
|
9
|
+
name,
|
|
10
|
+
type,
|
|
11
|
+
operators: OPERATORS_BY_TYPE[type],
|
|
12
|
+
example,
|
|
13
|
+
...more,
|
|
14
|
+
});
|
|
15
|
+
/** The query language of the owner's project tasks, every project at once; `text` takes the bare words. */
|
|
16
|
+
export const PROJECT_TASK_FIELDS = [
|
|
17
|
+
field("state", "enum", "state:todo", { values: TASK_STATES }),
|
|
18
|
+
field("kind", "enum", "kind:bug", { values: TASK_TYPES }),
|
|
19
|
+
field("project", "kw", "project:HRB", {
|
|
20
|
+
relation: "a project by its key or project:<id>; project:* is every project, and lists project tasks only",
|
|
21
|
+
}),
|
|
22
|
+
field("assignee", "kw", 'assignee:"person:12"', {
|
|
23
|
+
relation: "who does it: any, none, or a person (person:12 in quotes, or the bare 12)",
|
|
24
|
+
}),
|
|
25
|
+
field("priority", "num", "priority:[0 TO 1]", { relation: "0 urgent … 4 low; NOT priority:* has none" }),
|
|
26
|
+
field("due", "time", "due:[today TO 2026-12-31]", { relation: "the due date; NOT due:* has none" }),
|
|
27
|
+
field("overdue", "bool", "overdue:true", { relation: "open work whose due date has passed" }),
|
|
28
|
+
field("closed", "time", "closed:>=14d", { relation: "when it was done or dismissed" }),
|
|
29
|
+
field("ready", "bool", "ready:true", { relation: "open work whose blocked-by tasks are all done" }),
|
|
30
|
+
field("text", "txt", "text:invoice", { relation: "words of the title and description; a bare word means this" }),
|
|
31
|
+
];
|
|
32
|
+
const OPEN = "t.status NOT IN ('done', 'dismissed')";
|
|
33
|
+
const OWNER_PROJECTS = `SELECT id FROM projects WHERE deleted_at IS NULL AND length(key) <> ${INBOX_KEY_LENGTH}`;
|
|
34
|
+
const ASSIGNED = "SELECT 1 FROM task_assignments a WHERE a.task_id = t.id AND a.assignee_type = 'person' AND a.role = 'assignee'";
|
|
35
|
+
/** A `blocked-by` task that is not done holds the task back; so does one deleted or out of reach. */
|
|
36
|
+
export const UNFINISHED_BLOCKER = "SELECT 1 FROM links l LEFT JOIN tasks b ON b.id = l.to_id LEFT JOIN projects bp ON bp.id = b.project_id " +
|
|
37
|
+
"WHERE l.from_type = 'task' AND l.to_type = 'task' AND l.kind = 'blocked-by' AND l.from_id = t.id AND l.confirmed = 1 " +
|
|
38
|
+
"AND l.anchor IS NULL AND (b.id IS NULL OR b.deleted_at IS NOT NULL OR bp.deleted_at IS NOT NULL OR b.status <> 'done')";
|
|
39
|
+
/** Every word a prefix, all of them required; a quoted phrase as one. `undefined` when nothing in it is a word. */
|
|
40
|
+
const wordsMatch = (node) => {
|
|
41
|
+
const words = taskWordsOf(node.value.replaceAll("*", " "))
|
|
42
|
+
.split(/\s+/)
|
|
43
|
+
.filter((word) => /[\p{L}\p{N}]/u.test(word));
|
|
44
|
+
if (words.length === 0)
|
|
45
|
+
return undefined;
|
|
46
|
+
const quoted = (text) => `"${text.replaceAll('"', '""')}"`;
|
|
47
|
+
return `normalized_text : (${node.operator === "phrase" ? quoted(words.join(" ")) : words.map((word) => `${quoted(word)}*`).join(" AND ")})`;
|
|
48
|
+
};
|
|
49
|
+
const projectWhere = (node) => {
|
|
50
|
+
if (node.operator === "wildcard" && node.value === "*")
|
|
51
|
+
return { sql: "1", params: [] };
|
|
52
|
+
const id = /^project:(\d+)$/.exec(node.value.trim())?.[1];
|
|
53
|
+
if (id !== undefined)
|
|
54
|
+
return { sql: "t.project_id = ?", params: [Number(id)] };
|
|
55
|
+
const key = node.value.trim().toUpperCase();
|
|
56
|
+
return {
|
|
57
|
+
sql: `t.project_id IN (SELECT id FROM projects WHERE key ${node.operator === "wildcard" ? "GLOB" : "="} ?)`,
|
|
58
|
+
params: [key],
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const assigneeWhere = (node) => {
|
|
62
|
+
const value = node.value.trim().toLowerCase();
|
|
63
|
+
if (value === "any" || (node.operator === "wildcard" && value === "*"))
|
|
64
|
+
return { sql: `EXISTS (${ASSIGNED})`, params: [] };
|
|
65
|
+
if (value === "none")
|
|
66
|
+
return { sql: `NOT EXISTS (${ASSIGNED})`, params: [] };
|
|
67
|
+
const id = /^(?:person:)?(\d+)$/.exec(value)?.[1];
|
|
68
|
+
if (id === undefined)
|
|
69
|
+
throw new CliError("validation_error", `assignee: takes any, none or "person:<id>", not "${node.value}"`);
|
|
70
|
+
return { sql: `EXISTS (${ASSIGNED} AND a.assignee_id = ?)`, params: [Number(id)] };
|
|
71
|
+
};
|
|
72
|
+
const truth = (node, sql) => ({
|
|
73
|
+
sql: node.value.toLowerCase() === "true" ? sql : `NOT (${sql})`,
|
|
74
|
+
params: [],
|
|
75
|
+
});
|
|
76
|
+
/** `now` is when overdue is measured; one value per answer, so the list and its counts agree. */
|
|
77
|
+
export const projectTaskColumns = (now) => ({
|
|
78
|
+
state: { type: "enum", sql: "t.status" },
|
|
79
|
+
kind: { type: "enum", sql: "t.type" },
|
|
80
|
+
project: { where: projectWhere },
|
|
81
|
+
assignee: { where: assigneeWhere },
|
|
82
|
+
priority: { type: "num", sql: "t.priority" },
|
|
83
|
+
due: { type: "time", sql: "t.due_at" },
|
|
84
|
+
closed: { type: "time", sql: "t.closed_at" },
|
|
85
|
+
overdue: { where: (node) => truth(node, `${OPEN} AND t.due_at IS NOT NULL AND t.due_at < ${Number(now)}`) },
|
|
86
|
+
ready: { where: (node) => truth(node, `${OPEN} AND NOT EXISTS (${UNFINISHED_BLOCKER})`) },
|
|
87
|
+
text: {
|
|
88
|
+
where: (node) => {
|
|
89
|
+
const match = wordsMatch(node);
|
|
90
|
+
const byKey = { sql: "t.key = ?", params: [node.value.trim().toUpperCase()] };
|
|
91
|
+
if (match === undefined)
|
|
92
|
+
return byKey;
|
|
93
|
+
return {
|
|
94
|
+
sql: `(t.id IN (SELECT rowid FROM task_words WHERE task_words MATCH ?) OR ${byKey.sql})`,
|
|
95
|
+
params: [match, ...byKey.params],
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
/** Live tasks of the owner's projects; an account's inbox is listed by its account, never here. */
|
|
101
|
+
export const BOARD_SCOPE = `t.deleted_at IS NULL AND t.project_id IN (${OWNER_PROJECTS})`;
|
|
102
|
+
const STATE_ORDER = `CASE t.status ${TASK_STATES.map((state, at) => `WHEN '${state}' THEN ${at}`).join(" ")} END`;
|
|
103
|
+
/** The chosen order first, then priority, age and id, so a page boundary never splits a tie. */
|
|
104
|
+
export const boardOrder = (sort, descending) => {
|
|
105
|
+
const direction = descending ? "DESC" : "ASC";
|
|
106
|
+
const first = {
|
|
107
|
+
priority: [`coalesce(t.priority, 5) ${direction}`],
|
|
108
|
+
due: ["t.due_at IS NULL", `t.due_at ${direction}`],
|
|
109
|
+
created: [`t.created_at ${direction}`],
|
|
110
|
+
updated: [`t.updated_at ${direction}`],
|
|
111
|
+
key: [`(SELECT key FROM projects WHERE id = t.project_id) ${direction}`, `t.number ${direction}`],
|
|
112
|
+
state: [`${STATE_ORDER} ${direction}`],
|
|
113
|
+
}[sort];
|
|
114
|
+
return [...first, "coalesce(t.priority, 5)", "t.created_at", "t.id"].join(", ");
|
|
115
|
+
};
|
|
116
|
+
/** The query without its required terms on `name`: a picker counts the choices its own filter hides. */
|
|
117
|
+
export const withoutField = (query, name) => {
|
|
118
|
+
if (query === undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
const strip = (node) => {
|
|
121
|
+
if (walkQuery(node).every((leaf) => leaf.field === name))
|
|
122
|
+
return undefined;
|
|
123
|
+
if (node.kind === "predicate" || node.clauses.some((clause) => clause.occur === "should"))
|
|
124
|
+
return node;
|
|
125
|
+
const clauses = node.clauses.flatMap((clause) => {
|
|
126
|
+
const kept = strip(clause.node);
|
|
127
|
+
return kept === undefined ? [] : [{ ...clause, node: kept }];
|
|
128
|
+
});
|
|
129
|
+
return clauses.length === 0 ? undefined : { ...node, clauses };
|
|
130
|
+
};
|
|
131
|
+
const root = strip(query.root);
|
|
132
|
+
return root === undefined ? undefined : { ...query, root };
|
|
133
|
+
};
|
|
@@ -173,7 +173,7 @@ export const taskSimilarityOver = (context) => {
|
|
|
173
173
|
drainCorpus(database, CORPORA.task, stemmerFor);
|
|
174
174
|
const stemmer = stemmerFor(storeStemmers(database) ?? DEFAULT_STEMMERS);
|
|
175
175
|
const since = now() - RECENTLY_CLOSED_MS;
|
|
176
|
-
const live = "t.project_id = ? AND t.deleted_at IS NULL AND t.id <> ? AND (t.status
|
|
176
|
+
const live = "t.project_id = ? AND t.deleted_at IS NULL AND t.id <> ? AND (t.status NOT IN ('done', 'dismissed') OR t.closed_at >= ?)";
|
|
177
177
|
const scope = [side.project, side.row ?? -1, since];
|
|
178
178
|
const found = new Map();
|
|
179
179
|
const add = (id, by) => found.set(id, (found.get(id) ?? new Set()).add(by));
|
|
@@ -11,7 +11,7 @@ export const storeTaskCategories = (database) => savedTaskCategories(database) ?
|
|
|
11
11
|
export const unlistedTaskCategories = (database, list) => {
|
|
12
12
|
const listed = new Set(list.map(({ name }) => name));
|
|
13
13
|
return database
|
|
14
|
-
.prepare("SELECT category, COUNT(*) AS total, SUM(status
|
|
14
|
+
.prepare("SELECT category, COUNT(*) AS total, SUM(status NOT IN ('done', 'dismissed')) AS open FROM tasks " +
|
|
15
15
|
"WHERE category IS NOT NULL AND deleted_at IS NULL GROUP BY category ORDER BY category")
|
|
16
16
|
.all()
|
|
17
17
|
.map((row) => ({ name: String(row.category), open: Number(row.open), total: Number(row.total) }))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { isClosedState } from "../../tasks/index.js";
|
|
2
3
|
import { authorActor } from "./memories.js";
|
|
3
4
|
import { CLEAR_SUGGESTION, certaintyOf, closeSuggestionOf, saveAgentClose, storeAgentClose, } from "./task-close.js";
|
|
4
5
|
import { DEFAULT_FIRST_PASS_PER_CHAT, savedFirstPassPerChat, saveFirstPassPerChat } from "./task-first-pass.js";
|
|
@@ -24,7 +25,7 @@ export const taskCloseOver = (context) => {
|
|
|
24
25
|
closeSuggestion: async (id) => closeSuggestionOf(database, liveRow(id)),
|
|
25
26
|
suggestClose: async (id, suggestion, by) => write(database, () => {
|
|
26
27
|
const row = liveRow(id);
|
|
27
|
-
if (row.status
|
|
28
|
+
if (isClosedState(String(row.status)))
|
|
28
29
|
throw new CliError("validation_error", `task ${String(row.key)} is already ${String(row.status)}`, {
|
|
29
30
|
reason: "task.closed",
|
|
30
31
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { isClosedState } from "../../tasks/index.js";
|
|
2
3
|
import { folded } from "./folded.js";
|
|
3
4
|
import { linkKind } from "./link-kinds.js";
|
|
4
5
|
import { authorActor } from "./memories.js";
|
|
@@ -134,7 +135,7 @@ export const taskPairsOver = (context, similarity) => {
|
|
|
134
135
|
};
|
|
135
136
|
const followUp = async (row, candidates, by) => write(database, () => {
|
|
136
137
|
for (const one of candidates) {
|
|
137
|
-
if (one.status
|
|
138
|
+
if (!isClosedState(one.status) || one.band === "different")
|
|
138
139
|
continue;
|
|
139
140
|
const closed = required(one.key);
|
|
140
141
|
if (mergedInto(closed))
|
|
@@ -157,7 +158,7 @@ export const taskPairsOver = (context, similarity) => {
|
|
|
157
158
|
const alike = (await similarity.similar({ task: String(row.key) }, { limit: 40 })).filter((one) => one.band !== "different");
|
|
158
159
|
if (!linked)
|
|
159
160
|
await followUp(row, alike, { bot: "rule" });
|
|
160
|
-
const found = linked ? alike : alike.filter((one) => one.status
|
|
161
|
+
const found = linked ? alike : alike.filter((one) => !isClosedState(one.status));
|
|
161
162
|
if (found.length === 0)
|
|
162
163
|
return 0;
|
|
163
164
|
return write(database, () => {
|
|
@@ -216,7 +217,7 @@ export const taskPairsOver = (context, similarity) => {
|
|
|
216
217
|
const a = required(dup);
|
|
217
218
|
const b = required(into);
|
|
218
219
|
sameInbox(a, b);
|
|
219
|
-
if (a.status
|
|
220
|
+
if (isClosedState(String(a.status)))
|
|
220
221
|
throw new CliError("validation_error", `task ${String(a.key)} is already ${String(a.status)}; a merge closes an open task — reopen it with tasks unmerge if a merge closed it`, { reason: "task.closed" });
|
|
221
222
|
const onward = mergedInto(b);
|
|
222
223
|
if (onward)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { isClosedState } from "../../tasks/index.js";
|
|
2
3
|
import { authorActor } from "./memories.js";
|
|
3
4
|
import { taskIdOf, taskRowOf } from "./things.js";
|
|
4
5
|
import { read, write } from "./transaction.js";
|
|
@@ -138,7 +139,7 @@ export const taskRelationsStoreOver = (context, record) => {
|
|
|
138
139
|
? "dismissed"
|
|
139
140
|
: "open",
|
|
140
141
|
}));
|
|
141
|
-
return { ready: liveTask(database, rowId).status
|
|
142
|
+
return { ready: !isClosedState(String(liveTask(database, rowId).status)) && blockers.length === 0, blockers };
|
|
142
143
|
}),
|
|
143
144
|
readyTasks: async (project, options = {}) => read(database, () => {
|
|
144
145
|
const limit = options.limit ?? 50;
|
|
@@ -154,7 +155,7 @@ export const taskRelationsStoreOver = (context, record) => {
|
|
|
154
155
|
if (!row)
|
|
155
156
|
throw new CliError("not_found", "the owner project is unavailable");
|
|
156
157
|
const rows = database
|
|
157
|
-
.prepare("SELECT coalesce(t.package_id,t.key) AS id,t.key,substr(t.title,1,300) AS title FROM tasks t WHERE t.project_id=? AND t.status
|
|
158
|
+
.prepare("SELECT coalesce(t.package_id,t.key) AS id,t.key,substr(t.title,1,300) AS title FROM tasks t WHERE t.project_id=? AND t.status NOT IN ('done','dismissed') AND t.deleted_at IS NULL AND NOT EXISTS (SELECT 1 FROM links l LEFT JOIN tasks b ON b.id=l.to_id LEFT JOIN projects p ON p.id=b.project_id WHERE l.from_type='task' AND l.to_type='task' AND l.kind='blocked-by' AND l.from_id=t.id AND l.confirmed=1 AND l.anchor IS NULL AND (b.id IS NULL OR b.deleted_at IS NOT NULL OR p.deleted_at IS NOT NULL OR b.status!='done')) ORDER BY coalesce(t.priority,5),t.number,t.id LIMIT ?")
|
|
158
159
|
.all(Number(row.id), limit + 1);
|
|
159
160
|
return {
|
|
160
161
|
items: rows
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
-
import { matches, TASK_KINDS, TASK_ORIGINS, TASK_STATES, TASK_VERDICTS, } from "../../tasks/index.js";
|
|
2
|
+
import { isClosedState, matches, TASK_KINDS, TASK_ORIGINS, TASK_STATES, TASK_VERDICTS, } from "../../tasks/index.js";
|
|
3
3
|
import { taskCategoriesReader } from "../shared-config.js";
|
|
4
4
|
import { actorOfOrigin, originOfActor } from "./actors.js";
|
|
5
5
|
import { folded } from "./folded.js";
|
|
@@ -176,11 +176,12 @@ export const taskStoreOver = (context) => {
|
|
|
176
176
|
database
|
|
177
177
|
.prepare("UPDATE tasks SET status = ?, close_reason = ?, close_reason_folded = ?, due_at = ?, closed_at = ?, " +
|
|
178
178
|
"closed_by_type = ?, closed_by_id = ?, updated_at = ?, " +
|
|
179
|
-
"claimed_by = CASE WHEN ?
|
|
180
|
-
"
|
|
179
|
+
"claimed_by = CASE WHEN ? NOT IN ('done', 'dismissed') THEN claimed_by END, " +
|
|
180
|
+
"claimed_until = CASE WHEN ? NOT IN ('done', 'dismissed') THEN claimed_until END, " +
|
|
181
|
+
"close_certainty = CASE WHEN ? NOT IN ('done', 'dismissed') THEN NULL ELSE close_certainty END " +
|
|
181
182
|
"WHERE id = ?")
|
|
182
183
|
.run(task.state, task.reason ?? null, folded(task.reason), time(task.dueAt), time(task.closedAt), closedBy?.type ?? null, closedBy?.id ?? null, at, task.state, task.state, task.state, row);
|
|
183
|
-
if (task.state
|
|
184
|
+
if (isClosedState(task.state))
|
|
184
185
|
database.prepare(`UPDATE tasks SET ${CLEAR_SUGGESTION} WHERE id = ?`).run(row);
|
|
185
186
|
if (before.status !== task.state)
|
|
186
187
|
recordTaskEvent(database, row, closedBy ?? null, "status", { status: [before.status, task.state] }, at);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import type { QueryAst } from "@wirecat/cli-base/query";
|
|
1
2
|
import { TASK_TYPES, WORK_TASK_TYPES } from "../../domain/knowledge.js";
|
|
2
3
|
import { type TaskState, type TaskVerdict } from "../../tasks/index.js";
|
|
3
4
|
import type { CacheDatabase } from "../driver.js";
|
|
4
5
|
import type { Actor } from "./actors.js";
|
|
5
6
|
import { type Author } from "./memories.js";
|
|
6
7
|
import type { StoreContext } from "./open.js";
|
|
7
|
-
import type
|
|
8
|
+
import { type Where } from "./query-filter.js";
|
|
9
|
+
import { type TaskSort } from "./task-board.js";
|
|
8
10
|
import { type CloseSuggestion } from "./task-close.js";
|
|
9
11
|
import { type TaskRelationsStore } from "./task-relations.js";
|
|
10
12
|
export declare const TASK_ROLES: readonly ["assignee", "reviewer", "watcher"];
|
|
@@ -69,9 +71,43 @@ export interface TaskDetails {
|
|
|
69
71
|
createdAt: string;
|
|
70
72
|
updatedAt: string;
|
|
71
73
|
}
|
|
74
|
+
export interface BoardOptions {
|
|
75
|
+
limit: number;
|
|
76
|
+
offset?: number;
|
|
77
|
+
sort?: TaskSort;
|
|
78
|
+
descending?: boolean;
|
|
79
|
+
/** The zone `due:today` and bare dates are read in. */
|
|
80
|
+
zone?: string;
|
|
81
|
+
}
|
|
82
|
+
/** How many tasks match, and how many hold each value: the board's pickers show these beside their choices. */
|
|
83
|
+
export interface BoardCounts {
|
|
84
|
+
total: number;
|
|
85
|
+
project: {
|
|
86
|
+
value: string;
|
|
87
|
+
name: string;
|
|
88
|
+
count: number;
|
|
89
|
+
}[];
|
|
90
|
+
/** `person:<id>`, or `none` for the tasks nobody is assigned. */
|
|
91
|
+
assignee: {
|
|
92
|
+
value: string;
|
|
93
|
+
name: string | null;
|
|
94
|
+
count: number;
|
|
95
|
+
}[];
|
|
96
|
+
priority: {
|
|
97
|
+
value: number | null;
|
|
98
|
+
count: number;
|
|
99
|
+
}[];
|
|
100
|
+
state: {
|
|
101
|
+
value: TaskState;
|
|
102
|
+
count: number;
|
|
103
|
+
}[];
|
|
104
|
+
}
|
|
72
105
|
export interface TaskEvent {
|
|
73
106
|
kind: (typeof TASK_EVENT_KINDS)[number];
|
|
74
|
-
|
|
107
|
+
/** `name`: the person's or the bot's, as the store holds it now. */
|
|
108
|
+
actor: (Actor & {
|
|
109
|
+
name: string | null;
|
|
110
|
+
}) | null;
|
|
75
111
|
changes: Record<string, [unknown, unknown]> | null;
|
|
76
112
|
at: string;
|
|
77
113
|
}
|
|
@@ -100,6 +136,20 @@ export interface WorkTasks extends TaskRelationsStore {
|
|
|
100
136
|
limit?: number;
|
|
101
137
|
}): Promise<TaskDetails[]>;
|
|
102
138
|
events(id: string): Promise<TaskEvent[]>;
|
|
139
|
+
/** The owner's project tasks, every project at once, narrowed by a query over `PROJECT_TASK_FIELDS`. */
|
|
140
|
+
board(query: QueryAst | undefined, options: BoardOptions): Promise<{
|
|
141
|
+
items: TaskDetails[];
|
|
142
|
+
total: number;
|
|
143
|
+
}>;
|
|
144
|
+
/** The owner and everyone who holds a role on a live task, by name: who the board offers to assign. */
|
|
145
|
+
taskPeople(): Promise<{
|
|
146
|
+
person: string;
|
|
147
|
+
name: string | null;
|
|
148
|
+
owner: boolean;
|
|
149
|
+
tasks: number;
|
|
150
|
+
}[]>;
|
|
151
|
+
/** Each count leaves out the query's own terms on that field, so a chosen project does not hide the others. */
|
|
152
|
+
boardCounts(query: QueryAst | undefined, options?: Pick<BoardOptions, "zone">): Promise<BoardCounts>;
|
|
103
153
|
/**
|
|
104
154
|
* Takes an open task for `holder` until `until`, unless someone else holds it now — a lapsed claim holds nothing,
|
|
105
155
|
* and the holder's own claim is extended.
|